在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,从而导致它不会被刷新。

相关推荐
爬山算法8 分钟前
Netty(19)Netty的性能优化手段有哪些?
java·后端
Tony Bai9 分钟前
Cloudflare 2025 年度报告发布——Go 语言再次“屠榜”API 领域,AI 流量激增!
开发语言·人工智能·后端·golang
想用offer打牌22 分钟前
虚拟内存与寻址方式解析(面试版)
java·后端·面试·系统架构
無量26 分钟前
AQS抽象队列同步器原理与应用
后端
五阿哥永琪44 分钟前
RedisTemplate、StringRedisTemplate、RedisIndexedSessionRepository之间的区别?
spring boot
9号达人1 小时前
支付成功订单却没了?MyBatis连接池的坑我踩了
java·后端·面试
用户497357337981 小时前
【轻松掌握通信协议】C#的通信过程与协议实操 | 2024全新
后端
草莓熊Lotso1 小时前
C++11 核心精髓:类新功能、lambda与包装器实战
开发语言·c++·人工智能·经验分享·后端·nginx·asp.net
计算机毕设指导62 小时前
基于微信小程序的鸟博士系统【源码文末联系】
java·spring boot·mysql·微信小程序·小程序·tomcat·maven
追逐时光者2 小时前
精选 8 个 .NET 开发实用的类库,效率提升利器!
后端·.net