Principle and practice of custom externalization extension mechanism in Spring Cloud

Spring Cloud has enhanced the attribute source function of Environment,

In the spring cloud context package, the PropertySourceLocator interface is provided to implement the extension of property file loading. We can extend our external configuration loading through this interface. This interface is defined as follows

public interface PropertySourceLocator {

   /**
    * @param environment The current Environment.
    * @return A PropertySource, or null if there is none.
    * @throws IllegalStateException if there is a fail-fast condition.
    */
   PropertySource<?> locate(Environment environment);
}

The abstract method locate needs to return a PropertySource object.

This PropertySource is the property source stored in the Environment. In other words, if we want to implement custom externalized configuration loading, we only need to implement this interface and return PropertySource.

According to this idea, we follow the following steps to realize the custom loading of externalized configuration.

Custom PropertySource

Since the PropertySourceLocator needs to return a PropertySource, we must define our own PropertySource to obtain the configuration source from the outside.

GpDefineMapPropertySource represents a class that takes the Map result as the property source.

/**
 * Gobo education, ToBeBetterMan
 * Mic Teacher wechat: mic4096
 * WeChat official account: following Mic learning framework
 * https://ke.gupaoedu.cn
 * Use Map as the attribute source.
 **/
public class GpDefineMapPropertySource extends MapPropertySource {
    /**
     * Create a new {@code MapPropertySource} with the given name and {@code Map}.
     *
     * @param name   the associated name
     * @param source the Map source (without {@code null} values in order to get
     *               consistent {@link #getProperty} and {@link #containsProperty} behavior)
     */
    public GpDefineMapPropertySource(String name, Map<String, Object> source) {
        super(name, source);
    }

    @Override
    public Object getProperty(String name) {
        return super.getProperty(name);
    }

    @Override
    public String[] getPropertyNames() {
        return super.getPropertyNames();
    }
}

Extend PropertySourceLocator

Extend PropertySourceLocator and override locate to provide property source.

The attribute source is from Gupao The JSON file is loaded and saved into the custom property source GpDefineMapPropertySource.

public class GpJsonPropertySourceLocator implements PropertySourceLocator {

    //json data source
    private final static String DEFAULT_LOCATION="classpath:gupao.json";
    //Resource loader
    private final ResourceLoader resourceLoader=new DefaultResourceLoader(getClass().getClassLoader());

    @Override
    public PropertySource<?> locate(Environment environment) {
        //Set attribute source
        GpDefineMapPropertySource jsonPropertySource=new GpDefineMapPropertySource
                ("gpJsonConfig",mapPropertySource());
        return jsonPropertySource;
    }

    private Map<String,Object> mapPropertySource(){
        Resource resource=this.resourceLoader.getResource(DEFAULT_LOCATION);
        if(resource==null){
            return null;
        }
        Map<String,Object> result=new HashMap<>();
        JsonParser parser= JsonParserFactory.getJsonParser();
        Map<String,Object> fileMap=parser.parseMap(readFile(resource));
        processNestMap("",result,fileMap);
        return result;
    }
    //Load file and parse
    private String readFile(Resource resource){
        FileInputStream fileInputStream=null;
        try {
            fileInputStream=new FileInputStream(resource.getFile());
            byte[] readByte=new byte[(int)resource.getFile().length()];
            fileInputStream.read(readByte);
            return new String(readByte,"UTF-8");
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(fileInputStream!=null){
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
    //Save the parsed url to the complete set of result s. To implement @ Value injection
    private void processNestMap(String prefix,Map<String,Object> result,Map<String,Object> fileMap){
        if(prefix.length()>0){
            prefix+=".";
        }
        for (Map.Entry<String, Object> entrySet : fileMap.entrySet()) {
            if (entrySet.getValue() instanceof Map) {
                processNestMap(prefix + entrySet.getKey(), result, (Map<String, Object>) entrySet.getValue());
            } else {
                result.put(prefix + entrySet.getKey(), entrySet.getValue());
            }
        }
    }
}

Spring.factories

At / meta-inf / spring In the factories file, add the following spi extension to enable Spring Cloud to scan this extension when it starts, so as to load GpJsonPropertySourceLocator.

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
  com.gupaoedu.env.GpJsonPropertySourceLocator

Write controller test

@RestController
public class ConfigController {
    @Value("${custom.property.message}")
    private String name;
    
    @GetMapping("/")
    public String get(){
        String msg=String.format("Configuration value:%s",name);
        return msg;
    }
}

Phased summary

Through the above cases, it can be found that the extension of custom configuration source can be easily realized based on the PropertySourceLocator extension mechanism provided by Spring Boot.

Thus, two questions arise.

  1. Where was the PropertySourceLocator triggered?
  2. Since we can get from Gupao If the data source is loaded in JSON, can it be loaded from the remote server?

Loading principle of PropertySourceLocator

Let's explore the first problem, the execution process of PropertySourceLocator.

SpringApplication.run

When the spring boot project starts, there is a prepareContext method, which will call back all instances that implement ApplicationContextInitializer to do some initialization.

ApplicationContextInitializer is the original thing of Spring framework. Its main function is to allow us to further set and process the instance of ConfigurableApplicationContext before the ApplicationContext of ConfigurableApplicationContext type (or subtype) is refresh ed.

It can be used in web applications that need to programmatically initialize the application context, such as registering a propertySource or a configuration file according to the context. The requirements of the configuration center of Config just need such a mechanism to complete.

public ConfigurableApplicationContext run(String... args) {
   //Omit code
        prepareContext(context, environment, listeners, applicationArguments, printedBanner);
   //Omit code
    return context;
}

PropertySourceBootstrapConfiguration.initialize

Among them, PropertySourceBootstrapConfiguration implements applicationcontext initialize r, and the initialization method code is as follows.

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
    List<PropertySource<?>> composite = new ArrayList<>();
    //Sort the propertySourceLocators array according to the default AnnotationAwareOrderComparator
    AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
    boolean empty = true;
    //Gets the context of the running environment
    ConfigurableEnvironment environment = applicationContext.getEnvironment();
    for (PropertySourceLocator locator : this.propertySourceLocators) {
        //Call back all the locate methods that implement the PropertySourceLocator interface instance and collect them into the source collection.
        Collection<PropertySource<?>> source = locator.locateCollection(environment);
        if (source == null || source.size() == 0) { //If source is empty, go directly to the next cycle
            continue;
        }
        //Traverse the source, wrap the PropertySource as bootstrap PropertySource and add it to the sourceList.
        List<PropertySource<?>> sourceList = new ArrayList<>();
        for (PropertySource<?> p : source) {
            sourceList.add(new BootstrapPropertySource<>(p));
        }
        logger.info("Located property source: " + sourceList);
        composite.addAll(sourceList);//Add source to array
        empty = false; //Indicates that propertysource is not empty
    }
     //Only when propertysource is not empty can it be set in the environment
    if (!empty) {
        //Get all PropertySources in the current Environment
        MutablePropertySources propertySources = environment.getPropertySources();
        String logConfig = environment.resolvePlaceholders("${logging.config:}");
        LogFile logFile = LogFile.get(environment);
       // Traverse and remove the relevant properties of bootstrap property
        for (PropertySource<?> p : environment.getPropertySources()) {
            
            if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
                propertySources.remove(p.getName());
            }
        }
        //Insert the previously obtained PropertySource into PropertySources in the Environment.
        insertPropertySources(propertySources, composite);
        reinitializeLoggingSystem(environment, logConfig, logFile);
        setLogLevels(applicationContext, environment);
        handleIncludedProfiles(environment);
    }
}

The logic of the above code is described as follows.

  1. First, this PropertySourceLocators refers to all implementation classes that implement the PropertySourceLocators interface, including the GpJsonPropertySourceLocator we customized earlier.
  2. Sort the propertySourceLocators array according to the default AnnotationAwareOrderComparator collation.
  3. Gets the running environment context ConfigurableEnvironment
  4. Error traversing propertySourceLocators
  • Call the locate method and pass in the obtained context environment
  • Add source to the linked list of PropertySource
  • Set whether the source is empty or not
  1. Only when the source is not empty can it be set in the environment
  • Returns the variable form of Environment, and the available operations such as addFirst and addLast
  • Remove bootstrap properties from propertySources
  • Set propertySources according to the rules overridden by config server
  • Processing configuration information of multiple active profiles

Note: this The propertysourcelocators in the propertysourcelocators collection are injected through the automatic assembly mechanism. The specific implementation is in the bootstrap importselector class.

Understanding and use of ApplicationContextInitializer

ApplicationContextInitializer is the original thing of Spring framework. Its main function is to allow us to further set and process the instance of ConfigurableApplicationContext before the ApplicationContext of ConfigurableApplicationContext type (or subtype) is refresh ed.

It can be used in web applications that need to programmatically initialize the application context, such as registering a propertySource or a configuration file according to the context. The requirements of the configuration center of Config just need such a mechanism to complete.

Create a TestApplicationContextInitializer

public class TestApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>{
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        ConfigurableEnvironment ce=applicationContext.getEnvironment();
        for(PropertySource<?> propertySource:ce.getPropertySources()){
            System.out.println(propertySource);
        }
        System.out.println("--------end");
    }
}

Add spi load

Create a file / resources / meta-inf / spring factories. Add the following

org.springframework.context.ApplicationContextInitializer= \
  com.gupaoedu.example.springcloudconfigserver9091.TestApplicationContextInitializer

You can see the output of the current PropertySource on the console.

ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
StubPropertySource {name='servletConfigInitParams'}
StubPropertySource {name='servletContextInitParams'}
PropertiesPropertySource {name='systemProperties'}
OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
MapPropertySource {name='configServerClient'}
MapPropertySource {name='springCloudClientHostInfo'}
OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/application.yml]'}
MapPropertySource {name='kafkaBinderDefaultProperties'}
MapPropertySource {name='defaultProperties'}
MapPropertySource {name='springCloudDefaultProperties'}

Copyright notice: unless otherwise stated, all articles on this blog adopt CC BY-NC-SA 4.0 license agreement. Reprint please indicate from Mic to take you to learn architecture!
If this article is helpful to you, please pay attention and praise. Your persistence is the driving force of my continuous creation. Welcome to WeChat public official account for more dry cargo.

Keywords: Java

Added by ChrisLeah on Fri, 25 Feb 2022 08:26:51 +0200