How to dynamically refresh @ ConfigurationProperties?

Demand causes

Recently, when implementing the configuration hot update of the configuration center [Nacos in this example], we encountered the configuration bean marked with @ ConfigurationProperties, which involves the refresh problem. Here is a simple implementation method.

thinking

Follow the following questions to solve the problem of configuration refresh

Let's first verify how the configuration bean marked with @ ConfigurationProperties initializes and loads the configuration?

Because this process has the solution of combining configuration data with bean s

Relying on this scheme, we can deduce that the hot update of configuration is nothing more than the combination of new configuration data and bean s, so most solutions can be reused.

analysis

@Loading of ConfigurationProperties

If a bean can be used, the first step is to register it in spring.

Step 1: how to register the configuration bean marked with @ configurationproperties?

According to the way of use

@EnableConfigurationProperties(GovernanceProperties.class)

You can see that the bean is imported through the @ EnableConfigurationProperties annotation.

Further parse @ EnableConfigurationProperties

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesRegistrar.class)
public @interface EnableConfigurationProperties {
    //Other codes are omitted
}

You can see that EnableConfigurationPropertiesRegistrar is further imported through @ Import

The registration of configuration bean s is implemented in EnableConfigurationPropertiesRegistrar

//EnableConfigurationPropertiesRegistrar.java
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
		registerInfrastructureBeans(registry);
		ConfigurationPropertiesBeanRegistrar beanRegistrar = new     
        ConfigurationPropertiesBeanRegistrar(registry);

        //The most critical code in step 1, where the configuration bean is imported
		getTypes(metadata).forEach(beanRegistrar::register);
}

//ConfigurationPropertiesBeanRegistrar.java

void register(Class<?> type) {
		MergedAnnotation<ConfigurationProperties> annotation = MergedAnnotations
            .from(type,SearchStrategy.TYPE_HIERARCHY).get(ConfigurationProperties.class);

        //Extract @ ConfigurationProperties annotation metadata before registration
		register(type, annotation);
}

void register(Class<?> type, MergedAnnotation<ConfigurationProperties> annotation) {
		String name = getName(type, annotation);
		if (!containsBeanDefinition(name)) {
            //Register bean
			registerBeanDefinition(name, type, annotation);
		}
}

The registration of the configuration bean is completed

After a bean is registered, to use it, you also need to instantiate the bean and fill [inject] its related properties for the bean. After all these work are completed, it is an "available" bean.

Step 2: how do configuration bean s instantiate and inject configuration data?

Spring's life cycle events provide pre and post events such as bean instantiation and initialization. If you want to inject configuration data into specific fields, you must use these events. The qualified spring events are as follows:

//It is mainly responsible for pre and post events of bean instantiation
public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor {}

//It is mainly responsible for pre and post events of bean initialization
public interface BeanPostProcessor {}

After some searching, because there are too many specific implementation classes.

We found ConfigurationPropertiesBindingPostProcessor.java, which completed the configuration data injection into the configuration bean.

The configuration properties binding postprocessor uses bean s to initialize pre and post events

public class ConfigurationPropertiesBindingPostProcessor
		implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware,     InitializingBean {

//Other methods are omitted

    @Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws     
         BeansException {
        
        //The core step of step 2 is to complete the injection of configuration data
		bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
		return bean;
	}
}

  Step 3: the ConfigurationPropertiesBindingPostProcessor is responsible for injecting configuration data. When will the ConfigurationPropertiesBindingPostProcessor be registered?

Review step 1

//EnableConfigurationPropertiesRegistrar.java
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {

        //The injection of ConfigurationPropertiesBindingPostProcessor is completed
		registerInfrastructureBeans(registry);
		ConfigurationPropertiesBeanRegistrar beanRegistrar = new     
        ConfigurationPropertiesBeanRegistrar(registry);
		getTypes(metadata).forEach(beanRegistrar::register);
}

static void registerInfrastructureBeans(BeanDefinitionRegistry registry) {
        //Register ConfigurationPropertiesBindingPostProcessor
		ConfigurationPropertiesBindingPostProcessor.register(registry);

        //Register configuration data injection tool bean
		BoundConfigurationProperties.register(registry);

        //Also register a bean, but it's not too important
		ConfigurationBeanFactoryMetadata.register(registry);
}

Therefore, the ConfigurationPropertiesBindingPostProcessor is imported with the @ EnableConfigurationProperties annotation...

step4: specific details of configuring data injection?

//ConfigurationPropertiesBindingPostProcessor.java


//Inject in step 1
private ConfigurationPropertiesBinder binder;


private void bind(ConfigurationPropertiesBean bean) {

		if (bean == null || hasBoundValueObject(bean.getName())) {
			return;
		}

		Assert.state(bean.getBindMethod() == BindMethod.JAVA_BEAN, "Cannot bind @ConfigurationProperties for bean '"
				+ bean.getName() + "'. Ensure that @ConstructorBinding has not been applied to regular bean");


		try {
            //Use the tool bean injected in step 1 to complete the configuration data binding
            //The method is not expanded internally.
			this.binder.bind(bean);
		}
		catch (Exception ex) {
			throw new ConfigurationPropertiesBindException(bean, ex);
		}
}

Source code analysis so far, continue to post

After this.binder.bind(bean)

The source code is meaningless. Here are only a few core parts

Where does the configuration data come from?   The configuration data is obtained from the spring environment

Does the injection of configuration data ultimately use reflection?    yes

Practice to dynamically refresh @ ConfigurationProperties

By loading @ ConfigurationProperties, we already know how configuration data and bean s are combined. Therefore, we only need to maintain the spring environment when the configuration data is updated, and then call the method of configuration binding again

//Pseudo code
private void flushConfigurationProperties(){


    String bindBeanName = "org.springframework.boot.context.internalConfigurationPropertiesBinder";

    //Get the configuration data and inject the bean. Why not inject it directly? Because it is an internal class and cannot be directly introduced as a class member variable
    Object bean = applicationContext.getBean(bindBeanName);

    //beanName of the configuration bean
    String ConfigurationPropertiesBeanName = "Fill in according to your actual situation";

    //Get configuration bean
    Object ConfigurationPropertiesBean = applicationContext.getBean(ConfigurationPropertiesBeanName);

    //Get the bound method and execute reflection
    ReflectionUtils.doWithLocalMethods(bean.getClass(), m->{
      if(m.getName().equals("bind")){
                m.setAccessible(true);

    //Packaging metadata
ConfigurationPropertiesBean configurationPropertiesBean = ConfigurationPropertiesBean.get(applicationContext,ConfigurationPropertiesBean ,ConfigurationPropertiesBeanName);


      try {
            //Refresh complete
             m.invoke(bean,configurationPropertiesBean);
        }catch (Exception e){

        }
       }
 });
   
}

Keywords: Java Spring

Added by rayner75 on Wed, 27 Oct 2021 12:33:07 +0300