【深入理解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出一个新的EnvironmentStandardEnvironment environment = copyEnvironment(this.context.getEnvironment());// SpringApplicationBuilder是SpringApplication构造器// 会创建一个新的SpringApplicationSpringApplicationBuilder 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注解修饰的beansuper.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();// 遍历上面返回的每个beanfor (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();}}}// 返回beanreturn 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的属性具有动态刷新的效果的原因。