@value注解可以在项目启动时获取到配置中心的值,但是如果在nacos配置中心后台修改了值,此时项目是无法动态感知修改后的值,需要利用@refreshscope注解来实现动态感知。
@refreshscope实现动态感知的使用
只需要在类上加上@refreshscope注解即可。
@restcontroller
@requestmapping("order")
@refreshscope
public class ordercontroller {
@value("${user.age}")
private integer age;
@getmapping("age")
public integer getage() {
return age;
}
}
@refreshscope导致@scheduled定时任务失效问题
演示问题
开启定时任务功能
@springbootapplication
@enablescheduling // 开启定时任务功能
public class orderserviceapplication {
public static void main(string[] args) throws interruptedexception {
springapplication.run(orderserviceapplication.class, args);
}
}
当在配置中心变更属性后,定时任务失效,当再次访问/order/age3地址后,定时任务又生效。
@restcontroller
@requestmapping("/order")
@refreshscope // 动态感知修改后的值
public class scheduledcontroller {
@value("${user.age}")
private integer age;
@getmapping("/age3")
public integer getage() {
return age;
}
//触发@refreshscope执行逻辑会导致@scheduled定时任务失效
@scheduled(cron = "*/3 * * * * ?") //定时任务每隔3s执行一次
public void execute() {
system.out.println("定时任务正常执行。。。。。。");
}
}
解决方案
实现spring事件监听器,监听refreshscoperefreshedevent事件,监听方法中进行一次定时方法的调用,这样bean就会被创建。
@restcontroller
@requestmapping("/order")
@refreshscope //动态感知修改后的值
public class scheduledcontroller implements applicationlistener<refreshscoperefreshedevent> {
@value("${user.age}")
private integer age;
@getmapping("/age3")
public integer getage() {
return age;
}
//触发@refreshscope执行逻辑会导致@scheduled定时任务失效
@scheduled(cron = "*/3 * * * * ?") //定时任务每隔3s执行一次
public void execute() {
system.out.println("定时任务正常执行。。。。。。");
}
@override
public void onapplicationevent(refreshscoperefreshedevent refreshscoperefreshedevent) {
// 不要下面这行也行
execute();
}
}
@refreshscope实现原理
- 怎么实现属性的动态刷新的?
- @refreshscope和@scheduled一起使用,为什么定时任务会停止?
@refreshscope注解
@refreshscope上面有@scope注解,其内部就一个属性默认scopedproxymode.target_class。
@target({ elementtype.type, elementtype.method })
@retention(retentionpolicy.runtime)
@scope("refresh")
@documented
public @interface refreshscope {
/**
* @see scope#proxymode()
* @return proxy mode
*/
scopedproxymode proxymode() default scopedproxymode.target_class;
}
bean的实例化过程
被@refreshscope注解的类,最终会调用refreshscope的get()方法实例化bean。
org.springframework.beans.factory.support.abstractbeanfactory#dogetbean
... ...
if (mbd.issingleton()) {
// 单例bean的实例化
}
else if (mbd.isprototype()) {
// 多例bean的实例化
}
else {
// 自定义作用域的bean的实例化
// scopename为refresh
string scopename = mbd.getscope();
if (!stringutils.haslength(scopename)) {
throw new illegalstateexception("no scope name defined for bean ´" + beanname + "'");
}
// scope为refreshscope
scope scope = this.scopes.get(scopename);
if (scope == null) {
throw new illegalstateexception("no scope registered for scope name '" + scopename + "'");
}
try {
object scopedinstance = scope.get(beanname, () -> {
beforeprototypecreation(beanname);
try {
return createbean(beanname, mbd, args);
}
finally {
afterprototypecreation(beanname);
}
});
bean = getobjectforbeaninstance(scopedinstance, name, beanname, mbd);
}
catch (illegalstateexception ex) {
throw new beancreationexception(beanname,
"scope '" + scopename + "' is not active for the current thread; consider " +
"defining a scoped proxy for this bean if you intend to refer to it from a singleton",
ex);
}
}
refreshscope.get()
@refreshscope作用域的bean会在第一次创建时进行缓存,包装了一个内部类 beanlifecyclewrappercache来对加了@refreshscope从而创建的对象进行缓存,使其在不刷新时获取的都是同一个对象。
org.springframework.cloud.context.scope.genericscope#get
public object get(string name, objectfactory<?> objectfactory) {
beanlifecyclewrapper value = this.cache.put(name,
new beanlifecyclewrapper(name, objectfactory));
this.locks.putifabsent(name, new reentrantreadwritelock());
try {
return value.getbean();
}
catch (runtimeexception e) {
this.errors.put(name, e);
throw e;
}
}
知道了对象是缓存的,所以在配置修改后只需要清除缓存,重新创建就好了。
refresheventlistener处理容器的刷新事件
当配置中心的内容变更后,nacos客户端收到变更会触发refreshevent事件。
org.springframework.cloud.endpoint.event.refresheventlistener#handle(org.springframework.cloud.endpoint.event.refreshevent)
public void handle(refreshevent event) {
if (this.ready.get()) { // don't handle events before app is ready
log.debug("event received " + event.geteventdesc());
set<string> keys = this.refresh.refresh();
log.info("refresh keys changed: " + keys);
}
}
org.springframework.cloud.context.refresh.contextrefresher#refresh
public synchronized set<string> refresh() {
set<string> keys = refreshenvironment();
this.scope.refreshall();
return keys;
}
org.springframework.cloud.context.scope.refresh.refreshscope#refreshall
public void refreshall() {
super.destroy();
this.context.publishevent(new refreshscoperefreshedevent());
}
org.springframework.cloud.context.scope.genericscope#destroy()
public void destroy() {
list<throwable> errors = new arraylist<throwable>();
// 清理缓存
collection<beanlifecyclewrapper> wrappers = this.cache.clear();
for (beanlifecyclewrapper wrapper : wrappers) {
try {
lock lock = this.locks.get(wrapper.getname()).writelock();
lock.lock();
try {
// 销毁实例
wrapper.destroy();
}
finally {
lock.unlock();
}
}
catch (runtimeexception e) {
errors.add(e);
}
}
if (!errors.isempty()) {
throw wrapifnecessary(errors.get(0));
}
this.errors.clear();
}
在下一次使用对象的时候,代理对象中获取目标对象的时候会调用genericscope.get()方法创建一个新的对象,并存入缓存中,此时新对象因为spring的装配机制就是新的属性了。
定时任务停止的原因
@refreshscope和@scheduled一起使用,定时任务会停止并不是因为缓存失效了,而是因为容器刷新时会进行定时任务的取消。
在上面容器刷新时会调用beanlifecyclewrapper的destroy()方法。
org.springframework.cloud.context.scope.genericscope.beanlifecyclewrapper#destroy
public void destroy() {
if (this.callback == null) {
return;
}
synchronized (this.name) {
runnable callback = this.callback;
if (callback != null) {
callback.run();
}
this.callback = null;
this.bean = null;
}
}
看似这个方法啥也没干,callback对象为null,实际上callback是有值的,在bean实例化是会设置值。
org.springframework.beans.factory.support.abstractbeanfactory#registerdisposablebeanifnecessary
protected void registerdisposablebeanifnecessary(string beanname, object bean, rootbeandefinition mbd) {
accesscontrolcontext acc = (system.getsecuritymanager() != null ? getaccesscontrolcontext() : null);
if (!mbd.isprototype() && requiresdestruction(bean, mbd)) {
if (mbd.issingleton()) {
// register a disposablebean implementation that performs all destruction
// work for the given bean: destructionawarebeanpostprocessors,
// disposablebean interface, custom destroy method.
registerdisposablebean(beanname,
new disposablebeanadapter(bean, beanname, mbd, getbeanpostprocessors(), acc));
}
else {
// a bean with a custom scope...
scope scope = this.scopes.get(mbd.getscope());
if (scope == null) {
throw new illegalstateexception("no scope registered for scope name '" + mbd.getscope() + "'");
}
// callback为disposablebeanadapter
scope.registerdestructioncallback(beanname,
new disposablebeanadapter(bean, beanname, mbd, getbeanpostprocessors(), acc));
}
}
}
callback为disposablebeanadapter,当容器刷新时会调用disposablebeanadapter的run()方法。
org.springframework.beans.factory.support.disposablebeanadapter#run
public void run() {
destroy();
}
@override
public void destroy() {
if (!collectionutils.isempty(this.beanpostprocessors)) {
for (destructionawarebeanpostprocessor processor : this.beanpostprocessors) {
// scheduledannotationbeanpostprocessor
processor.postprocessbeforedestruction(this.bean, this.beanname);
}
}
... ...
}
public void postprocessbeforedestruction(object bean, string beanname) {
set<scheduledtask> tasks;
synchronized (this.scheduledtasks) {
// 删除任务
tasks = this.scheduledtasks.remove(bean);
}
if (tasks != null) {
for (scheduledtask task : tasks) {
// 取消任务
task.cancel();
}
}
}
最后任务被删了,取消了,所以不会执行了。
那为什么监听了refreshscoperefreshedevent事件,定时任务又正常启动了呢?
因为容器刷新后会触发refreshscoperefreshedevent事件,spring容器会查找所有监听refreshscoperefreshedevent事件的bean,并调用其onapplicationevent()方法,这样会触发scheduledcontroller的实例化过程。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论