Spring Cloud @RefreshScope底层原理:刷新执行长任务的Bean时的处理行为
@RefreshScope 触发刷新时正在运行的Bean的实际处理逻辑
首先明确结论:既不会立即中断运行中的任务流程,也不会等任务跑完再初始化新实例,属于第三种独立的处理逻辑,核心是由RefreshScope的底层代理机制决定的。
底层实现逻辑拆解
@RefreshScope标注的Bean,Spring容器注册的并不是真实的业务实例,而是一个CGLIB生成的代理对象,这个代理的核心作用是:
- 每次收到方法调用时,先从RefreshScope维护的本地缓存中取对应的真实业务Bean实例,再把调用转发给这个真实实例
- 缓存为空时,才会根据当前配置的属性值初始化新的真实Bean实例,放进缓存后再转发调用
当通过Actuator的/actuator/refresh端点触发刷新时,框架的操作非常轻量:
- 直接清空RefreshScope内部的所有缓存条目,不会主动销毁缓存中存放的任何旧Bean实例,更不会打断正在运行的线程
不同场景下的实际行为
结合长耗时任务的运行场景,所有行为都可以顺着上面的代理逻辑推导出来:
- 长任务启动阶段:代理会先从缓存取到旧的Bean实例,然后把长任务的方法调用转发给这个旧实例。一旦方法调用进入旧实例的执行逻辑,整个执行流程就完全脱离了代理和缓存的控制,哪怕这时候缓存被清空、新实例已经被创建,正在运行的旧任务也会顺畅执行完毕,不会被强制中断。
- 长任务运行期间:
- 如果在长任务里把需要用到的配置值、依赖调用结果都提前存在了本地变量里,后续执行完全不经过代理,那么整个任务从头到尾都用旧实例跑,没有任何影响。
- 如果在长任务的长间隔逻辑里,中途又去通过代理调用这个@RefreshScope Bean的方法(比如跑了10分钟之后才去读配置值、调业务方法),这时候因为缓存已经被清空,代理不会等旧任务执行完,会直接根据最新配置初始化一个新的Bean实例,把这次新的方法调用转发给新实例。这时候就会出现同一个长任务里,前半段跑在旧实例、后半段跑在新实例的情况,如果新旧配置差异较大,就可能出现逻辑不一致,但这也不是对旧流程的中断。
对三种假设情况的澄清
- 不会出现“立即中断正在运行的Bean执行流程”的情况:框架没有强制打断线程、终止方法执行的逻辑,所有运行中的执行流都是自然跑完的。
- 不会出现“等待Bean完成当前执行流程后再优雅重初始化”的情况:新实例的初始化完全和旧任务的执行状态解耦,只要有新的方法调用触发代理逻辑,哪怕旧任务还在跑,新实例也会被立即创建,不会等待。
实践避坑提示
- 长耗时任务启动时,第一时间把所有依赖的配置值、关联的业务逻辑结果获取后存到本地变量,不要在长任务的执行中途调用@RefreshScope Bean的方法,避免中途切到新实例带来的逻辑不一致。
- 不要依赖@RefreshScope Bean的
@PreDestroy方法做资源释放:刷新清空缓存的时候,框架根本不会调用旧Bean的销毁钩子,这种写法很容易导致连接、文件句柄这类资源泄漏。
内容的提问来源于stack exchange,提问作者KidCrippler
相关产品推荐
相关产品推荐

