【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的实例化过程。

相关内容

热门资讯

【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
ASM贪吃蛇游戏-解决错误的问... 要解决ASM贪吃蛇游戏中的错误问题,你可以按照以下步骤进行:首先,确定错误的具体表现和问题所在。在贪...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...