在Spring应用中使用acutator/refresh刷新属性不生效的问题

问题的引入

在Spring应用收到/actuator/refresh的POST请求后,标注了@RefreshScope以及@ConfiguratioinProperties的bean会被Spring容器重新加载。这样,如果配置文件(一般来自于配置中心,或者k8s的ConfigMap)发生了变化,那么这些变化就会因为Bean的重新加载而被应用感知。

但是,在实际应用中,可能会发现有些标注了@ConfigurationProperties的bean,并没有按照预期被Spring容器加载。本文将讨论导致这种未按预期刷新的一种原因。

结论

在进行详细的讨论之前,先把结论写出来。如果大家时间紧张,而且碰巧遇到了这样的问题,可以直接根据结论把问题解决掉。

确保标注了@ConfigurationProperties注解的bean没有被任何Advisor依赖

比如:如下的Bean就不会被Spring容器刷新。

java 复制代码
@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,代码如下:

java 复制代码
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

而其中有一个BeanPostProcessor叫做:ConfigurationPropertiesBeans,是一个用来注册所有标注了@ConfigurationProperties注解的后置处理器。

在初始化BeanPostProcessor的时候,会经历到下面的一段代码:

java 复制代码
@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

java 复制代码
@Endpoint(id = "refresh")
public class RefreshEndpoint {
    //...
    @WriteOperation
    public Collection<String> refresh() {
       Set<String> keys = this.contextRefresher.refresh();
       return keys;
    }
}

然后调用ContextRefresher#refresh方法进入下面的代码:

java 复制代码
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捕获,如下:

java 复制代码
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
    if (this.applicationContext.equals(event.getSource())
          // Backwards compatible
          || event.getKeys().equals(event.getSource())) {
       rebind();
    }
}

然后rebind方法被调用:

java 复制代码
@ManagedOperation
public void rebind() {
    this.errors.clear();
    for (String name : this.beans.getBeanNames()) {
       rebind(name);
    }
}

这里beans变量的类型是:ConfigurationPropertiesBeans,它里面保存了所有待刷新的ConfigurationProperties的bean。

结论

因为启动阶段中,MyProp优先于ConfigurationPropertiesBeans被加载,导致ConfigurationPropertiesBeans里面不会包含MyProp这个bean,从而导致它不会被刷新。

相关推荐
他҈姓҈林҈21 分钟前
使用 Spring Boot 进行开发
spring boot
柏油3 小时前
MySQL InnoDB 行锁
数据库·后端·mysql
咖啡调调。3 小时前
使用Django框架表单
后端·python·django
Java&Develop3 小时前
onloyoffice历史版本功能实现,版本恢复功能,编辑器功能实现 springboot+vue2
前端·spring boot·编辑器
白泽talk3 小时前
2个小时1w字| React & Golang 全栈微服务实战
前端·后端·微服务
摆烂工程师3 小时前
全网最详细的5分钟快速申请一个国际 “edu教育邮箱” 的保姆级教程!
前端·后端·程序员
一只叫煤球的猫3 小时前
你真的会用 return 吗?—— 11个值得借鉴的 return 写法
java·后端·代码规范
Asthenia04123 小时前
HTTP调用超时与重试问题分析
后端
颇有几分姿色4 小时前
Spring Boot 读取配置文件的几种方式
java·spring boot·后端
AntBlack4 小时前
别说了别说了 ,Trae 已经在不停优化迭代了
前端·人工智能·后端