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); } } }