Spring应用中使用acutator/refresh刷新属性不生效的问题分析及解决
作者:龙鹤鹿
问题的引入
在Spring应用收到/actuator/refresh
的POST请求后,标注了@RefreshScope
以及@ConfiguratioinProperties
的bean会被Spring容器重新加载。这样,如果配置文件(一般来自于配置中心,或者k8s的ConfigMap)发生了变化,那么这些变化就会因为Bean的重新加载而被应用感知。
但是,在实际应用中,可能会发现有些标注了@ConfigurationProperties
的bean,并没有按照预期被Spring容器加载。本文将讨论导致这种未按预期刷新的一种原因。
结论
在进行详细的讨论之前,先把结论写出来。如果大家时间紧张,而且碰巧遇到了这样的问题,可以直接根据结论把问题解决掉。
确保标注了@ConfigurationProperties
注解的bean没有被任何Advisor
依赖
比如:如下的Bean就不会被Spring容器刷新。
@ConfigurationProperties(prefix="com.dadaer.test") public class MyProp { //... } // ............ @Component public class MyAdvisor extends AbstractPointcutAdvisor { private final MyProp myProp; public class MyAdvisor(MyProp myProp) { this.myProp = myProp; } //... }
这里的MyAdvisor依赖了MyProp,所以在收到/actuator/refresh
的请求以后,MyProp
的bean不会被重新加载。
分析
应用启动阶段
在Spring应用启动时,在执行到AbstractApplicationContext#refresh
方法初始化容器的时候,其中有一个步骤(12大步中的第5步),Spring会向容器中注入所有的BeanPostProcessor
,代码如下:
// Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory);
而其中有一个BeanPostProcessor
叫做:ConfigurationPropertiesBeans
,是一个用来注册所有标注了@ConfigurationProperties
注解的后置处理器。
在初始化BeanPostProcessor
的时候,会经历到下面的一段代码:
@Override protected boolean shouldSkip(Class<?> beanClass, String beanName) { // TODO: Consider optimization by caching the list of the aspect names List<Advisor> candidateAdvisors = findCandidateAdvisors(); for (Advisor advisor : candidateAdvisors) { if (advisor instanceof AspectJPointcutAdvisor pointcutAdvisor && pointcutAdvisor.getAspectName().equals(beanName)) { return true; } } return super.shouldSkip(beanClass, beanName); }
这里,会查找所有的Advisor
并初始化。这样,如果某个Advisor
(比如上述MyAdvisor
)依赖了一个@ConfigurationProperties
注解的类(比如上述MyProp
)。那么此时MyProp
就需要在BeanPostProcessor
之前初始化完成,即:MyProp
先于ConfigurationPropertiesBeans
完成初始化。
应用运行阶段
在应用运行阶段,当收到/actuator/refresh
的POST请求时,会触发RefreshEndpoint
:
@Endpoint(id = "refresh") public class RefreshEndpoint { //... @WriteOperation public Collection<String> refresh() { Set<String> keys = this.contextRefresher.refresh(); return keys; } }
然后调用ContextRefresher#refresh
方法进入下面的代码:
public synchronized Set<String> refreshEnvironment() { Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources()); updateEnvironment(); Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet(); this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys)); return keys; }
这里,发送了一个EnvironmentChangeEvent
事件,这个事件会被ConfigurationPropertiesRebinder
捕获,如下:
@Override public void onApplicationEvent(EnvironmentChangeEvent event) { if (this.applicationContext.equals(event.getSource()) // Backwards compatible || event.getKeys().equals(event.getSource())) { rebind(); } }
然后rebind
方法被调用:
@ManagedOperation public void rebind() { this.errors.clear(); for (String name : this.beans.getBeanNames()) { rebind(name); } }
这里beans
变量的类型是:ConfigurationPropertiesBeans
,它里面保存了所有待刷新的ConfigurationProperties
的bean。
结论
因为启动阶段中,MyProp
优先于ConfigurationPropertiesBeans
被加载,导致ConfigurationPropertiesBeans
里面不会包含MyProp
这个bean,从而导致它不会被刷新。
以上就是Spring应用中使用acutator/refresh刷新属性不生效的问题分析及解决的详细内容,更多关于Spring使用acutator/refresh不生效的资料请关注脚本之家其它相关文章!