一、引言:一个“改了配置却不生效”的经典问题
在微服务开发中,我们常使用 @RefreshScope 来让配置变更后无需重启即可生效。典型场景是:修改 Consul/Nacos 上的配置,调用 /actuator/refresh,配置就“神奇”地更新了。
但项目里也经常出现这样的困惑:
“我已经加了
@RefreshScope,也 refresh 了,为什么这个值还是旧的?”
“ServiceA 注入了
@RefreshScope的 ConfigBean,为什么 ConfigBean 已经刷新了,ServiceA 里缓存的旧值仍然存在?”
这些问题的答案隐藏在 @RefreshScope 的 CGLIB 代理机制、RefreshScope 作用域的生命周期 以及 单例 Bean 不变性边界 之中。本文将带你从现象到原理,一层层拨开迷雾。
二、@RefreshScope 快速回顾
@RefreshScope 是 Spring Cloud 提供的一个自定义 Scope,标记在 Bean 上后,该 Bean 会在配置刷新时被重建,从而获取最新的配置值。
@Component
@RefreshScope
public class MyConfig {
@Value("${user.name}")
private String userName;
public String getUserName() {
return userName;
}
}
调用 /actuator/refresh 后,后续通过 myConfig.getUserName() 获取到的就是最新的 ${user.name} 值。
但它是怎么做到的呢?
三、核心机制一:基于 CGLIB 的代理对象
3.1 @RefreshScope 本质就是一个 @Scope
查看 @RefreshScope 的定义:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
public @interface RefreshScope {
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
它其实就是 @Scope("refresh"),且默认 proxyMode = ScopedProxyMode.TARGET_CLASS。这意味着 Spring 容器不会直接暴露这个 Bean 的实例,而是给它创建一个 CGLIB 代理。
3.2 代理如何生效
当单例 Bean 注入一个 @RefreshScope Bean 时,注入的其实是它的 CGLIB 代理对象:

。
关键点:单例 Bean 拥有的始终是同一个代理对象(地址不变),但代理内部会动态委托给 RefreshScope 中当前有效的真实 Bean。
3.3 为什么不用 JDK 动态代理?
因为 @RefreshScope 通常加在具体类上(如 @Component),而不是接口。TARGET_CLASS 强制使用 CGLIB 生成子类代理,这样才能拦截具体类的方法调用。
四、核心机制二:RefreshScope 作用域的刷新行为
4.1 RefreshScope 的实现
RefreshScope 继承自 GenericScope,内部维护了一个缓存 BeanLifecycleWrapperCache,用于存放所有被 refresh 作用域管理的 Bean 实例。
当调用 /actuator/refresh 时,流程如下:

。
核心操作:RefreshScope.refreshAll() 会销毁作用域内的所有 Bean,相当于清空缓存。下一次通过代理访问这些 Bean 时,会重新创建实例,从而读取最新的 Environment。
4.2 代码验证
@RestController
public class TestController {
@Autowired
private MyConfig myConfig; // 实际是 CGLIB 代理
@GetMapping("/config")
public String getConfig() {
// 每次方法调用都通过代理 -> RefreshScope -> 当前真实 Bean
return myConfig.getUserName();
}
}
在 refresh 后再次访问 /config,代理会委托给全新的 MyConfig 实例(已经使用新 user.name 初始化),所以返回新值。
五、单例 Bean 的不变性边界:为什么缓存的旧值不会自动更新?
5.1 现象:Service 中缓存了刷新 Bean 的属性
@Service
public class OrderService {
@Autowired
private MyConfig myConfig; // 注入的是代理
private String cachedUserName;
@PostConstruct
public void init() {
// 构造函数/初始化时读取并缓存了属性值
this.cachedUserName = myConfig.getUserName();
}
public void processOrder() {
// 始终使用缓存的旧值
System.out.println(cachedUserName);
}
}
调用 /actuator/refresh 后,myConfig 内部代理会委托给新的 Bean,但 OrderService 的 cachedUserName 变量已经保存了一个字符串副本,它不会自动被更新。
这就是“不变性边界”:单例 Bean 自身的字段(如 cachedUserName)不会因为所依赖的 RefreshScope Bean 刷新而自动变化。代理只能拦截对代理对象的方法调用,无法修改单例 Bean 已经缓存的数据。
5.2 原理图解

。
边界:只有通过代理对象实时调用,才能穿透到 RefreshScope 获取最新实例。任何“提前求值并存储”的做法都会导致观察不到刷新后的变化。
5.3 解决方案
方案一:始终通过代理方法访问
@Service
public class OrderService {
@Autowired
private MyConfig myConfig;
public void processOrder() {
// 每次实时获取
String userName = myConfig.getUserName();
System.out.println(userName);
}
}
方案二:使用 @RefreshScope 标记 Service 本身
如果 Service 本身也被 @RefreshScope 代理,那么 OrderService 在 refresh 后也会重建,字段自然更新。但这会带来额外的性能开销和 Bean 作用域复杂性,一般不推荐。
方案三:使用 Environment 直接获取
@Autowired
private Environment env;
public void processOrder() {
String userName = env.getProperty("user.name");
}
Environment 自身支持动态刷新,无需额外 Scope。
六、RefreshScope 的潜在坑点与最佳实践
6.1 @RefreshScope 与 @ConfigurationProperties 的区别
@RefreshScope基于 Scope 代理,每次方法调用走代理。@ConfigurationProperties绑定的 Bean 默认是单例,但可以通过@RefreshScope标记或使用Environment来配合刷新。- Spring Cloud 团队推荐使用
@RefreshScope搭配@ConfigurationProperties,以获得刷新能力。
@Component
@ConfigurationProperties(prefix = "app")
@RefreshScope
public class AppProperties {
private String name;
// getters/setters
}
6.2 刷新时的短暂不一致
RefreshScope 清空缓存并重建 Bean 的过程不是原子的。如果你在 refresh 过程中访问,可能会拿到正在创建的新 Bean,这在极高并发下需留意(但通常可忽略)。
6.3 与 @Scheduled 等定时任务结合
若定时任务所在的 Bean 被标记为 @RefreshScope,每次 refresh 都会重建该 Bean,可能导致任务重新调度异常。建议定时任务的 Bean 保持单例,内部通过注入代理来获取动态配置。
七、总结
@RefreshScope 通过 CGLIB 代理 + 自定义 Scope 实现了配置的动态刷新,其核心行为可总结为下表:
| 维度 | 机制 | 注意事项 |
|---|---|---|
| Bean 暴露形式 | CGLIB 代理对象 | 单例 Bean 注入的是同一个代理 |
| 方法调用 | 代理委托给 RefreshScope 当前实例 | 每次调用都可能拿到新 Bean |
| Refresh 触发 | refreshAll() 清空作用域缓存 | 下一次调用时重建 Bean |
| 单例 Bean 不变性边界 | 提前缓存的属性值不会自动刷新 | 必须通过代理实时调用或改用 Environment |
| 适用场景 | 需要动态刷新的配置 Bean | 避免滥用,作用域代理有一定性能开销 |
牢记一条金句:
代理可以让你“每次调用都看到最新的世界”,但无法改变你“手上已经保存的历史快照”。
当我们理解了这层“不变性边界”,就能在设计时避开那些隐蔽的静态缓存陷阱,让配置刷新真正生效。
如果你在项目中遇到过更诡异的刷新不生效问题,欢迎在评论区一起探讨!


3万+

被折叠的 条评论
为什么被折叠?



