【Nacos】@RefreshScope注解的使用与原理
创始人
2024-05-21 02:38:22
0

@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 {@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("定时任务正常执行。。。。。。");}@Overridepublic void onApplicationEvent(RefreshScopeRefreshedEvent refreshScopeRefreshedEvent) {// 不要下面这行也行execute();}
}

@RefreshScope实现原理

  1. 怎么实现属性的动态刷新的?
  2. @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为refreshString scopeName = mbd.getScope();if (!StringUtils.hasLength(scopeName)) {throw new IllegalStateException("No scope name defined for bean ´" + beanName + "'");}// scope为RefreshScopeScope 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 readylog.debug("Event received " + event.getEventDesc());Set keys = this.refresh.refresh();log.info("Refresh keys changed: " + keys);}
}

org.springframework.cloud.context.refresh.ContextRefresher#refresh

public synchronized Set refresh() {Set 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 errors = new ArrayList();// 清理缓存Collection 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为DisposableBeanAdapterscope.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) {// ScheduledAnnotationBeanPostProcessorprocessor.postProcessBeforeDestruction(this.bean, this.beanName);}}
... ...
}
public void postProcessBeforeDestruction(Object bean, String beanName) {Set 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的实例化过程。

相关内容

热门资讯

保存时出现了1个错误,导致这篇... 当保存文章时出现错误时,可以通过以下步骤解决问题:查看错误信息:查看错误提示信息可以帮助我们了解具体...
汇川伺服电机位置控制模式参数配... 1. 基本控制参数设置 1)设置位置控制模式   2)绝对值位置线性模...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
表格中数据未显示 当表格中的数据未显示时,可能是由于以下几个原因导致的:HTML代码问题:检查表格的HTML代码是否正...
本地主机上的图像未显示 问题描述:在本地主机上显示图像时,图像未能正常显示。解决方法:以下是一些可能的解决方法,具体取决于问...
不一致的条件格式 要解决不一致的条件格式问题,可以按照以下步骤进行:确定条件格式的规则:首先,需要明确条件格式的规则是...
表格列调整大小出现问题 问题描述:表格列调整大小出现问题,无法正常调整列宽。解决方法:检查表格的布局方式是否正确。确保表格使...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...