demand

Unconsciously, Web development has entered the era of “micro services” and “distributed”. Spring, which is committed to providing universal Java development solutions, naturally does not want to be left behind. Spring Cloud is proposed to expand the influence of Spring in micro services, which has also been recognized by the market. We also have applications in our business.

A few days ago, I was also having problems with Spring Cloud in a requirement. What we are using is the Config module of Spring Cloud, which is used to support distributed configuration. After using Spring Cloud, the original stand-alone configuration can support dynamic modification and reloading of third-party storage configuration and configuration, and the reloading of configuration can be realized in the business code by ourselves. Spring Cloud decouples the entire process into a framework and is well integrated into Spring’s existing configuration and Bean modules.

Although I took some detours in solving the requirements problem, I also took the opportunity to understand part of Spring Cloud. Take time to summarize the problem and the knowledge learned in the query problem, and share it so that students who encounter this problem again will not step on the pit.

This article is based on Spring 5.0.5, Spring Boot 2.0.1, and Spring Cloud 2.0.2.

Background and questions

Our service originally had a batch of stand-alone configurations that were too long for the same key, so it was configured as an array and resolved to Bean properties using Spring Boot’s @ConfigurationProperties and @Value annotations.

SpringBoot basic tutorial will not introduce, not small partners, I recommend a set of I have seen the power node video, very good, click on the following link 👇 :

Watch online:

www.bilibili.com/video/BV1PZ…

Information download:

www.bjpowernode.com/javavideo/1…

Properties file configuration like:

test.config.elements[0]=value1
test.config.elements[1]=value2
test.config.elements[2]=value3
Copy the code

In use:

@ConfigurationProperties(prefix="test.config")
Class Test{
    @Value("${#elements}")
    private String[] elements;
}
Copy the code

In this way, Spring will automatically inject the Test class, injecting the array [value1,value2,value3] into the Elements property.

Our posture with Spring Cloud for auto-loading configuration is as follows:

@RefreshScope
class Test{
    @Value("${test.config.elements}")
    private String[] elements;
}
Copy the code

A class using the @refreshScope annotation, which automatically reloads when the environment variable changes to inject the latest property into the class property, does not support automatic injection of arrays.

My goal was to find a way to inject array-type properties while using Spring Cloud’s auto-refresh configuration feature.

Environment and Attributes

No matter how great the features of Spring Cloud are, when it comes to Spring’s turf, do as the Romans do, and get along with the basic components of Spring. So to understand the process, we need to understand the basics of Spring.

Spring is a large container that stores not only beans and their dependencies, but also the configuration of the entire application. In contrast to BeanFactory, which stores various beans, Spring’s container for managing Environment configuration is Environment. We can get all the configurations by key, and we can switch configurations by scenario (Profile, such as dev,test,prod).

But the smallest unit of Spring management configuration is not a property, but an PropertySource, which we can understand as a file, or some configuration data table, Spring maintains an PropertySourceList in the Environment, and when we fetch the configuration, Spring looks up the corresponding value from these PropertySource, ConversionService is used to convert the value to the corresponding type.

Spring Cloud configuration refresh mechanism

Distributed configuration

The latest Spring tutorial is here!

Watch online:

www.bilibili.com/video/BV1nz…

Information download:

www.bjpowernode.com/javavideo/1…

Spring Cloud provides the PropertySourceLocator interface to connect with Spring’s PropertySource system. Through PropertySourceLocator, We will get a “custom” PropertySource, Spring is there an implementation in the Cloud ConfigServicePropertySourceLocator, through it, we can define a remote ConfigService, The distributed configuration service is implemented by sharing this ConfigService.

As you can see from the ConfigClientProperties configuration class, it also presets security controls such as usernames and passwords for remote configurations, and labels to distinguish configurations such as service pools.

Scope Configuration Refresh

With remote configuration in place, the next step is to monitor changes and refresh based on configuration changes. Spring Cloud provides ContextRefresher to help us refresh the environment. The main logic is in the refreshEnvironment method and scope.refreshall () method, which we’ll look at separately. Let’s start with the Scope.refreshall method supported by Spring Cloud.

public void refreshAll() {
 super.destroy();
 this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
Copy the code

Scope. RefreshAll was a bit more “barbaric” and directly destroy the scope, and issued a RefreshScopeRefreshedEvent events, Destruction of scope causes all beans within scope (annotated by RefreshScope) to be destroyed. When the beans that are forced to be lazyInit are created again, the new configuration is reloaded.

ConfigurationProperties Configuration refresh

Then come back to the refreshEnvironment method.

Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
addConfigFilesToEnvironment();
Set<String> keys = changes(before,extract(this.context.getEnvironment().getPropertySources())).keySet();
this.context.publishEvent(new EnvironmentChangeEvent(context, keys));
return keys;
Copy the code

After reading the configuration in all the PropertySource within the environment, it creates a New SpringApplication to refresh the configuration, reads all the configuration items again and compares them to the previously saved configuration items, Finally, an EnvironmentChangeEvent is published for the configuration difference. And EnvironmentChangeEvent listeners are implemented by ConfigurationPropertiesRebinder, its main logic in the rebind method.

Object bean = this.applicationContext.getBean(name); if (AopUtils.isAopProxy(bean)) { bean = ProxyUtils.getTargetObject(bean); } if (bean ! = null) { this.applicationContext.getAutowireCapableBeanFactory().destroyBean(bean); this.applicationContext.getAutowireCapableBeanFactory().initializeBean(bean, name); return true;Copy the code

Can see the processing logic, that is, combine the internal storage ConfigurationPropertiesBeans in destruction of logic, to perform initialization logic implement rebinding properties. As you can see, Spring Cloud considers ConfigurationProperties during configuration refreshes. After testing, after ContextRefresher refreshes the context, The properties of the ConfigurationProperties annotation class are dynamically refreshed.

Something that can be solved in one test feels like a waste of time.

But now that we’re here, let’s dig a little deeper.

Bean creation and environment

Next, let’s look at how the attributes in the environment are used during Bean creation.

As we know, the Spring beans are created within the BeanFactory AbstractBeanFactory. The entrance to the create logical doGetBean (name, requiredType, args, false) method, And specific implementation in AbstractAutowireCapableBeanFactory. DoCreateBean method, in this method, has realized the Bean instance creation, fill, such as initialization method invocation logic.

One very complicated step here is to call the global BeanPostProcessor, which is Spring’s hook subinterface for Bean creation. Classes that implement this interface can modify the operations that the Bean does when it is created. It is a very important interface and an important entry point through which we can interfere with the Spring Bean creation process.

We want to say is it a specific implementation ConfigurationPropertiesBindingPostProcessor, It by calling the chain ConfigurationPropertiesBinder. The bind () – > Binder. The bindObject () – > Binder. The findProperty () method to find the environment attributes.

private ConfigurationProperty findProperty(ConfigurationPropertyName name,
  Context context) {
 if (name.isEmpty()) {
  return null;
 }
 return context.streamSources()
   .map((source) -> source.getConfigurationProperty(name))
   .filter(Objects::nonNull).findFirst().orElse(null);
}
Copy the code

After finding the corresponding property, use Converter to convert the property to the corresponding type and inject it into the Bean bone.

private <T> Object bindProperty(Bindable<T> target, Context context,
  ConfigurationProperty property) {
 context.setConfigurationProperty(property);
 Object result = property.getValue();
 result = this.placeholdersResolver.resolvePlaceholders(result);
 result = context.getConverter().convert(result, target);
 return result;
}
Copy the code

A trick

As you can see, Spring supports dynamic changes to the @ConfigurationProperties property, but I found a trick trick way to query the flow.

Let’s first sort out the key points for dynamic property injection and then look for modifiable points from those key points.

  1. The PropertySourceLocator brings in the PropertySource from the remote data source, and if we can change the result of the data source, But Spring Cloud of a remote resource locator ConfigServicePropertySourceLocator and remote call tools RestTemplate are implementation class, if stiffly on its inheritance and change, the code is not very elegant.
  2. Bean creation in turn uses BeanPostProcessor to manipulate the context. Add a BeanPostProcessor to manually modify the Bean properties. But this approach is complex to implement, and since every BeanPostProcessor is called when all beans are created, there can be security issues.
  3. When Spring addresses class property injection, it uses PropertyResolver to resolve configuration items to the type specified by class properties. Add a PropertyResolver or a type converter ConversionService to handle the properties. But they are only responsible for handling one property, and since my goal is to have “multiple” properties become one property, they can’t do much about it either.

One way I can think of is to use Spring’s auto-injection capabilities to inject an Environment Bean into a class and then modify the PropertySource within the Environment in the class’s initialization method. I’m going to put some pseudo-code here.

@ Component @ RefreshScope / / borrow Spring Cloud implement this Bean refresh the public class ListSupportPropertyResolver {@autowired ConfigurableEnvironment env; @postconstruct public void init() {// Map<String, Object> properties = extract(env.getPropertySources()); // Parse the array in the environment, Map<String, List<String>> listProperties = collectListProperties(properties) Object> propertiesMap = new HashMap<>(listProperties); MutablePropertySources propertySources = env.getPropertySources(); // The array configuration generates a PropertySource and puts it inside the environment's PropertySourceList propertysources.addFirst (new MapPropertySource("modifiedProperties", propertiesMap)); }}Copy the code

This way, we can use our modified PropertySource first priority when creating the Bean.

Of course, with a more “formal” approach, we don’t need to make changes to the PropertySource, since global changes equate to unknown risks or pothole.

summary

In the process of searching for the answer, I got a deeper understanding that Environment and BeanFactory are the cornerstones of Spring, and various fancy functions provided by the framework are realized based on them. Mastering these knowledge is very helpful for understanding the advanced features displayed by it. It’s also more directional to look for framework problems later.

Java Technology Stack (ID: Javastack)

Prohibit second reprint, reprint please contact authorizing.