Contents: 0. Preface

  1. Treatment scheme
  2. A simple example

preface

Sometimes, you may need to put some configuration in the Spring environment, but it cannot be written to the configuration file, only put in at runtime. So, what to do at this point?

Apollo is engaged in configuration, so naturally encountered this problem, how did he deal with it?

Treatment scheme

First, what is a configured data structure in the Spring environment?

The abstract class PropertySource

contains a key value structure. This T can be of any type, depending on the design of the subclass.

Subclasses can get the configuration by overriding the getProperty abstract method.

Spring’s own org. Springframework. Core. The env. MapPropertySource will rewrite the method.

public class MapPropertySource extends EnumerablePropertySource<Map<String.Object>> {

	public MapPropertySource(String name, Map<String, Object> source) {
		super(name, source);
	}


	@Override
	public Object getProperty(String name) {
		return this.source.get(name);
	}

	@Override
	public boolean containsProperty(String name) {
		return this.source.containsKey(name);
	}

	@Override
	public String[] getPropertyNames() {
		return StringUtils.toStringArray(this.source.keySet()); }}Copy the code

As you can see, his generic type is Map, and the getProperty method is taken from Map.

Apollo uses this class directly.

Two different subclasses, different refresh logic. We don’t care about their differences for the moment.

Both of these classes are combined by RefreshableConfig and added to the Spring environment.

import org.springframework.core.env.ConfigurableEnvironment;

public abstract class RefreshableConfig {

  @Autowired
  private ConfigurableEnvironment environment; / / Spring environment

  @PostConstruct
  public void setup(a) {
  // omit the code
    for (RefreshablePropertySource propertySource : propertySources) {
      propertySource.refresh();
      // Note: After a successful refresh, place it in the Spring environment
      environment.getPropertySources().addLast(propertySource);
    }
  // omit the code
Copy the code

When getting the configuration from the Spring environment, the code looks like this:

protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
		for(PropertySource<? > propertySource :this.propertySources) {
            / / note: this is called propertySource. The getProperty method, subclasses just rewrite method
			Object value = propertySource.getProperty(key);
             // omit extraneous code........
			return convertValueIfNecessary(value, targetValueType);
		}
	    return null;
}
Copy the code

Spring maintains a collection of PropertySource, and the combination is sequential, that is, the first in line has the highest priority (traversal starts at subscript 0).

The user can maintain a configuration dictionary (Map) within PropertySource, which is a data structure similar to a two-dimensional array.

Therefore, configurations can be duplicated, using the previous configuration in PropertySource. So, Spring leaves a few apis:

  1. addFirst(PropertySource
    propertySource)
  2. addLast(PropertySource
    propertySource)
  3. addBefore(String relativePropertySourceName, PropertySource
    propertySource)
  4. addAfter(String relativePropertySourceName, PropertySource
    propertySource)

As the name suggests, with these apis we can insert the propertySource where we specify it. In this way, you can manually control the configured priority.

Spring has a CompositePropertySource class that internally aggregates a PropertySource Set that is iterated over when getProperty(String Name) is invoked, The getProperty(name) method of the propertySource is then called. It’s a 3 dimensional array.

The general design is like this:

There are multiple PS (PropertySource for short) in an environment, and each PS can either contain configuration directly or wrap another layer of PS.

A simple example

Here is a simple example. The requirement is: the application has a configuration, but it cannot be written to the configuration file. It can only be configured during the application startup process, and then injected into the Spring environment, so that Spring can use the configuration in the IOC.

The code is as follows:

@SpringBootApplication
public class DemoApplication {

  @Value("${timeout:1}")
  String timeout;


  public static void main(String[] args) throws InterruptedException {
    ApplicationContext c = SpringApplication.run(DemoApplication.class, args);
    for(; ;) { Thread.sleep(1000); System.out.println(c.getBean(DemoApplication.class).timeout); }}}Copy the code

Application.properties configuration file

timeout=100
Copy the code

In the above code, we define a property timeout in the bean, write a value of 100 in the local configuration file, and give a default value of 1 in the expression.

So now the printed value is the value in the configuration file: 100.

However, this is not what we want, so we need to change the code.

Let’s add a class:

@Component
class Test implements EnvironmentAware.BeanFactoryPostProcessor {

  @Override
  public void setEnvironment(Environment environment) {
    ((ConfigurableEnvironment) environment).getPropertySources()
        // Here is addFirst, which takes precedence over the application.properties configuration
        .addFirst(new PropertySource<String>("timeoutConfig"."12345") {
          / / the key
          @Override
          public Object getProperty(String s) {
            if (s.equals("timeout")) {//
              return source;// Return source :12345 in the constructor
            }
            return null; }}); }@Override
  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
      throws BeansException {
    // NOP}}Copy the code

After running, the result is 12345

2018-07-02 15:26:54.315  INFO 43393 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-07-02 15:26:54.327  INFO 43393 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.991 seconds (JVM running for 1.49)
12345
12345
Copy the code

Why does adding this class replace properties in the configuration file? Explain what this class does.

All we need to do is insert a custom PS object into the Spring environment so that when the container gets it, it can get the configuration using the getProperty method.

So, to get the Spring environment object, we also need to create a PS object and rewrite the getProperty method, and note that our PS configuration needs to take precedence over the container configuration file, just to be safe.

The first argument to the PS constructor is just an identifier, and the second argument is source, and it can be any type, String, Map, whatever, so in this case, it’s just a String, and it returns that value, and if it’s a Map, it calls the Map get method.

Why implement the BeanFactoryPostProcessor interface? The purpose of implementing the BeanFactoryPostProcessor interface is to allow the Bean to load earlier than the target Bean’s initialization. Otherwise, the timeout properties in the target Bean are injected and the subsequent operations are meaningless.

conclusion

In plain English, don’t want to write configuration files!!

And I don’t want to change the code of the old project, which can still use the configuration center even if the configuration file is deleted!

This requires familiarity with Spring’s configuration loading logic and property fetching logic.

Now, we know that we just need to get the Spirng environment objects, add custom PS objects to the environment, and override the PS getProperty method to get the configuration (note the priority).

It is also important to note that the bean that loads this configuration has a high priority as well. Usually implementing the BeanFactoryPostProcessor interface is sufficient, but if not, you need to do something special.