Nacos自动刷新配置源码解析
文章目录
- 前言
- 一、@RefreshScope
- 1.1、@RefreshScope的扫描
- 1.2、代理类的创建
- 1.3、GenericScope
- 1.4、代理类的调用
- 二、NacosConfigAutoConfiguration
- 三、RefreshEventListener
- 3.1、refreshEnvironment
- 3.2、refreshAll
- 总结 : @RefreshScope 注解的工作机制解析
前言
在Nacos配置中心的使用中,当配置中心的相关配置信息发生变更,客户端(微服务)通常需要重新启动使变更生效。Nacos也提供了配置项热更新的功能,当配置中心中的配置被修改并发布后,标注了@RefreshScope
的 bean 会重新加载最新的配置。
@RefreshScope
的作用范围是Spring的Bean,配置项热更新的原理是@RefreshScope
会让 bean 在配置变更时被销毁并重新创建。
@RestController
@RefreshScope
public class TestController {@Value("${common.name}")private String name;@Value("${common.age}")private String age;@RequestMapping("/m1")public void test() {System.out.println(name + ":" + age);}
}
common.yml的初始值
访问http://localhost:8060/m1
修改配置信息
不重启应用,重新访问http://localhost:8060/m1,配置信息发生变更
一、@RefreshScope
@RefreshScope
是一个复合注解,其中包含了@Scope("refresh")
注解:
最关键的属性:proxyMode
设置成了TARGET_CLASS
,即当使用 @RefreshScope(或者其他 @Scope 注解)标记一个 Bean 时,Spring 将自动为该 Bean 创建一个代理对象,以便在作用域变动(如刷新)时动态替换真正的实例,外部依赖的其实是“代理对象”,当配置刷新时,RefreshScope 会销毁旧的真实对象,下次调用时重新构造(外部访问的依旧是代理对象)
1.1、@RefreshScope的扫描
在Spring Boot 启动过程中,会调用容器的refresh方法。在invokeBeanFactoryPostProcessors
这一步,会在ClassPathBeanDefinitionScanne
的doScan
方法中,对目标路径下的所有类进行扫描。(如果没有指定,则扫描@SpringBootApplication
注解所在的引导类的类路径)
在doScan
方法中,会去委托scopeMetadataResolver
扫描@Scope
注解:
最终会获取注解中的 proxyMode
,设置进ScopeMetadata
供后续使用。
然后会执行doScan中的AnnotationConfigUtils.applyScopedProxyMode:
1.2、代理类的创建
在AnnotationConfigUtils.applyScopedProxyMode中,proxyTargetClass属性为true:
最终调用到的是ScopedProxyUtils#createScopedProxy,在该方法中,首先会对bean的名称进行处理:(testController -> scopedTarget.testController )
然后会创建一个ScopedProxyFactoryBean
目标代理对象:
那么ScopedProxyFactoryBean
是在什么时候被触发的?
ScopedProxyFactoryBean
实现了BeanFactoryAware
接口,会在bean生命周期的初始化这一步被调用,并执行setBeanFactory
方法:
在ScopedProxyFactoryBean
的setBeanFactory
方法中,完成了代理对象的创建,并且赋值给了proxy
属性。
上面的过程,是在Spring启动时完成的
1.3、GenericScope
在1.2、代理类的创建 中,为什么有:
RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
这一行代码?原因在于,GenericScope
在执行父类BeanDefinitionRegistryPostProcessor
的postProcessBeanDefinitionRegistry
方法时,需要对于目标类的Bean定义进行改造,主要是将目标类型替换为LockedScopedProxyFactoryBean
:
1.4、代理类的调用
cglib在调用目标方法前,会先执行TargetSource
的getTarget
方法:
这里的TargetSource
,是在spring启动过程中,bean生命周期的初始化这一步被调用,并执行setBeanFactory
方法时被设置进去的:
SimpleBeanTargetSource
的getTarget
方法,是再次调用getBean
,这次的bean名称,是scopedTarget.testController
,并且它的作用域不再是单例,而是refresh
:
单例bean会放在singletonObjects
单例池中,refresh
作用域的bean,也会放在一个类似于单例池的cache
中:
在GenericScope
的get
中,重点是getBean:
如果获取不到,就会回调lambda表达式中的逻辑,创建bean:
当执行目标方法时,实际会调用LockedScopedProxyFactoryBean
的invoke
方法,该类是上一步中提到的ScopedProxyFactoryBean
的子类:
流程图链接
二、NacosConfigAutoConfiguration
在客户端引入的spring-cloud-starter-alibaba-nacos-config
jar包中的spring.factories
文件中,有一个关键的类:NacosConfigAutoConfiguration
。它与Nacos配置中心的自动配置有关。其中与自动刷新配置相关的,是NacosContextRefresher
:
它实现了ApplicationListener<ApplicationReadyEvent>
接口:
在Spring Boot启动过程中,就会触发监听ApplicationReadyEvent
事件的类的onApplicationEvent
方法:
NacosContextRefresher
的onApplicationEvent
方法:
在registerNacosListenersForApplications
方法中,会先判断是否支持自动刷新配置,然后会为每一个dataId绑定一个监听器:
当这个dataId + group
下的配置有变更时,就会触发上面的 innerReceive
,发布一个RefreshEvent
类型的事件。(这里只是将Listener进行注册,实际是在配置有变更的时候才会触发)
上述过程是在Spring Boot启动过程中完成的
三、RefreshEventListener
真正处理该事件的,是RefreshEventListener
,那么这个监听器又是什么时候被加入Spring 容器的呢?
在客户端的spring-cloud-context
jar包中的spring.factories
文件中,有一个RefreshAutoConfiguration
,其中就将RefreshEventListener
注册成了bean。
在RefreshEventListener
的handle
方法中,又会去调用ContextRefresher
的refresh
方法:
ContextRefresher
也是RefreshAutoConfiguration
中的一个bean:
ContextRefresher
的refresh
方法:
3.1、refreshEnvironment
在refreshEnvironment
中,又做了三步操作:
extract(this.context.getEnvironment().getPropertySources());
会提取出除了SYSTEM
、JNDI
、SERVLET
的其他环境变量。(热更新前的参数值保存一份)
addConfigFilesToEnvironment
会模拟 Spring Boot 启动时加载配置文件的过程,把最新的配置加载出来,替换当前上下文的配置源(PropertySources),并完成一次“无感知的热刷新”。
把已有配置源替换/插入进去,保留系统变量、JVM 参数、已有上下文等环境信息。相当于增量更新,只替换由配置文件加载的部分,别的都不动。
changes(before,extract(this.context.getEnvironment().getPropertySources())).keySet();
会找出新的参数值和热更新前的参数值进行对比,找出变化的部分,然后发布一个EnvironmentChangeEvent
环境变更事件。
3.2、refreshAll
RefreshScope
的refreshAll
方法,首先会去把标注了@RefreshScope
的旧 Bean 销毁,然后发布一个RefreshScopeRefreshedEvent
类型的事件:
标注了@RefreshScope
注解的类,可以同时实现ApplicationListener<RefreshScopeRefreshedEvent>
。监听该事件。
这里的destroy
方法,实际调用的是父类GenericScope
的,会将refresh
作用域缓存池清空,然后销毁其中的所有bean。下一次访问目标类方法的时候,发现缓存池中没有,就会再次创建目标类,从而达到刷新的效果。
总结 : @RefreshScope 注解的工作机制解析
当某个类(如 TestController
)被标注为 @RefreshScope
时,Spring 应用在启动过程中会在应用上下文中注册两个相关的 Bean:
- 名称为
testController
的 Bean,是由ScopedProxyFactoryBean
创建的 CGLIB 动态代理对象,作用域为单例(singleton
); - 名称为
scopedTarget.testController
的 Bean,是实际的目标类TestController
,其作用域为refresh
。
代理对象的主要作用是拦截对目标类的访问,在方法调用前,通过内部的 TargetSource
获取当前上下文中最新的目标实例。当目标实例因配置变更而被刷新时,代理对象能够自动指向最新的实例,从而实现运行时的动态刷新。
在 Spring Boot 应用启动时,若启用了配置自动刷新机制,Spring Cloud 会为每个 Nacos 的 dataId
注册一个配置变更监听器,并通过 NacosContextRefresher
将变更事件传递到 Spring 环境。容器中也会注册一个 RefreshEventListener
,监听并响应配置变更事件。
配置刷新由 ContextRefresher
的 refresh()
方法驱动,其核心流程如下:
- 提取环境变量:排除系统环境(如
SYSTEM
、JNDI
、SERVLET
)后,提取用户自定义的配置项,并缓存刷新前的配置值; - 加载最新配置:模拟 Spring Boot 启动过程,重新加载配置源(
PropertySources
),更新环境上下文; - 检测参数变更:对比新旧配置,识别变化项,并发布
EnvironmentChangeEvent
环境变更事件; - 刷新作用域 Bean:调用
refreshAll()
方法,清空refresh
作用域的缓存池并销毁其中的 Bean。下一次访问该 Bean 时,将自动重新创建实例,从而完成动态刷新。