【Nacos】@RefreshScope注解的使用与原理

@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实现原理

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

你可能感兴趣的:(Nacos,spring,springcloud,springboot,nacos,refreshscope)