如何动态刷新@ConfigurationProperties?
如何动态刷新@ConfigurationProperties?
需求起因
最近在实现配置中心配置热更新时,遇到了@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){
}
}
});
}
更多推荐
所有评论(0)