https://zhuanlan.zhihu.com/p/623988831
https://cloud.tencent.com/developer/article/1695249
https://zhuanlan.zhihu.com/p/618729460
https://zhuanlan.zhihu.com/p/618729460
原文链接:https://blog.csdn.net/qq_49619863/article/details/129458433
###----------------------------------------------------理解nacos的配置变更如何更新到应用的配置上?
例子:
@RefreshScope
@RestController
public class TestController {
@Value("${t}")
private String t;
@RequestMapping("/test")
public String test() {
System.out.println("server port is :" + port);
return UUID.randomUUID().toString() + " server port is :" + port;
}
}
### 配置中心源码解析
前提:当引入spring-cloud-alibaba-nacos-config时,会同时引入spring-cloud-context包。
Nacos配置是怎么接入到Spring中的?先看BootStrapApplicationListener怎么来的吧?
spring boot启动,通过SpringAppplication的构造方法调用setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
添加ApplicationListener监听器。
-> new SpringApplication()
setListeners() spring-cloud-context 包的Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.cloud.bootstrap.BootstrapApplicationListener,\
org.springframework.cloud.bootstrap.LoggingSystemShutdownListener,\
org.springframework.cloud.context.restart.RestartListener
可以看到这里引入了BootstrapApplicationListener类,会把BootstrapApplicationListener添加进来。
SpringApplication的run方法执行时,当执行到prepareEnvironment环境准备时,
会广播ApplicationEnvironmentPreparedEvent事件,此时BootstrapApplicationListener监听到这个事件,
在onApplicationEvent方法中创建一个SpringApplicationBuilder,就是建造者模式,
里面就是创建SpringApplication的,然后设置一些属性,是一个最简单的一般上下文设置,
然后设置一个配置文件BootstrapImportSelectorConfiguration。
即builder.sources(BootstrapImportSelectorConfiguration.class); 此时主类是BootstrapImportSelectorConfiguration
-> BootstrapApplicationListener.onApplicationEvent()
bootstrapServiceContext()
-> BootstrapApplicationListener.bootstrapServiceContext()
builder.sources(BootstrapImportSelectorConfiguration.class);
ConfigurableApplicationContext context = builder.run();
-> SpringApplicationBuilder.run()
-> SpringApplication.run() 此时主类是BootstrapImportSelectorConfiguration
...
-> AbstractApplicationContext.invokeBeanFactoryPostProcessors()
invokeBeanFactoryPostProcessors()
...
-> ConfigurationClassPostProcessor.processConfigBeanDefinitions()
parser.parse(candidates); 此时主类是BootstrapImportSelectorConfiguration
-> ConfigurationClassParser.parse()
this.deferredImportSelectorHandler.process() 处理导入
-> ConfigurationClassParser$DeferredImportSelectorHandler.process()
-> ConfigurationClassParser$DeferredImportSelectorGroupingHandler.processGroupImports()
-> ConfigurationClassParser$DeferredImportSelectorGrouping.getImports()
-> ConfigurationClassParser$DefaultDeferredImportSelectorGroup.process()
public void process(AnnotationMetadata metadata, DeferredImportSelector selector) { selector是从主类解析的BootstrapImportSelector
for (String importClassName : selector.selectImports(metadata)) {
this.imports.add(new Entry(metadata, importClassName));
}
}
-> BootstrapImportSelector.selectImports() 调用SpringApplicationBuilder的run方法解析主类,
此时主类primarySources添加了前面注册进去的BootstrapImportSelectorConfiguration配置类。
BootstrapImportSelectorConfiguration通过注解@Import(BootstrapImportSelector.class)
导入BootstrapImportSelector
List<String> names = SpringFactoriesLoader.loadFactoryNames(BootstrapConfiguration) 加载自动配置的BootstrapConfiguration类型的配置类
names = {ArrayList@3842} size = 6
0 = "com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration"
1 = "org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration"
2 = "org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration"
3 = "org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration"
4 = "org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration"
5 = "com.alibaba.cloud.nacos.discovery.configclient.NacosDiscoveryClientConfigServiceBootstrapConfiguration"
在spring-cloud-context包的spring.factories中加载到PropertySourceBootstrapConfiguration:解析配置文件入口类。
首先, PropertySourceBootstrapConfiguration 通过注解 @Configuration 表明类本身是一个配置类,通过
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class) 注解表明只有开启配置属性解析开关,
PropertySourceBootstrapConfiguration 才会正常解析配置属性。
其次,PropertySourceBootstrapConfiguration 实现 ApplicationContextInitializer 接口类,其作用是在程序启动时,
调用initialize方法进行相关初始化工作,也就是接下来要讲的 ConfigServer 的客户端解析配置文件的核心流程。
在spring-cloud-starter-alibaba-nacos-config\2021.1\spring-cloud-starter-alibaba-nacos-config-2021.1.jar包的spring.factories中
加载到NacosConfigBootstrapConfiguration:会加载两个bean,一个是NacosConfigManager,另外一个是NacosPropertySourceLocator。
NacosConfigManager的构造函数中调用createConfigService方法,
该方法主要是为了创建ConfigService实例,创建的方式是通过反射来实现,
对应的属性填充是由nacosConfigProperties的assembleConfigServiceProperties方法来完成实例属性的填充。
NacosPropertySourceLocator的实例化,是将上面生成好的nacosConfigManager以及nacosConfigProperites填充到当前类中。
然后NacosPropertySourceLocator是继承了PropertySourceLocator类,重写了locate方法,
此方法是由bootstrap上下文的SpringApplication.prepareContext
方法中的applyInitializers里的PropertySourceBootstrapConfiguration.initialize调用。
-> SpringApplication.prepareContext()
applyInitializers(context)
-> SpringApplication.applyInitializers(context)
protected void applyInitializers(ConfigurableApplicationContext context) {
for (ApplicationContextInitializer initializer : getInitializers()) {
Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
ApplicationContextInitializer.class);
Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
initializer.initialize(context);
}
}
-> PropertySourceBootstrapConfiguration.initialize()
public void initialize(ConfigurableApplicationContext applicationContext) {
List<PropertySource<?>> composite = new ArrayList<>();
AnnotationAwareOrderComparator.sort(this.propertySourceLocators); 将属性源定位器propertySourceLocators排序
boolean empty = true;
ConfigurableEnvironment environment = applicationContext.getEnvironment();
for (PropertySourceLocator locator : this.propertySourceLocators) {
Collection<PropertySource<?>> source = locator.locateCollection(environment); 进行定位
if (source == null || source.size() == 0) {
continue;
}
List<PropertySource<?>> sourceList = new ArrayList<>();
for (PropertySource<?> p : source) {
if (p instanceof EnumerablePropertySource<?> enumerable) {
sourceList.add(new BootstrapPropertySource<>(enumerable));
}
else {
sourceList.add(new SimpleBootstrapPropertySource(p));
}
}
logger.info("Located property source: " + sourceList);
composite.addAll(sourceList); 放进封装成PropertySource并放进集合里
empty = false;
}
if (!empty) {
MutablePropertySources propertySources = environment.getPropertySources();
String logConfig = environment.resolvePlaceholders("${logging.config:}");
LogFile logFile = LogFile.get(environment);
for (PropertySource<?> p : environment.getPropertySources()) {
if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
propertySources.remove(p.getName());
}
}
insertPropertySources(propertySources, composite);
reinitializeLoggingSystem(environment, logConfig, logFile);
setLogLevels(applicationContext, environment);
handleIncludedProfiles(environment);
}
}
this.propertySourceLocators = {ArrayList@6471} size = 1
0 = {NacosPropertySourceLocator@4398}
第一阶段:解析nacos的配置存到环境中的propertySources属性中
-> NacosPropertySourceLocator.locate()
loadSharedConfiguration(composite); 共享配置文件
loadExtConfiguration(composite); 拓展配置文件
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env); 应用自身的配置文件
-> NacosPropertySourceLocator.loadApplicationConfiguration() https://blog.csdn.net/qq_40125268/article/details/131478039
-> NacosPropertySourceLocator.loadNacosPropertySource() 加载nacos中的配置封装成NacosPropertySource
nacosPropertySourceBuilder.build()
-> NacosPropertySourceBuilder.build()
loadNacosData()
-> NacosPropertySourceBuilder.loadNacosData() 真正加载配置数据的方法
data = configService.getConfig(dataId, group, timeout) 通过之前实例化好的ConfigService类,调用其getConfig方法获取配置数据
-> NacosConfigService.getConfigInner() 重点:
String content = LocalConfigInfoProcessor.getFailover(worker.getAgentName(), 通过dataId和group 来获取nacos本地配置
dataId, group, tenant)
ConfigResponse response = worker.getServerConfig(dataId, group, tenant, timeoutMs, false) 如果nacos的本地配置为空,则需要向服务端发送请求,获取服务配置。
nacos 1.x 版本是通过http请求,在nacos 2.x 已经改为gRpc方式。
通过ClientWorker类的getServiceConfig来向服务端发送请求获取配置信息
-> ClientWorker.queryConfig()
ConfigQueryResponse response = requestProxy(rpcClient, request, readTimeouts) 本地配置为空才会触发请求服务器的配置,并保存配置快照
LocalConfigInfoProcessor.saveSnapshot()
退栈
-> PropertySourceBootstrapConfiguration.environment()
private Environment environment(MutablePropertySources incoming) {
ConfigurableEnvironment environment = new AbstractEnvironment() {
};
for (PropertySource<?> source : incoming) {
environment.getPropertySources().addLast(source); source: bootstrapProperties-ww-nacos.properties,DEFAULT_GROUP
}
return environment;
}
至此,环境中有nacos的配置了
第七步:由于spring boot启用了@EnableAutoConfiguration,所以nacos-config包的spring.factories文件中会加载到NacosConfigAutoConfiguration、 NacosConfigEndpointAutoConfiguration。
第八步:注入NacosConfigAutoConfiguration刷新配置用,在类中注入如下bean:
NacosRefreshHistory
这个类就是用来对获取的配置数据内容用md5加密作为历史记录,然后存在在一个集合里。
NacosRefreshProperties(废弃了)
NacosContextRefresher做监听用,监听ApplicationReadyEvent事件,上下文都准备好了,然后注册一个监听器到ConfigService中,
ConfigService根据监听的情况去获取配置信息,获取到配置信息后,调用监听器的innerReceive方法,
让nacosRefreshHistory保存记录,让上下文去进行RefreshEvent通知。
第九步:注入NacosConfigEndpointAutoConfiguration监控用,在类中注入如下bean:
NacosConfigEndpoint
保存NacosConfigProperties和NacosRefreshHistory。做监控用,跟actuator相关,可以获取很多信息。比如用http://xxx:xxx/actuator/nacos-config。
NacosConfigHealthIndicator
通过ConfigService去检查服务器健康状况。
第二阶段:更改nacos配置使值变动
当远程 Nacos Config Server 中的配置信息发生了变更,我们springboot应用作为Nacos Config Client 是如何感知到的呢?
此处我们需要知道一点前置知识,那就是两个数据消费模型,push模型和pull模型。
Push 模型:当 Server 端的数据发生了变更,其会主动将更新推送给 Client。Push 模型 适合于 Client 数量不多,且 Server 端数据变化比较频繁的场景。其实时性较好,但其需要维护长连接,占用系统资源。
Pull 模型:需要 Client 定时查看 Server 端数据是否更新。其实时性不好,且可能会产生数据更新的丢失。
那什么是长轮询呢?
长轮询模型整合了 Push 与 Pull 模型的优势。Client 仍定时发起 Pull 请求,查看 Server 端数据是否更新。
若发生了更新,则 Server 立即将更新数据以响应的形式发送给 Client 端;
若没有发生更新,Server 端不会发送任何信息,但其会临时性的保持住这个连接一段时间。
若在此时间段内,Server 端数据发生了变更,这个变更就会触发 Server 向 Client 发送变更结果。
这次发送的执行,就是因为长连接的存在。
若此期间仍未发生变更,则放弃这个连接。 等待着下一次 Client 的 Pull 请求。
长轮询模型,是 Push 与 Pull 模型的整合,既减少了 Push 模型中长连接的被长时间维护 的时间,又降低了 Pull 模型实时性较差的问题。
Nacos断开连接默认为3秒
Nacos实时拉取配置文件
1、ClientWorker类
起点就是这个ClientWorker类(com.alibaba.nacos.client.config.impl.ClientWorker),在构造时,就会启动一个定时任务,
每0s检测一次,具体执行的任务就是这个startInternal()
-> ClientWorker$ConfigRpcTransportClient.executeConfigListen()
refreshContentAndCheck()
-> ClientWorker.refreshContentAndCheck()
cacheData.checkListenerMd5() 因为在这个方法里面,触发了监听器,从而触发了Spring等相关操作
在checkListenerMd5中会调用safeNotifyListener,从而调用listener,
而这个listener就是NacosContextRefresher,
-> CacheData.safeNotifyListener()
Runnable job 新建一个job
INTERNAL_NOTIFIER.submit(job) 开启一个线程执行job
当执行到listener.receiveConfigInfo(contentTmp),就会跳转到NacosContextRefresher对应的实现
-> NacosContextRefresher.registerNacosListener() 发布RefreshEvent事件
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"))
至此 NacosClient端的主要任务已经完成了,通过一个applicationContext发送event。接下来就看applicationContext又会对此做什么样的反应。
ApplicationContext对RefreshEvent的反应
-> AbstractApplicationContext.publishEvent()
执行getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType) 之后就会触发RefreshEventListener的对应事件
-> RefreshEventListener.onApplicationEvent()
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationReadyEvent) {
handle((ApplicationReadyEvent) event);
}
else if (event instanceof RefreshEvent) { 处理刷新事件
handle((RefreshEvent) event);
}
}
-> RefreshEventListener.handle()
执行Set<String> keys = this.refresh.refresh()
-> ContextRefresher.refresh()
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
this.scope.refreshAll();
return keys;
}
在refreshEnvironment()中,除了刷新了Environment中properties,还发送了一个新的event事件,即EnvironmentChangeEvent事件。
而这个事件对应的动作是对那些被@ConfigurationProperties注解的类的属性的刷新。
在refreshAll()中,
public void refreshAll() {
super.destroy(); Collection<BeanLifecycleWrapper> wrappers = this.cache.clear() 清空缓存
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
至此,因Nacos中属性变更触发,Spring做了以下事情
更新environment中的propertSource属性
发送EnvironmentChangeEvent事件->即会被@ConfigurationProperties类更新属性
清除了GenericScope中的cache
第三阶段:被@RefreshScope注解的类的属性是什么时候更新的
对于被@RefreshScope注解的类,会对应一个LockedScopedProxyFactoryBean的MethodInterceptor的类,
所以在代理时,会调用到LockedScopedProxyFactoryBean(GenericScope的内部类)的invoke方法。
...
-> org.springframework.web.method.support.InvocableHandlerMethod#doInvoke()
-> GenericScope$LockedScopedProxyFactoryBean#invoke()
调用堆栈如下:
invoke:463, GenericScope$LockedScopedProxyFactoryBean (org.springframework.cloud.context.scope)
proceed:184, ReflectiveMethodInvocation (org.springframework.aop.framework)
proceed:756, CglibAopProxy$CglibMethodInvocation (org.springframework.aop.framework)
intercept:708, CglibAopProxy$DynamicAdvisedInterceptor (org.springframework.aop.framework)
test:-1, TestController$$SpringCGLIB$$0 (com.yfw.learn)
invoke0:-1, NativeMethodAccessorImpl (jdk.internal.reflect)
invoke:77, NativeMethodAccessorImpl (jdk.internal.reflect)
invoke:43, DelegatingMethodAccessorImpl (jdk.internal.reflect)
invoke:568, Method (java.lang.reflect)
doInvoke:207, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest:152, InvocableHandlerMethod (org.springframework.web.method.support)
invokeAndHandle:118, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
invokeHandlerMethod:884, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handleInternal:797, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle:87, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
doDispatch:1081, DispatcherServlet (org.springframework.web.servlet)
doService:974, DispatcherServlet (org.springframework.web.servlet)
...省略部分堆栈
执行 ReflectionUtils.invokeMethod(method,
advised.getTargetSource().getTarget(),
invocation.getArguments())
-> SimpleBeanTargetSource.getTarget()
执行 getBeanFactory().getBean(getTargetBeanName()) 其中:getTargetBeanName()=scopedTarget.testController
从BeanFactory中获取scopedTarget.testController
而scopedTarget.testController的beanDefination是被定义为scope,那么会走到scope对应的创建bean的逻辑,即
Object scopedInstance = scope.get(beanName, () -> {})
-> 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;
}
}
当再项目中再次引用到@RefreshScope注解的实例时,因为实例首先是从 GenericScope中的cache获取,
因为之前被清空了,所以获取不到,
那么就得走原来的getBean创建实例的过程,
那么就会重新用最新的Environment中properties进行属性渲染,所以此时获取到的bean中的属性便是最新的。
总结:
第一阶段:解析nacos的配置存到环境中的propertySources属性中;
第二阶段:当Nacos Client 会每10s 拉取一次Nacos Server上变动的配置,当确实是有配置发生变化时,
就调用applicationContext就会发出一个RefreshEvent事件,然后就会发送给所有的监听者,
其中一个重要的listener就是NacosContextRefresher,当它接收到RefreshEvent事件时,会更新Environment中的properties,
然后清空GenericScope中的cache(这个cache的作用就是缓存scope类型的对象,也即被@RefreshScope注解的类)。
第三阶段:当再项目中再次引用到@RefreshScope注解的实例时,因为实例首先是从 GenericScope中的cache获取,
因为之前被清空了,所以获取不到,那么就得走原来的getBean创建实例的过程,
那么就会重新用最新的Environment中properties进行属性渲染,所以此时获取到的bean中的属性便是最新的。