@Value注解可以在项目启动时获取到配置中心的值,但是如果在Nacos配置中心后台修改了值,此时项目是无法动态感知修改后的值,需要利用@RefreshScope注解来实现动态感知。
只需要在类上加上@RefreshScope注解即可。
@RestController
@RequestMapping("order")
@RefreshScope
public class OrderController {@Value("${user.age}")private Integer age;@GetMapping("age")public Integer getAge() {return age;}
}
开启定时任务功能
@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上面有@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;}
被@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作用域的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;}
}
知道了对象是缓存的,所以在配置修改后只需要清除缓存,重新创建就好了。
当配置中心的内容变更后,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的实例化过程。