【深入理解SpringCloud微服务】深入理解nacos配置中心(六)——spring-cloud-context关于配置刷新的公共逻辑
- 原理分析
- 源码解析
- RefreshEventListener#onApplicationEvent(ApplicationEvent)
- ContextRefresher#refresh()
- ContextRefresher#refreshEnvironment()
- RefreshScope#refreshAll()
- @RefreshScope注解的原理
我们在上一篇文章《客户端监听配置变更并刷新的源码分析》中最后说到,nacos客户端监听到配置变更通知后,会发布一个RefreshEvent事件,触发spring-cloud-context关于配置刷新的公共逻辑。但是由于它不是nacos实现的逻辑,我们没有对这段逻辑进行分析,本篇文章将会分析spring-cloud-context关于配置刷新的公共逻辑。
原理分析
spring-cloud-context会往Spring容器中注册一个监听器RefreshEventListener,这个监听器会监听并处理RefreshEvent事件。
RefreshEventListener监听到RefreshEvent事件后,会调用ContextRefresher的refresh()进行配置刷新的操作。
ContextRefresher的refresh()会做两件事情:
- 创建一个新的SpringApplication再跑一遍run()方法拿到最新的配置,覆盖到当前环境Environment中
- 销毁被@RefreshScope注解修饰的bean,等下一次调用到该bean时从容器中获取发现没有了,会重新创建一个,此时就会拿到最新的配置
首先解析第一件事情:创建一个新的SpringApplication再跑一遍run()方法。
SpringApplication的run()方法就是SpringBoot工程启动时的main方法执行的方法,这里创建一个新的SpringApplication,执行它的run()方法,就会得到一个新的ApplicationContext,里面的Environment中的配置都是最新的配置,拿到这个Environment就等于拿到了最新的配置。
也就是说这里就是为了加载到最新的配置,因此才新建一个新的SpringApplication并执行它的run()方法的,这是拿到最新配置最省事的做法。
然后解析第二件事情:销毁被@RefreshScope注解修饰的bean。
@RefreshScope注解修饰的bean的属性引用的配置都是会动态刷新的,也就是说如果我们更新了配置,那么它就会读到最新的配置。
如果让我们去实现这个功能,我们第一时间想到的是拿到最新的配置,重新给这些bean赋值。这么做确实是可以的,就是太麻烦了。
因此最好的做法就是把它们销毁,下次如果要用到这个bean,Spring发现没有,就会重新创建一个并初始化,由于当前Environment已经被覆盖了最新的配置,因此新创建的bean的属性引用到的配置值就是最新的。
带着对原理理解的认知,我们就可以去看源码了。
源码解析
RefreshEventListener#onApplicationEvent(ApplicationEvent)
spring-cloud-context的spring.factories文件指定了自动配置类RefreshAutoConfiguration,通过SpringBoot的自动装配机制,会自动加载并解析RefreshAutoConfiguration。
RefreshAutoConfiguration中通过@Bean注解注册了一个监听器RefreshEventListener。
RefreshEventListener的onApplicationEvent方法监听RefreshEvent事件并进行处理。
public void onApplicationEvent(ApplicationEvent event) {
...
handle((RefreshEvent) event);
...
}
public void handle(RefreshEvent event) {
...
// 调用ContextRefresher的refresh()方法进行配置刷新操作
Set<String> keys = this.refresh.refresh();
log.info("Refresh keys changed: " + keys);
...
}
RefreshEventListener的onApplicationEvent方法监听RefreshEvent事件后,会调用ContextRefresher的refresh()方法进行配置刷新操作。
这个ContextRefresher也是在RefreshAutoConfiguration中通过@Bean注册到Spring容器中的,并且会作为RefreshEventListener构造方法的参数。
ContextRefresher#refresh()
public synchronized Set<String> refresh() {
// 第一件事情:创建一个新的SpringApplication再跑一遍run()方法
Set<String> keys = refreshEnvironment();
// 第二件事情:销毁被@RefreshScope注解修饰的bean,
// 调用的是RefreshScope#refreshAll()方法
this.scope.refreshAll();
return keys;
}
ContextRefresher的refresh()方法中的这两行代码,做的就是我们上面说的两件事情。
refreshEnvironment()方法会创建一个新的SpringApplication再跑一遍run()方法,得到一个新的Environment,获取里面最新的配置,覆盖到当前环境的Environment中。
this.scope.refreshAll()方法则是销毁被@RefreshScope注解修饰的bean,调用的是RefreshScope#refreshAll()方法。下一从容器中获取时就会重新创建并初始化,这个bean引用的配置值自然就是最新的。
ContextRefresher#refreshEnvironment()
public synchronized Set<String> refreshEnvironment() {
...
addConfigFilesToEnvironment();
...
}
ConfigurableApplicationContext addConfigFilesToEnvironment() {
...
try {
// 使用当前环境的Environment,copy出一个新的Environment
StandardEnvironment environment = copyEnvironment(
this.context.getEnvironment());
// SpringApplicationBuilder是SpringApplication构造器
// 会创建一个新的SpringApplication
SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
.bannerMode(Mode.OFF).web(WebApplicationType.NONE)
// 新的Environment设置到SpringApplicationBuilder中
.environment(environment);
...
// SpringApplicationBuilder的run()方法,
// 里面会执行新建的SpringApplication的run()方法,
// 执行完后,新的Environment就会加载到最新的配置
capture = builder.run();
...
// 拿到当前环境的Environment中的MutablePropertySources
// MutablePropertySources包含了Environment中的所有属性
MutablePropertySources target = this.context.getEnvironment()
.getPropertySources();
...
// 遍历新的Environment中的所有属性
for (PropertySource<?> source : environment.getPropertySources()) {
// 这里面就是把新的Environment的属性覆盖到当前Environment中的逻辑
...
if (target.contains(name)) {
target.replace(name, source);
}
...
}
}
finally {
...
}
...
}
ContextRefresher#refreshEnvironment()方法的处理流程如下:
- 使用当前环境的Environment,copy出一个新的Environment
- 创建一个SpringApplicationBuilder,SpringApplicationBuilder是SpringApplication构造器,SpringApplicationBuilder的构造方法会创建一个新的SpringApplication
- 将新的Environment设置到SpringApplicationBuilder中
- 执行SpringApplicationBuilder的run()方法,里面会执行新的SpringApplication的run()方法,执行完后,新的Environment就会加载到最新的配置
- for循环遍历新的Environment中的所有属性,覆盖到当前Environment中
RefreshScope#refreshAll()
我们回到ContextRefresher的refresh()方法中,看一下第二行代码“this.scope.refreshAll();”里面的逻辑。
public void refreshAll() {
// 销毁被@RefreshScope注解修饰的bean
super.destroy();
// 发布一个RefreshScopeRefreshedEvent事件,
// 我们可以监听RefreshScopeRefreshedEvent事件,
// 从而得知@RefreshScope注解修饰的bean被刷新(也就是被销毁了)
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
RefreshScope的refreshAll()方法调用父类的destroy()方法销毁被@RefreshScope注解修饰的bean。
super.destroy()方法会进入到GenericScope的destroy方法中。
public void destroy() {
...
// 清空GenericScope中的缓存,
// 这里面缓存的都是@RefreshScope注解修饰的bean
// 只是每个bean都被包裹在一个BeanLifecycleWrapper对象中
Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
// 遍历上面返回的每个bean
for (BeanLifecycleWrapper wrapper : wrappers) {
try {
...
try {
// 销毁bean
// 如果实现了DisposableBean接口,执行bean的destroy()方法
// 如果配置了自定义销毁方法destroyMethod,执行它
wrapper.destroy();
}
...
}
catch (...) {...}
}
...
}
GenericScope的cache属性是一个缓存池,里面缓存了被@RefreshScope注解修饰的bean。只是这些bean每一个都被包装在一个BeanLifecycleWrapper对象中。
调用this.cache.clear()就是清空GenericScope的缓存,然后返回这里面的bean。
获取到this.cache.clear()方法返回的bean后,就for循环遍历每一个bean,调用BeanLifecycleWrapper的destroy()去销毁bean。
BeanLifecycleWrapper的destroy()最终会执行bean的销毁方:如果这个bean实现了DisposableBean接口,执行bean的destroy()方法;如果这个bean配置了自定义销毁方法destroyMethod,执行这个自定义销毁方法。
把@RefreshScope注解修饰的bean销毁后,下一次从Spring容器中获取时,就会重新创建并初始化,那么bean的属性引用到的配置值自然就是最新的,也就是达到了配置刷新的效果。
可能有人会有疑问,为什么销毁的是GenericScope中缓存的bean,而不是从Spring的单例缓存池中清除出去呢?
这里就要说到@RefreshScope注解的原理了。
@RefreshScope注解的原理
那是因为被@RefreshScope注解修饰的bean,都不会缓存到Spring的单例缓存池,而是缓存到GenericScope的cache缓存池中。
被@RefreshScope注解修饰的bean的作用域是自定义作用域,不是单例作用域,因此不会缓存到单例缓存池中。
@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注解上面有一个@Scope(“refresh”),代表被@RefreshScope注解修饰的bean的作用域都是名为“refresh”的自定义作用域,生成的bean会放入到这个作用域对应的Scope对象中,由于这里的作用域是refresh,那么就是放入到RefreshScope中。而RefreshScope继承了GenericScope,那么就是放入到GenericScope的cache缓存池中。
而@RefreshScope注解的proxyMode()属性默认是ScopedProxyMode.TARGET_CLASS。
/**
* Create a class-based proxy (uses CGLIB).
*/
TARGET_CLASS
可以看到注释上说使用CGLIB。
那么依赖到这个bean的属性,注入进去的不是这个bean本身,而是一个用CGLIB生成的代理对象。然后调用这个属性的方法时,调用的其实是这个代理对象,会通过代理对象调用GenericScope的get()方法从GenericScope的cache缓存池中获取到这个bean,然后调用这个bean的对应方法。
好像很抽象,画个图就知道了。
因此这样就形成了闭环:@Scope(“refresh”)使得该bean缓存在GenericScope的cache缓存池中,而proxyMode()属性是ScopedProxyMode.TARGET_CLASS使得引用该bean的属性被注入的都是CGLIB生成的代理对象,代理对象的增强逻辑又会从GenericScope的cache缓存池中取到真正的bean并调用对应方法。
我们看看GenericScope#get()方法:
public Object get(String name, ObjectFactory<?> objectFactory) {
// objectFactory包装成BeanLifecycleWrapper,放入cache中
BeanLifecycleWrapper value = this.cache.put(name,
new BeanLifecycleWrapper(name, objectFactory));
...
try {
// 调用BeanLifecycleWrapper的getBean()方法
return value.getBean();
}
catch (...) {...}
}
再看一下BeanLifecycleWrapper的getBean()方法:
public Object getBean() {
// bean属性为空,加双重锁,调用objectFactory.getObject()创建
// 如果bean顺序不为空,则不会再创建
if (this.bean == null) {
synchronized (this.name) {
if (this.bean == null) {
this.bean = this.objectFactory.getObject();
}
}
}
// 返回bean
return this.bean;
}
这个objectFactory是个什么东西呢?答案就在Spring的AbstractBeanFactory的doGetBean方法中:
objectFactory就是上面的这个lambda表达式,this.objectFactory.getObject()会调用createBean()方法创建bean。
而这里的scope对象就是RefreshScope,scope.get(beanName, () -> {…})会调用到GenericScope的get方法。
因此,把GenericScope中的cache缓存池清空了,那么下次再次获取该bean时,就会重新创建,重新创建的bean引用的配置就会被赋值为最新的配置值(因为此时Environment中的配置已被覆盖为最新的配置值),这也就是为什么被@RefreshScope注解修饰的bean的属性具有动态刷新的效果的原因。