理解nacos的配置变更如何更新到应用的配置上

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中的属性便是最新的。

  • 26
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值