“This is the 16th day of my participation in the First Challenge 2022. For details: First Challenge 2022.”
Configuration center is an important module when we use microservice architecture. There are many commonly used configuration center components, from the early Spring Cloud Config to Disconf, Apollo, Nacos, etc., which support different functions, product performance and user experience.
Although there are many differences on the function, but they solve the core problem, is undoubtedly the modified configuration file, sometimes in move brick Hydra in the curious immediate effect is how to implement, if let me design and how to implement, so spare a little bit of free time these days, touch the fish pulled out a simple version of the single machine configuration center, Take a look at the results first:
The reason why it is a simple version, first of all, because the core function is only the real-time effect of configuration modification, and the implementation of the code is also very simple, a total of only 8 classes to achieve this core function, take a look at the code structure, the core class is the core package of these 8 classes:
Is it a little curious to see that even though it is a low profile version, it is possible to implement a configuration center with just a few classes? So let’s look at the overall design flow, and then we’ll talk about the code.
Code brief
The following is a brief description of the 8 core classes and post the core code. Some of the classes have long codes, which may not be very friendly to mobile browsing partners. It is suggested to open the computer browser after collection (cheat wave collection, plan to pass!). . In addition, Hydra has uploaded all the code of the project to Git. If you need it, you can go to the end of the text to obtain the address.
1, ScanRunner
ScanRunner implements the CommandLineRunner interface, which ensures that it is executed at the end of the SpringBoot boot, thus ensuring that other beans have been instantiated and put into the container. The name ScanRunner comes from the fact that it’s all about scanning classes. Take a look at the code:
@Component
public class ScanRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
doScanComponent();
}
private void doScanComponent(a){
String rootPath = this.getClass().getResource("/").getPath();
List<String> fileList = FileScanner.findFileByType(rootPath,null,FileScanner.TYPE_CLASS);
doFilter(rootPath,fileList);
EnvInitializer.init();
}
private void doFilter(String rootPath, List<String> fileList) {
rootPath = FileScanner.getRealRootPath(rootPath);
for (String fullPath : fileList) {
String shortName = fullPath.replace(rootPath, "")
.replace(FileScanner.TYPE_CLASS,"");
String packageFileName=shortName.replaceAll(Matcher.quoteReplacement(File.separator),"\ \.");
try {
Class clazz = Class.forName(packageFileName);
if(clazz.isAnnotationPresent(Component.class) || clazz.isAnnotationPresent(Controller.class) ||clazz.isAnnotationPresent(Service.class)){ VariablePool.add(clazz); }}catch(ClassNotFoundException e) { e.printStackTrace(); }}}}Copy the code
FileScanner can scan all files in a certain directory according to the suffix of the file. Here first, scan all files in the target directory with the.class end:
Once all the class files have been scanned, you can get the class object using the class’s fully qualified name. The next step is to call the doFilter method to filter the class. For the moment, let’s consider injecting property values in configuration files using @Value annotations. The next question is, what class does the @Value annotation work in? The answer is to pass the @Component, @Controller, and @Service annotations to spring container-managed classes.
In summary, we filter out the qualified classes again through these annotations, and present them to VariablePool for processing.
2, FileScanner
FileScanner is a tool class for scanning files. It can filter out certain types of files according to the file suffix. In addition to scanning class files in ScanRunner, it can also be used to scan YML files in the following logic. In FileScanner, file scanning is implemented in the following code:
public class FileScanner {
public static final String TYPE_CLASS=".class";
public static final String TYPE_YML=".yml";
public static List<String> findFileByType(String rootPath, List<String> fileList,String fileType){
if (fileList==null){
fileList=new ArrayList<>();
}
File rootFile=new File(rootPath);
if(! rootFile.isDirectory()){ addFile(rootFile.getPath(),fileList,fileType); }else{
String[] subFileList = rootFile.list();
for (String file : subFileList) {
String subFilePath=rootPath + "\ \" + file;
File subFile = new File(subFilePath);
if(! subFile.isDirectory()){ addFile(subFile.getPath(),fileList,fileType); }else{ findFileByType(subFilePath,fileList,fileType); }}}return fileList;
}
private static void addFile(String fileName,List<String> fileList,String fileType){
if(fileName.endsWith(fileType)){ fileList.add(fileName); }}public static String getRealRootPath(String rootPath){
if (System.getProperty("os.name").startsWith("Windows")
&& rootPath.startsWith("/")){
rootPath = rootPath.substring(1);
rootPath = rootPath.replaceAll("/", Matcher.quoteReplacement(File.separator));
}
returnrootPath; }}Copy the code
The logic of searching for files is very simple, that is, in the given root directory rootPath, loop through each directory, compare the found files with suffixes, and add them to the returned file name list if they meet the conditions.
As for the following getRealRootPath method, it is because in Windows, the project run directory is obtained like this:
/F:/Workspace/hermit-purple-config/target/classes/
Copy the code
The class filename looks like this:
F:\Workspace\hermit-purple-config\target\classes\com\cn\hermimt\purple\test\service\UserService.class
Copy the code
If you want to get the fully qualified name of a class, remove the run directory and replace the backslash \ in the file name with a dot.
3, VariablePool
Back in the main flow above, each Class scanned by ScanRunner with @Component, @Controller, and @service annotations is handed over to VariablePool for processing. As the name implies, a VariablePool stands for a pool of variables, and this container will be used to encapsulate all attributes annotated with @Value.
public class VariablePool {
public static Map<String, Map<Class,String>> pool=new HashMap<>();
private static final String regex="^ (\ \ $\ \ {) (.). + (\ \}) $";
private static Pattern pattern;
static{
pattern=Pattern.compile(regex);
}
public static void add(Class clazz){
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Value.class)){
Value annotation = field.getAnnotation(Value.class);
String annoValue = annotation.value();
if(! pattern.matcher(annoValue).matches())continue;
annoValue=annoValue.replace("${"."");
annoValue=annoValue.substring(0,annoValue.length()-1);
Map<Class,String> clazzMap = Optional.ofNullable(pool.get(annoValue))
.orElse(newHashMap<>()); clazzMap.put(clazz,field.getName()); pool.put(annoValue,clazzMap); }}}public static Map<String, Map<Class,String>> getPool() {
returnpool; }}Copy the code
A brief description of the design of this piece of code:
- Get by reflection
Class
Object, and determine whether attributes are added@Value
annotations @Value
If you want to inject a value from the configuration file, be sure to conform${xxx}
The format (here is not considered for the time being${xxx:defaultValue}
This format sets the default value), so you need to use the regular expression to verify whether the match, and check after passing the start of the deleteThe ${
And at the end of the}
To obtain the fields in the corresponding configuration fileVariablePool
A static HashMap is declared in theProperties in profile – Class – Properties in classAnd then I’m going to store this relationship herepool
In the
In simple terms, a variable pool looks like this:
Here, for example, we introduce two test services:
@Service
public class UserService {
@Value("${person.name}")
String name;
@Value("${person.age}")
Integer age;
}
@Service
public class UserDeptService {
@Value("${person.name}")
String pname;
}
Copy the code
After the add method is executed on all classes, the data in the variable pool looks like this:
In the pool, the inner Map corresponding to person.name contains two data fields: UserService’s name field and UserDeptService’s pname field.
4, EnvInitializer
After all variable data is encapsulated in VariablePool, ScanRunner calls the init method of EnvInitializer to parse the yML file and initialize the configuration center environment. In plain English, the environment is a static HashMap, where the key is the name of the property and the value is the value of the property.
public class EnvInitializer {
private static Map<String,Object> envMap=new HashMap<>();
public static void init(a){
String rootPath = EnvInitializer.class.getResource("/").getPath();
List<String> fileList = FileScanner.findFileByType(rootPath,null,FileScanner.TYPE_YML);
for (String ymlFilePath : fileList) {
rootPath = FileScanner.getRealRootPath(rootPath);
ymlFilePath = ymlFilePath.replace(rootPath, "");
YamlMapFactoryBean yamlMapFb = new YamlMapFactoryBean();
yamlMapFb.setResources(new ClassPathResource(ymlFilePath));
Map<String, Object> map = yamlMapFb.getObject();
YamlConverter.doConvert(map,null,envMap); }}public static void setEnvMap(Map<String, Object> envMap) {
EnvInitializer.envMap = envMap;
}
public static Map<String, Object> getEnvMap(a) {
returnenvMap; }}Copy the code
First, FileScanner is used to scan all. Yml files in the root directory, and YamlMapFactoryBean of Spring is used to parse yML files. However, there is a problem here. After all YML files are parsed, an independent Map is generated, which needs to be merged to generate a configuration information table. As for this piece of specific operations, are handed over to the following YamlConverter for processing.
To demonstrate this, prepare two yML files: application.yml
spring:
application:
name: hermit-purple
server:
port: 6879
person:
name: Hydra
age: 18
Copy the code
Configuration file 2: config/test.yml
my:
name: John
friend:
name: Jay
sex: male
run: yeah
Copy the code
After the environment is initialized, the generated data format looks like this:
5, YamlConverter
YamlConverter mainly implements three methods:
doConvert()
Will:EnvInitializer
The multiple maps provided in themonoToMultiLayer()
: Convert a single-layer Map to a multi-layer Map (to generate yML format strings)convert()
: yML string parsed to Map (to determine if attributes have changed)
Since the latter two functions are not covered for now, let’s take a look at the first code:
public class YamlConverter {
public static void doConvert(Map<String,Object> map,String parentKey,Map<String,Object> propertiesMap){
String prefix=(Objects.isNull(parentKey))?"":parentKey+".";
map.forEach((key,value)->{
if (value instanceof Map){
doConvert((Map)value,prefix+key,propertiesMap);
}else{ propertiesMap.put(prefix+key,value); }}); }/ /...
}
Copy the code
The logic is also simple. Multiple maps are merged into the destination envMap through a loop, and if multiple maps are nested, the key of each Map is passed through the point. The concatenation results in a single-layer Map of the style shown in the above image.
The remaining two methods will be discussed in the scenario we use below.
6, ConfigController
ConfigController, as a controller, interacts with the front end and has only two interfaces, save and GET, as described below.
get
The front page, when opened, invokes the Get interface in ConfigController and populates the textArea. Let’s look at the implementation of the get method:
@GetMapping("get")
public String get(a){
ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
String yamlContent = null;
try {
Map<String, Object> envMap = EnvInitializer.getEnvMap();
Map<String, Object> map = YamlConverter.monoToMultiLayer(envMap, null);
yamlContent = objectMapper.writeValueAsString(map);
} catch (Exception e) {
e.printStackTrace();
}
return yamlContent;
}
Copy the code
The configuration file attributes were encapsulated into envMap of EnvInitializer during project startup. The envMap is a single-layer Map with no nesting relationship. But here we are using Jackson to generate a yML document in standard format, which is not adequate and we need to restore it to a hierarchical multi-layer Map by calling YamlConverter’s monoToMultiLayer() method.
The monoToMultiLayer() method is a bit too long to post here, based on the key. Split and continuously create sub-level Map. The multi-layer Map data obtained after conversion is as follows:
After obtaining the Map in this format, we can call the Jackson method to convert the Map to a string in YML format and pass it to the front end. Look at the string returned to the front end after processing:
save
After modifying the YML content on the front page, click Save, the save method will be called to save and update the configuration. The implementation of the method is as follows:
@PostMapping("save")
public String save(@RequestBody Map<String,Object> newValue) {
String ymlContent =(String) newValue.get("yml");
PropertyTrigger.change(ymlContent);
return "success";
}
Copy the code
After receiving the YML string passed from the front end, the change method of PropertyTrigger is called to implement the subsequent change logic.
7, PropertyTrigger
After calling the change method, there are two main things you do:
- Modify the
EnvInitializer
In the environmentenvMap
, used to return new data when the front page is refreshed, and for comparison when the next property changes - Modifying the value of the properties in the bean is also the most important function of the entire configuration center
Take a look at the code:
public class PropertyTrigger {
public static void change(String ymlContent) { Map<String, Object> newMap = YamlConverter.convert(ymlContent); Map<String, Object> oldMap = EnvInitializer.getEnvMap(); oldMap.keySet().stream() .filter(key->newMap.containsKey(key)) .filter(key->! newMap.get(key).equals(oldMap.get(key))) .forEach(key->{ System.out.println(key); Object newVal = newMap.get(key); oldMap.put(key, newVal); doChange(key,newVal); }); EnvInitializer.setEnvMap(oldMap); }private static void doChange(String propertyName, Object newValue) {
System.out.println("newValue:"+newValue);
Map<String, Map<Class, String>> pool = VariablePool.getPool();
Map<Class, String> classProMap = pool.get(propertyName);
classProMap.forEach((clazzName,realPropertyName)->{
try {
Object bean = SpringContextUtil.getBean(clazzName);
Field field = clazzName.getDeclaredField(realPropertyName);
field.setAccessible(true);
field.set(bean, newValue);
} catch(NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); }}); }}Copy the code
The previous paving so much, in fact, is to achieve the function of this code, the specific logic is as follows:
- call
YamlConverter
theconvert
Method, the yML format string parsing from the front end is encapsulated into a single layer Map, data format andEnvInitializer
In theenvMap
The same - Traverse the old
envMap
Check whether the corresponding attribute value of the key is changed in the new Map. If no change is made, no subsequent operations are performed - If it changes, replace it with the new value
envMap
The old value - By attribute name, from
VariablePool
Get something that involves changeClass
, and fields in the classField
. And go through the backSpringContextUtil
Gets the bean instance object and changes the value of the field through reflection - Write the modified Map back
EnvInitializer
In theenvMap
At this point, all the functionality is complete.
8 SpringContextUtil.
SpringContextUtil gets the Spring container by implementing the ApplicationContextAware interface, and the container’s getBean() method makes it easy to get the beans in spring for subsequent changes.
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public static <T> T getBean(Class<T> t) {
returnapplicationContext.getBean(t); }}Copy the code
9. Front-end code
As for the front-end code, it is a very simple form, you can go to Git to see the code.
The last
Here all the code is introduced, finally do a brief summary, although through these several classes can achieve a simple version of the configuration center function, but there are many defects, such as:
- No processing
@ConfigurationProperties
annotations - Only yML files are processed, not properties files
- The beans currently being processed are based on
singleton
Mode if the scope isprototype
There will also be problems - Reflection performance is low. If a property involves many classes, performance will be affected
- Currently, the code can only be embedded into the project, and independent deployment and remote registration are not supported
- …
In general, there are still many points to be improved in the follow-up, which really feels like a long way to go.
Finally, we will talk about the name of the project, why was it named hermit Purple? It comes from jojo’s double hermit purple. It seems that the ability of this double matches the perception function of the configuration center, so we used this ha ha.
I’m Hydra and I’ll see you next time.
Project Git address:
Github.com/trunks2008/…
If you have any suggestions or good ideas about the code, please leave a message in the background or add my wechat friends to discuss.
The last
If you feel helpful, you can click a “like” ah, thank you very much ~
Nongcanshang, an interesting, in-depth and direct public account that loves sharing, will talk to you about technology. Welcome to Hydra as a “like” friend.