Spring boot source code parsing EnvironmentPostProcessor

This article will focus on another extension point environment postprocessor provided by SpringBoot, which allows us to go to any specified directory, load a set of configurations in any way, and give any priority

The configureEnvironment of the prepareEnvironment method has been concluded above. This article continues to look at the third line of code listeners environmentPrepared

    private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
        ConfigurableEnvironment environment = this.getOrCreateEnvironment();
        this.configureEnvironment((ConfigurableEnvironment)environment, applicationArguments.getSourceArgs());
        listeners.environmentPrepared((ConfigurableEnvironment)environment);
        this.bindToSpringApplication((ConfigurableEnvironment)environment);
        if (!this.isCustomEnvironment) {
            environment = (new EnvironmentConverter(this.getClassLoader())).convertEnvironmentIfNecessary((ConfigurableEnvironment)environment, this.deduceEnvironmentClass());
        }
        ConfigurationPropertySources.attach((Environment)environment);
        return (ConfigurableEnvironment)environment;
    }

This listener should still be impressed by those who have read the previous article. There is only one element, eventpublishing runlistener

    public void environmentPrepared(ConfigurableEnvironment environment) {
        Iterator var2 = this.listeners.iterator();

        while(var2.hasNext()) {
            SpringApplicationRunListener listener = (SpringApplicationRunListener)var2.next();
            listener.environmentPrepared(environment);
        }

    }

Therefore, it is equivalent to calling the environmentPrepared method of eventpublishing runlistener to view the implementation of this method

    public void environmentPrepared(ConfigurableEnvironment environment) {
        this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
    }

An event ApplicationEnvironmentPreparedEvent is published through the internal event multicast, and SringApplication and Environment objects are propagated. The publishing process is the same as the previous ApplicationStartingEvent event, so I won't repeat it

There are 7 listeners capturing this event this time

Most of the listeners do some marginal initialization. This article first focuses on one of the more important listeners, configfileapplicationlister, which completes the loading of the configuration file and introduces the EnvironmentPostProcessor to be discussed in this article. It is also another extension point provided by SpringBoot

Directly enter the method onApplicationEvent of ConfigFileApplicationListener to receive events

    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            this.onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent)event);
        }
        if (event instanceof ApplicationPreparedEvent) {
            this.onApplicationPreparedEvent(event);
        }
    }

The event type is ApplicationEnvironmentPreparedEvent. Take the first branch

    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        List<EnvironmentPostProcessor> postProcessors = this.loadPostProcessors();
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        Iterator var3 = postProcessors.iterator();

        while(var3.hasNext()) {
            EnvironmentPostProcessor postProcessor = (EnvironmentPostProcessor)var3.next();
            postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
        }
    }

The first line loadPostProcessors to meta-inf / spring The implementation class of EnvironmentPostProcessor is loaded in factories. The loading method is similar to the process during spring application initialization. Although different API s are called, they are finally loaded through spring factoriesloader Loadfactorynames got spring Contents of factories

    List<EnvironmentPostProcessor> loadPostProcessors() {
        return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, this.getClass().getClassLoader());
    }

Finally, three classes are found, all of which are spring under spring boot In factories

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor

Back to the onApplicationEnvironmentPreparedEvent method, after loading into the list of EnvironmentPostProcessor, ConfigFileApplicationListener adds itself to the collection because it also implements the EnvironmentPostProcessor interface

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {

Then traverse the EnvironmentPostProcessor list, call its postProcessEnvironment method in turn, and pass in Environment and SpringApplication

Let's take a look at what each implementation class of EnvironmentPostProcessor does in turn

SpringApplicationJsonEnvironmentPostProcessor

    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        MutablePropertySources propertySources = environment.getPropertySources();
        propertySources.stream().map(SpringApplicationJsonEnvironmentPostProcessor.JsonPropertyValue::get).filter(Objects::nonNull).findFirst().ifPresent((v) -> {
            this.processJson(environment, v);
        });
    }

First, traverse the PropertySource in the Environment and pass it to the static method get of the internal class JsonPropertyValue in turn

    public static SpringApplicationJsonEnvironmentPostProcessor.JsonPropertyValue get(PropertySource<?> propertySource) {
        String[] var1 = CANDIDATES;
        int var2 = var1.length;

        for(int var3 = 0; var3 < var2; ++var3) {
            String candidate = var1[var3];
            Object value = propertySource.getProperty(candidate);
            if (value != null && value instanceof String && StringUtils.hasLength((String)value)) {
                return new SpringApplicationJsonEnvironmentPostProcessor.JsonPropertyValue(propertySource, candidate, (String)value);
            }
        }

        return null;
    }

CANDIDATES is a final variable, which is initialized when declared

    private static class JsonPropertyValue {
        private static final String[] CANDIDATES = new String[]{"spring.application.json", "SPRING_APPLICATION_JSON"};

That is, for each PropertySource, see if there is a property spring application. JSON or SPRING_APPLICATION_JSON, if any, is converted into JsonPropertyValue and stored. Then take the first one and call the processJson method

    private void processJson(ConfigurableEnvironment environment, SpringApplicationJsonEnvironmentPostProcessor.JsonPropertyValue propertyValue) {
        JsonParser parser = JsonParserFactory.getJsonParser();
        Map<String, Object> map = parser.parseMap(propertyValue.getJson());
        if (!map.isEmpty()) {
            this.addJsonPropertySource(environment, new SpringApplicationJsonEnvironmentPostProcessor.JsonPropertySource(propertyValue, this.flatten(map)));
        }
    }

In this method, the value corresponding to the found property is converted into a map, and then a JsonPropertySource is constructed, which is a subclass of MapPropertySource. The configured group name is spring application. json

	JsonPropertySource(SpringApplicationJsonEnvironmentPostProcessor.JsonPropertyValue 	propertyValue, Map<String, Object> source) {
        super("spring.application.json", source);
        this.propertyValue = propertyValue;
    }

Then it is added to the PropertySource list of Environemtn. If there are jndiProperties in the list, it will be placed behind it. If there are no jndiProperties, it will be placed in systemProperties. If there are none, it will be placed in the head of the list as the configuration with the highest priority

    private void addJsonPropertySource(ConfigurableEnvironment environment, PropertySource<?> source) {
        MutablePropertySources sources = environment.getPropertySources();
        String name = this.findPropertySource(sources);
        if (sources.contains(name)) {
            sources.addBefore(name, source);
        } else {
            sources.addFirst(source);
        }
    }
    private String findPropertySource(MutablePropertySources sources) {
        return ClassUtils.isPresent("org.springframework.web.context.support.StandardServletEnvironment", (ClassLoader)null) && sources.contains("jndiProperties") ? "jndiProperties" : "systemProperties";
    }

SystemEnvironmentPropertySourceEnvironmentPostProcessor

    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        String sourceName = "systemEnvironment";
        PropertySource<?> propertySource = environment.getPropertySources().get(sourceName);
        if (propertySource != null) {
            this.replacePropertySource(environment, sourceName, propertySource);
        }
    }

First take the PropertySource named systemEnvironment from Environment, that is, the environment variable of the system, then call replacePropertySource to make a packaging replacement for it.

    private void replacePropertySource(ConfigurableEnvironment environment, String sourceName, PropertySource<?> propertySource) {
        Map<String, Object> originalSource = (Map)propertySource.getSource();
        SystemEnvironmentPropertySource source = new SystemEnvironmentPropertySourceEnvironmentPostProcessor.OriginAwareSystemEnvironmentPropertySource(sourceName, originalSource);
        environment.getPropertySources().replace(sourceName, source);
    }

The original specific type of systemEnvironment is SystemEnvironmentPropertySource. The replaced type originawaresystemepropertysource here is its subclass, which additionally implements the OriginLookup interface

Therefore, the SystemEnvironmentPropertySourceEnvironmentPostProcessor wraps the system environment variables without affecting the original functions

CloudFoundryVcapEnvironmentPostProcessor

    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        if (CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) {
            Properties properties = new Properties();
            JsonParser jsonParser = JsonParserFactory.getJsonParser();
            this.addWithPrefix(properties, this.getPropertiesFromApplication(environment, jsonParser), "vcap.application.");
            this.addWithPrefix(properties, this.getPropertiesFromServices(environment, jsonParser), "vcap.services.");
            MutablePropertySources propertySources = environment.getPropertySources();
            if (propertySources.contains("commandLineArgs")) {
                propertySources.addAfter("commandLineArgs", new PropertiesPropertySource("vcap", properties));
            } else {
                propertySources.addFirst(new PropertiesPropertySource("vcap", properties));
            }
        }
    }
	CLOUD_FOUNDRY {
        public boolean isActive(Environment environment) {
            return environment.containsProperty("VCAP_APPLICATION") || environment.containsProperty("VCAP_SERVICES");
        }
    },

This postProcessor is mainly for projects that have introduced CloudFound. If vcap is configured_ Application or vcap_ For the services property, add a configuration named vcap to the PropertySource list of the Environment

I haven't seen any projects use CloudFound. It is an open source PAAS. It is said to be the earliest open source cloud platform in the industry. However, like Liquibase mentioned earlier, although it is supported by SpringBoot by default, at least it has a relatively small audience in China

ConfigFileApplicationListener

    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        this.addPropertySources(environment, application.getResourceLoader());
    }

The resourceLoader passed in the parameter can abstract a file at a specified location into a Resource for subsequent parsing, but it is still empty because the attribute is not initialized when the spring application is created

    protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
        RandomValuePropertySource.addToEnvironment(environment);
        (new ConfigFileApplicationListener.Loader(environment, resourceLoader)).load();
    }

The first line adds a PropertySource named random to the Environment

    public static void addToEnvironment(ConfigurableEnvironment environment) {
        environment.getPropertySources().addAfter("systemEnvironment", new RandomValuePropertySource("random"));
        logger.trace("RandomValuePropertySource add to Environment");
    }

The type is RandomValuePropertySource, and the type of storage resource is specified as Random

public class RandomValuePropertySource extends PropertySource<Random> {
    public static final String RANDOM_PROPERTY_SOURCE_NAME = "random";
    private static final String PREFIX = "random.";
    private static final Log logger = LogFactory.getLog(RandomValuePropertySource.class);

    public RandomValuePropertySource(String name) {
        super(name, new Random());
    }

Its function is to provide random number function for some attributes in the configuration file. For example, if we want the system to run on a random port within 5000-6000, we can add the following configuration

server:
    port: ${random.int[5000,6000]}

As for the following line of code, it is really to load the system configuration file, which will be discussed in a separate article later

Customize EnvironmentPostProcessor

Through the above built-in environmentpostprocessors, we have a general understanding of the role of this interface. By customizing the implementation of this interface, we can do whatever we want with the Environment and even change the default configuration of the system. First, do an experiment, customize an EnvironmentPostProcessor and delete the RandomValuePropertySource added above, Make the system lose random number support for configuration files

First, start the project normally. You can see that the ports are randomly generated in 5000-6000

Create a new class MyEnvironmentPostProcessor, implement the EnvironmentPostProcessor interface, and delete the PropertySource named random from the environment

@Order(-2147483637)
public class MyEnvironmentPostProcessor implements EnvironmentPostProcessor {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        MutablePropertySources propertySources = environment.getPropertySources();
        propertySources.remove("random");
    }
}

It should be noted that the EnvironmentPostProcessor is a list that will be executed in a certain order. We need to delete it after ConfigFileApplicationListener adds the random to the environment

ConfigFileApplicationListener implements the Ordered interface and specifies the order as - 2147483638

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {

	......
    private int order = -2147483638;
    
	......
	public int getOrder() {
        return this.order;
    }
    ......

The smaller the Order value, the earlier it will be executed. Therefore, our own class is annotated with @ Order, and the Order is set to - 2147483637, as long as it is larger than ConfigFileApplicationListener

Run again and throw an exception, unable to generate a random number for the port

Of course, this example only shows that the EnvironmentPostProcessor gives us high permission to interfere with the environment configuration during normal startup. In fact, we can use it to handle some public configuration

If we have three services, a, B and C, which depend on the underlying module common. If we want to put some common configurations in the common module and have the highest priority, we can create a new configuration file application common in common Properties, and then define an implementation class CommonEnvironmentPostProcessor to parse the file and load it into the header of the PropertySource list. Of course, note that if you want to make the configuration priority the highest, you should ensure that it is executed at the latest as far as possible, so the order should be set relatively large

@Order(Integer.99999)
@Slf4j
public class CommonEnvironmentPostProcessor implements EnvironmentPostProcessor {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        try {
        	//Define a resource loader
        	ResourceLoader resourceLoader = new DefaultResourceLoader();
        	//Find application common under classpath Properties file, abstracted as Resource
            Resource resource = resourceLoader.getResource("classpath:/application-common.properties");
            if (!resource.exists()) {
                return;
            }
            Properties properties = new Properties();
            //Load configuration from file
            properties.load(new InputStreamReader(resource.getInputStream()));
            PropertiesPropertySource propertiesPropertySource = new PropertiesPropertySource("commonProperties", properties);
            //Get the configuration list in the environment
            MutablePropertySources mutablePropertySources = environment.getPropertySources();
            //Add the current configuration to the top of the list, that is, it has the highest priority
            mutablePropertySources.addFirst(propertiesPropertySource);
        } catch (Exception e) {
            log.error("Failed to load public configuration", e);
        }
    }

}

Keywords: Java Spring Boot

Added by mauri_gato on Sat, 01 Jan 2022 06:12:47 +0200