配置中⼼@RefreshScope导致@Scheduled失效问题
⼀、问题
最近在运维项⽬的时候,出现了⼀个问题,在⼀个定时处理数据的类(TaskSchedule)⾥⾯,有⽤到配置⽂件(bootstrap.properties)中的信息,所以使⽤@Value()来获取配置信息,但使⽤@RefreshScope刷新配置信息后,发现定时任务不执⾏了,代码如下:
@Component
@RefreshScope
public class TaskSchedule{
@Value("${distribute.source.sqlrver:fal}")
Boolean readFromSqlrver;
@Scheduled(cron ="0/30 * * * * ?")
public void distribute(){
....
}
本来以为它会直接拿到刷新后的信息,但从结果看却不是,那就定位原因了,要定位原因,⾸先要知道 @RefreshScope的执⾏流程,那就只能看源码了
⼆、原因
⼤概看了⼀下,实现@RefreshScope 动态刷新的就需要以下⼏个:
@ Scope
@RefreshScope
RefreshScope
GenericScope
Scope
ContextRefresher
1、@Scope
⼀句话,@RefreshScope 能实现动态刷新全仰仗着@Scope 这个注解,这是为什么呢?
@Scope 代表了Bean的作⽤域,我们来看下其中的属性:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public@interface Scope {
/**
* Alias for {@link #scopeName}.
* @e #scopeName
*/
@AliasFor("scopeName")
String value()default"";
brooch
/**
* singleton 表⽰该bean是单例的。(默认)
研究生培训班
* prototype 表⽰该bean是多例的,即每次使⽤该bean时都会新建⼀个对象。
* request 在⼀次http请求中,⼀个bean对应⼀个实例。
* ssion 在⼀个httpSession中,⼀个bean对应⼀个实例
*/
@AliasFor("value")
String scopeName()default"";
/**
* DEFAULT 不使⽤代理。(默认)
* NO 不使⽤代理,等价于DEFAULT。
* INTERFACES 使⽤基于接⼝的代理(jdk dynamic proxy)。
* TARGET_CLASS 使⽤基于类的代理(cglib)。
*/
ScopedProxyMode proxyMode()default ScopedProxyMode.DEFAULT;
}
通过代码我们可以清晰的看到两个主要属性value 和 proxyMode,value就不多说了,⼤家平时经常⽤看看注解就可以。proxyMode 这个就有意思了,⽽这个就是@RefreshScope 实现的本质了。
我们需要关⼼的就是ScopedProxyMode.TARGET_CLASS 这个属性,当ScopedProxyMode 为TARGET_CLASS 的时候会给当前创建的bean ⽣成⼀个代理对象,会通过代理对象来访问,每次访问都会创建⼀个新的对象。
理解起来可能⽐较晦涩,那先来看下实现再回头来看这句话。
archers2、RefreshScope 的实现原理
1.先来看下@RefreshScope
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public@interface RefreshScope {below反义词
/**
* @e Scope#proxyMode()
*/商务英语报名时间
ScopedProxyMode proxyMode()default ScopedProxyMode.TARGET_CLASS;
}
2. 可以看出,它使⽤就是 @Scope ,其内部就⼀个属性默认 ScopedProxyMode.TARGET_CLASS。知道了是通过Spring Scope 来实现的那就简单了,我们来看下Scope 这个接⼝
public interface Scope {
/**
* Return the object with the given name from the underlying scope,
* {@link org.springframework.beans.factory.ObjectFactory#getObject() creating it}
* if not found in the underlying storage mechanism.
* <p>This is the central operation of a Scope, and the only operation
* that is absolutely required.
* @param name the name of the object to retrieve
* @param objectFactory the {@link ObjectFactory} to u to create the scoped
* object if it is not prent in the underlying storage mechanism
* @return the desired object (never {@code null})
* @throws IllegalStateException if the underlying scope is not currently active
*/
Object get(String name, ObjectFactory<?> objectFactory);
@Nullable
Object remove(String name);
void registerDestructionCallback(String name, Runnable callback);
@Nullable
Object resolveContextualObject(String key);
@Nullable
String getConversationId();
}
看下接⼝,我们只看Object get(String name, ObjectFactory<?> objectFactory); 这个⽅法帮助我们来创建⼀个新的bean ,也就是说,@RefreshScope 在调⽤ 刷新的时候会使⽤此⽅法来给我们创建新的对象,这样就可以通过spring 的装配机制将属性重新注⼊了,也就实现了所谓的动态刷新。
那它究竟是怎么处理⽼的对象,⼜怎么触发创建新的对象呢?
在开头我提过⼏个重要的类,⽽其中 RefreshScope extends GenericScope, GenericScope implements Scope。
所以通过查看代码,是GenericScope 实现了 Scope 最重要的 get(String name, ObjectFactory<?> objectFactory) ⽅法,在GenericScope ⾥⾯ 包装了⼀个内部类 BeanLifecycleWrapperCache 来对加了 @RefreshScope 从⽽创建的对象进⾏缓存,使其在不刷新时获取的都是同⼀个对象。(这⾥你可以把 BeanLifecycleWrapperCache 想象成为⼀个⼤Map 缓存了所有@RefreshScope 标注的对象)朗诵培训
知道了对象是缓存的,所以在进⾏动态刷新的时候,只需要清除缓存,重新创建就好了。 来看代码,眼见为实,只留下关键⽅法:
// ContextRefresher 外⾯使⽤它来进⾏⽅法调⽤ ============================== 我是分割线
public synchronized Set<String>refresh(){
Set<String> keys =refreshEnvironment();
freshAll();
return keys;
}
// RefreshScope 内部代码 ============================== 我是分割线
@ManagedOperation(description ="Dispo of the current instance of all beans in this scope and force a refresh on next method execution.")
public void refreshAll(){
super.destroy();
}
// GenericScope ⾥的⽅法 ============================== 我是分割线
//进⾏对象获取,如果没有就创建并放⼊缓存
@Override
public Object get(String name, ObjectFactory<?> objectFactory){
BeanLifecycleWrapper value =this.cache.put(name,
mbo是什么意思new BeanLifecycleWrapper(name, objectFactory));
locks.putIfAbnt(name,new ReentrantReadWriteLock());
try{
Bean();
}
catch(RuntimeException e){
throw e;
}
}
//进⾏缓存的数据清理
@Override
public void destroy(){
List<Throwable> errors =new ArrayList<Throwable>();
Collection<BeanLifecycleWrapper> wrappers =this.cache.clear();
for(BeanLifecycleWrapper wrapper : wrappers){
try{
Lock lock = (Name()).writeLock();
lock.lock();
try{
wrapper.destroy();
}
finally{
storagelock.unlock();
}
}
catch(RuntimeException e){
errors.add(e);
}
}
if(!errors.isEmpty()){
throw (0));
}
}
通过观看源代码我们得知,我们截取了三个⽚段所得之,ContextRefresher 就是外层调⽤⽅法⽤的,
GenericScope ⾥⾯的 get ⽅法负责对象的创建和缓存,destroy ⽅法负责再刷新时缓存的清理⼯作。当然spring 内部还进⾏很多其他有趣的处理,有兴趣的同学可以详细看⼀下。
3、总结
综上所述,来总结下@RefreshScope 实现流程
1. 需要动态刷新的类标注@RefreshScope 注解
2. @RefreshScope 注解标注了@Scope 注解,并默认了ScopedProxyMode.TARGET_CLASS; 属性,此属性的功能就是在创建⼀
个代理,在每次调⽤的时候都⽤它来调⽤GenericScope get ⽅法来获取对象
3. 如属性发⽣变更会调⽤ ContextRefresher refresh() -》RefreshScope refreshAll() 进⾏缓存清理⽅法调⽤,并发送刷新事件通知
-》 GenericScope 真正的 清理⽅法destroy() 实现清理缓存
4. 在下⼀次使⽤对象的时候,代理对象中获取⽬标对象的时候会调⽤GenericScope get(String name, ObjectFactory<?>
星期一到星期日的英文缩写
objectFactory) ⽅法创建⼀个新的对象,并存⼊缓存中,此时新对象因为Spring 的装配机制就是新的属性了
三、解决⽅案
1、 RefreshScopeRefreshedEvent(公认最简单)
@Component
@RefreshScope
public class TaskSchedule implements ApplicationListener<RefreshScopeRefreshedEvent>{
@Value("${distribute.source.sqlrver:fal}")
Boolean readFromSqlrver;
@Scheduled(cron ="0/30 * * * * ?")
public void distribute(){
....
}
@Override
public void onApplicationEvent(RefreshScopeRefreshedEvent refreshScopeRefreshedEvent){}
}
使⽤ RefreshScopeRefreshedEvent 从 config配置服务器成功获取并覆盖值。在这个事件监听⽅法⾥⾯,只需要⼿动再从Spring容器中获取⼀次当前Bean即可,因为这样便可以迫使当前Bean重新加载,从⽽重新初始化定时任务。
2、⽐较复杂的
/**
* Listener of Spring's lifecycle to revive Scheduler beans, when spring's
* scope is refreshed.
* <p>
* Spring is able to restart beans, when we change their properties. Such a
* beans marked with RefreshScope annotation. To make it work, spring creates
* <b>lazy</b> proxies and push them instead of real object. The issue with
* scope refresh is that right after refresh in order for such a lazy proxy
* to be actually instantiated again someone has to call for any method of it.
* <p>
* It creates a tricky ca with Schedulers, becau there is no bean, which
* directly call anything on any Scheduler. Scheduler lifecycle is to start
* few threads upon instantiation and schedule tasks. No other bean needs
* anything from them.
* <p>
* To overcome this, we had to create artificial method on Schedulers and call
* them, when there is a scope refresh event. This actually instantiates.
*/
@RequiredArgsConstructor
public class RefreshScopeListener implements ApplicationListener<RefreshScopeRefreshedEvent>{
private final List<RefreshScheduler> refreshSchedulers;
@Override
tremendous
public void onApplicationEvent(RefreshScopeRefreshedEvent event){
refreshSchedulers.forEach(RefreshScheduler::materializeAfterRefresh);
}
}
所以,我们定义了⼀个接⼝,它没有做任何特别的事情,但允许我们调⽤⼀个刷新的作业。