需求起因

最近在实现配置中心配置热更新时,遇到了@ConfigurationProperties标注的配置bean,这就涉及到刷新问题,这里提供一个简便的实现方式。

思路

循着下面的问题,来解决配置刷新的问题

我们先求证,@ConfigurationProperties标注的配置bean是如何初始化,如何加载配置的?

因为这个过程就有配置数据与bean结合的解决方案

我们依托这个方案可以推导出,配置的热更新,无非就是新的配置数据与bean的结合,所以可以复用大部分解决方案。

分析

@ConfigurationProperties的加载

注:基于spring boot  2.3.2.RELEASE

一个bean能够被使用,第一步是需要注册到spring中。

step1:@ConfigurationProperties标注的配置bean如何注册?

根据使用方式

@EnableConfigurationProperties(GovernanceProperties.class)

可知,通过@EnableConfigurationProperties注解导入了bean。

进一步解析@EnableConfigurationProperties

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesRegistrar.class)
public @interface EnableConfigurationProperties {
    //其他代码略
}

可知,进一步通过@Import导入了EnableConfigurationPropertiesRegistrar

在EnableConfigurationPropertiesRegistrar实现了配置bean的注册

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

        //step1中最关键的代码,在此导入了配置bean
		getTypes(metadata).forEach(beanRegistrar::register);
}

//ConfigurationPropertiesBeanRegistrar.java

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

        //注册之前提取@ConfigurationProperties注解元数据
		register(type, annotation);
}

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

此时完成了配置bean的注册

一个bean被注册后,要使用它,还需要实例化这个bean,以及为这个bean填充【注入】它的相关属性,这些工作都完成之后,才是一个”可用“bean。

step2:配置bean如何实例化和注入配置数据?

spring的生命周期事件中,提供了诸如bean实例化前后置事件,初始化前后置事件,如果要注入配置数据到具体字段,一定会利用这些事件。符合条件的spring的事件如下:

//主要负责bean实例化前后置事件
public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor {}

//主要负责bean初始化前后置事件
public interface BeanPostProcessor {}

经过一番查找,因为具体实现类实在太多。

我们查找到了ConfigurationPropertiesBindingPostProcessor.java 他完成了配置数据注入配置bean。

ConfigurationPropertiesBindingPostProcessor的方案是利用bean初始化前后置事件

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

//其他方法略

    @Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws     
         BeansException {
        
        //step2 最核心步骤 这里完成了配置数据的注入
		bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
		return bean;
	}
}

 step3:ConfigurationPropertiesBindingPostProcessor负责注入配置数据,那么ConfigurationPropertiesBindingPostProcessor何时注册?

回顾step1中

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

        //完成了ConfigurationPropertiesBindingPostProcessor的注入
		registerInfrastructureBeans(registry);
		ConfigurationPropertiesBeanRegistrar beanRegistrar = new     
        ConfigurationPropertiesBeanRegistrar(registry);
		getTypes(metadata).forEach(beanRegistrar::register);
}

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

        //注册配置数据注入工具bean
		BoundConfigurationProperties.register(registry);

        //也是注册一个bean,不过不是太重要
		ConfigurationBeanFactoryMetadata.register(registry);
}

所以,ConfigurationPropertiesBindingPostProcessor被@EnableConfigurationProperties注解一起导入了。。。

step4:配置数据注入的具体细节?

//ConfigurationPropertiesBindingPostProcessor.java


//在 步骤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 {
            //利用步骤1中注入的工具bean 完成配置数据绑定
            //该方法内部不在展开。
			this.binder.bind(bean);
		}
		catch (Exception ex) {
			throw new ConfigurationPropertiesBindException(bean, ex);
		}
}

源码解析至此,继续贴

this.binder.bind(bean)后的

源码已经没有意义,这里只说明几个核心部分

配置数据从哪里来? 配置数据从spring environment中获取

配置数据的注入最终是不是用反射?  

实践,实现动态刷新@ConfigurationProperties

通过@ConfigurationProperties的加载,我们已经知道,配置数据和bean是如何结合的,所以,我们只需要在配置数据更新时,维护好spring environment,然后重新调用配置绑定的方法即可

//伪代码 仅供参考,使用时请替换变量值
private void flushConfigurationProperties(){

    //从nacos 拉取最新配置数据,装入spring enviroment中  此实现暂略


    //这个beanName可以在源码中找到
    String bindBeanName = "org.springframework.boot.context.internalConfigurationPropertiesBinder";

    //获取配置数据注入bean,为啥不直接注入? 因为他是内部类,无法直接引入为类成员变量
    Object bean = applicationContext.getBean(bindBeanName);

    //配置bean的beanName
    String ConfigurationPropertiesBeanName = "根据你的实际情况填写";

    //获取配置bean
    Object ConfigurationPropertiesBean = applicationContext.getBean(ConfigurationPropertiesBeanName);

    //获取绑定的方法,反射执行
    ReflectionUtils.doWithLocalMethods(bean.getClass(), m->{
      if(m.getName().equals("bind")){
                m.setAccessible(true);

    //包装元数据
ConfigurationPropertiesBean configurationPropertiesBean = ConfigurationPropertiesBean.get(applicationContext,ConfigurationPropertiesBean ,ConfigurationPropertiesBeanName);


      try {
            //完成刷新
             m.invoke(bean,configurationPropertiesBean);
        }catch (Exception e){

        }
       }
 });
   
}

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐