大家好,本文将和大家聊一聊SpringCloudAlibaba中的微服务组件Nacos。Nacos既可以是注册中心,也可以是配置中心。本文主要讲一下做配置中心时客户端的一些设计。主要从源码层面分析。相信看完这篇文章,你应该明白Nacos客户端的工作原理了。有了更深刻的认识。SpringCloud应用启动pulltoconfigure我们之前写过一篇文章,介绍了Spring提供的一些扩展机制。说到ApplicationContextInitializer,这个扩展是在上下文准备阶段(prepareContext),在容器刷新前做一些初始化工作。比如我们常用的配置中心客户端,基本都继承了initializer,在容器刷新前,配置从远程拉取到本地,然后封装成PropertySource放在Environment中使用。在SpringCloud场景下,SpringCloud规范提供了PropertySourceBootstrapConfiguration来继承ApplicationContextInitializer,同时也提供了一个PropertySourceLocator,两者配合完成对配置中心的访问。从上面的截图可以看出,在初始化单例对象PropertySourceBootstrapConfiguration时,Spring容器中的所有PropertySourceLocator实现都会被注入。然后循环遍历initialize方法中的所有PropertySourceLocators,获取配置。从这里可以看出,SpringCloud应用支持引入多个配置中心。获取到配置后,调用insertPropertySources方法将所有的PropertySources(封装后的配置文件)插入到Spring环境变量environment中。上图是spring-cloud-starter-alibaba-nacos-config包提供的自动组装类中NacosPropertySourceLocator的定义。该类继承上述PropertySourceLocator,重写了locate方法读取配置。我们来分析NacosPropertySourceLocator。locate方法只提取主要流程代码。可以看到Nacos启动会加载以下三个配置文件,即我们在bootstrap.yml文件中配置的扩展配置extension-configs和shared-configs,并应用自己的配置,加载到配置文件后,会封装到NacosPropertySource中返回。publicPropertySource>locate(Environmentenv){//生成一个NacosConfigService实例,后续的配置操作都是围绕这个类进行的ConfigServiceconfigService=nacosConfigManager.getConfigService();if(null==configService){log.warn("未找到配置服务实例,无法从nacos加载配置");返回空值;}longtimeout=nacosConfigProperties.getTimeout();//配置获取(使用configService)、配置打包、配置缓存等操作nacosPropertySourceBuilder=newNacosPropertySourceBuilder(configService,timeout);CompositePropertySourcecomposite=newCompositePropertySource(NACOS_PROPERTY_SOURCE_NAME);加载共享配置(复合);loadExtConfiguration(复合);loadApplicationConfiguration(复合、dataIdPrefix、nacosConfigProperties、env);配置,分别不带扩展后缀,应用带扩展后缀为application.yml带环境,带扩展后缀application-prod.yml,从上到下加载优先级依次递增。加载的核心方法是loadNacosDataIfPresent->loadNacosPropertySourcebuild方法调用loadNacosData获取配置,然后封装到NacosPropertySource中,并将该对象缓存在NacosPropertySourceRepository中,后面会使用loadNacosData方法将实际的配置加载请求委托给configService,然后解析返回的字符串。解析器实现了PropertySourceLoader接口,支持yml、properties、xml、json。种类。getConfig方法会调用getConfigInner方法通过namespace、dataId、group唯一定位一个配置文件。首先获取本地缓存文件的配置内容。如果有,直接返回。如果第一步在本地没有找到对应的配置文件,就开始远程拉取。Nacos2.0及以上版本使用Grpc协议进行远程通信,1.0及以下版本使用Http协议进行远程通信。这里以1.x为例说明getServerConfig方法会构造最终的http请求参数并调用。如果返回ok,则将返回的内容写入本地缓存文件并返回。至此,在项目启动时(上下文准备阶段),我们已经拉取了远程Nacos中的配置,并封装为NacosPropertySource放在Spring环境变量中。监听器注册在上一章中,我们说了服务启动时,会从远程Nacos服务器上拉取配置。在本章中,我们将讨论如何实时通知客户端配置更改。首先,您需要注册一个监听器。主要看NacosContextRefresher这个类,它会监听服务启动时释放的ApplicationReadyEvent事件,然后注册配置监听器。registerNacosListenersForApplications方法会判断,如果开启了自动刷新机制,就会注册监听器。上一章我们提到拉取的配置会缓存在NacosPropertySourceRepository中。这里会从缓存中获取所有配置,然后循环注册监听器(如果配置文件中配置刷新字段为false,则不会注册监听器)。我们可以看到监听器是在dataId+groupId+namespace这个维度注册的,监听器的主要操作是三步。REFRESH_COUNT++,上面提到的loadNacosPropertySource方法,有用的是在NacosRefreshHistory#records中添加一条刷新记录,发布一个RefreshEvent事件。该事件由SpringCloud提供。主要用于通过ConfigService进行环境变化刷新的注册操作,在ClientWorker处理中,这块会创建一个CacheData对象,主要用来管理监听器,也是一个很重要的类。CacheData中的字段如下图所示。ManagerListenerWrap包裹了Listener层,内部保存了listener、最近更改的内容、md5(用于判断配置是否发生变化)。并且在addCacheDataIfAbsent方法中,会将刚刚创建的CacheData缓存到ClientWorker中的一个Map中,后面会用到。至此,服务启动后,为每一个需要支持热更新的配置注册一个监听器,监听远程配置的变化,对配置热更新做相应的处理。服务端拉取配置,服务启动后,为需要支持热更新的配置注册一个监听器。本章我们来谈谈如何处理配置变化。回到上面提到的NacosPropertySourceLocator的locate方法。这个方法会先获取一个ConfigService。NacosConfigManager中会创建一个ConfigService单例对象。创建过程最终会委托给ConfigFactory使用反射创建NacosConfigService的实例对象。NacosConfigService是一个非常核心的类。配置获取和监听器注册都需要经过这个。我们看NacosConfigService的构造函数,会创建一个ClientWorker类的对象,ClientWorker类是实现配置热更新的核心类。ClientWorker的构造函数会创建两个线程池,executor每10ms检查一次配置变化,executorService主要用于处理长轮询请求。在checkConfigInfo方法中,会创建一个长轮询任务,丢到executorService线程池中处理。LongPollingRunnable的run方法代码比较多,主要流程如下:获取上一章提到的缓存cacheMap,然后遍历判断配置是否使用本地缓存方式,然后调用checkListenerMd5检查读取本地缓存文件的内容Md5是否与上次更新的Md5相同?如果没有,则调用safeNotifyListener通知监听器处理,更新listenerWrap中的content和Md5checkUpdateDataIds。在该方法中,会将所有的dataId按照定义的格式拼接成一个字符串,向服务器发送一个长轮询请求。Long-Pulling-Timeout超时默认为30秒。如果服务器上没有配置更改,请求将一直保持到超时。如果配置有变化,直接返回变化后的dataId列表。第二步获取到变化的dataId后,会调用getServerConfig获取最新的配置内容,然后遍历调用checkListenerMd5检查最新拉取的配置内容的Md5是否与上次更新的Md5相同。如果不一样,调用safeNotifyListener通知监听器处理更新listenerWrap中的内容,md5checkListenerMd5方法如下,主要判断两个md5是否相同,如果不同,则调用safeNotifyListener来过程。safeNotifyListener方法主要是调用listener的receiveConfigInfo方法,然后更新listenerwrapper中的lastContent和lastCallMd5字段。监听器要执行的方法上面已经说了,这里贴出截图,主要是释放RefreshEvent事件。至此Nacos的处理流程结束,RefreshEvent事件主要由SpringCloud相关类来处理。RefreshEvent事件处理RefreshEvent事件将由RefreshEventListener处理,该侦听器包含一个ContextRefresher对象。如下图,refreshEnvironment会刷新Spring的环境变量,其实是通过updateEnvironment方法刷新的。具体刷新思路是重新创建一个Spring容器,然后将新容器中的环境信息设置为原来的SpringEnvironment。获取到所有变化的配置项后,发布一个环境变化的EnvironmentChangeEvent事件。ConfigurationPropertiesRebinder会监听EnvironmentChangeEvent事件。监听事件后,会销毁并重新初始化所有标有ConfigurationProperties注解的配置类。之后,我们的配置类中的属性将是最新的。这里提到了ConfigurationProperties注解标注的配置类会被rebind,那么普通组件类中@Value注解标注的属性如何生效呢?这实际上需要@RefreshScope注解才能生效。我们继续回到上面的refresh()方法,接下来会有一个refreshAll操作,会调用父类的destroy方法。父类是GenericScope。我们知道Spring中的bean有Scope的概念。Spring默认使用Scopewithsingleton和prototype。它还提供了一个范围扩展接口。通过实现这个接口,我们可以定义自己的Scope。从doGetBean方法可以看出,这些自定义Scope类型对象的管理都会交给对应的Scope实现来管理。SpringCloud实现的RefreshScope用于在运行时动态刷新bean。RefreshScope继承了GenericScope并提供了get和destroy方法。GenericScope内部有一个缓存,用于保存该Scope类型的所有对象。回到主线,所以在refreshAll中调用super.destroy方法的时候,scope中的所有bean都会被销毁,下次get的时候会重新创建bean,新创建的bean会有我们最新的配置。至此,我们就实现了配置热更新的效果。总结文章从服务启动时的配置拉取、服务启动后的配置监听器注册、配置变更后的热更新三个方面,从源码层面分析了整个原理。希望对大家有所帮助。个人开源项目DynamicTp是一个基于配置中心的轻量级动态线程池管理工具。主要功能可以概括为动态参数调整、通知告警、运行监控、三方包线程池管理。目前累计2.2??kstar,欢迎大家试用,感谢大家的star,欢迎pr,营业后一起贡献开源。官网:https://dynamicctp.cngitee地址:https://gitee.com/dromara/dynamic-tpgithub地址:https://github.com/dromara/dynamic-tp
