@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<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上面有@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为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作用域的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 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的实例化过程。