【深入理解SpringCloud微服务】深入理解nacos配置中心(五)——客户端监听配置变更并刷新的源码分析
原理回顾
在之前的《宏观理解nacos配置中心原理》这一篇文章中,我们分析过nacos客户端监听配置变更并刷新配置的这一部分的流程。
首先是nacos会给当前客户端引用的每个配置文件都添加一个监听器,这个监听器收到通知会触发配置刷新的操作。
以上是给被客户端引用的配置文件添加监听器的流程。在SpringBoot启动完成的时候,会发布ApplicationReadyEvent事件,触发Spring的事件监听机制,该事件被NacosContextRefresher接收并处理,然后NacosContextRefresher的onApplicationEvent方法就会给每个被客户端引用的配置文件创建一个CacheData对象,然后在CacheData对象中添加一个上面所说的监听器。
我们还解析了这个CacheData的结构,包含了配置文件的dataId、group、tenant(也就是namespace)、配置文件内容content,配置文件MD5,监听器列表listeners等成员属性。
既然给每个被客户端引用的配置文件添加了监听器,那么就会有触发该监听器的地方。
在nacos配置中心客户端启动的时候,会创建NacosConfigService,NacosConfigService的构造方法中又会创建ClientWorker,然后ClientWorker初始化的时候会启动定时任务。这个监听器就是在ClientWorker的定时任务中触发的。
ClientWorker的定时任务流程大概四这样:
- 通过http请求获取服务端中发生变更的DataID列表。
- 根据这些DataID列表再次请求服务端获取对应的配置文件内容content。
- 再把content赋值到CacheData并重新计算MD5值。
- 计算出来的MD5值跟CacheData的listeners中每个Listener保存的老MD5值比较,如果不一致,就会调用Listener的receiveConfigInfo()方法。
这里调用Listener的receiveConfigInfo()方法就会调用到NacosContextRefresher添加的监听器的receiveConfigInfo()方法,也就是说监听器就是在这里触发的。
NacosContextRefresher添加的这个监听器的receiveConfigInfo()方法会发布一个RefreshEvent事件。
发布的RefreshEvent事件会触发Spring的事件监听机制,会被spring-cloud-context包提供的RefreshEventListener监听器接收并处理,触发配置刷新。这里的配置刷新不是nacos实现的,nacos只是发布了一个RefreshEvent事件,后续的都是spring-cloud-context里面的公共逻辑。
因此我们本篇文章只会讲到nacos发布RefreshEvent事件,后续的逻辑在下一篇文章中分析。
源码分析
给当前微服务引用的配置文件创建CacheData并添加监听器
ApplicationReadyEvent事件发布
SpringBoot启动之后,在SpringApplication的run方法执行到最后,会在调用listeners.running(context)时发布ApplicationReadyEvent事件。
SpringApplication#run(java.lang.String…)
public ConfigurableApplicationContext run(String... args) {
...
try {
listeners.running(context);
}
catch (...) {...}
return context;
}
SpringApplicationRunListeners#running
void running(ConfigurableApplicationContext context) {
for (SpringApplicationRunListener listener : this.listeners) {
listener.running(context);
}
}
EventPublishingRunListener#running
@Override
public void running(ConfigurableApplicationContext context) {
context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context));
...
}
NacosContextRefresher监听ApplicationReadyEvent事件
在spring-cloud-starter-alibaba-nacos-config的spring.factories文件中指定了nacos的自动配置类NacosConfigAutoConfiguration,在NacosConfigAutoConfiguration中通过@Bean注解配置了NacosContextRefresher。
NacosContextRefresher实现了ApplicationListener接口监听ApplicationReadyEvent事件。
NacosContextRefresher#onApplicationEvent(ApplicationReadyEvent)
NacosContextRefresher#onApplicationEvent(ApplicationReadyEvent)
public void onApplicationEvent(ApplicationReadyEvent event) {
if (this.ready.compareAndSet(false, true)) {
this.registerNacosListenersForApplications();
}
}
NacosContextRefresher#registerNacosListenersForApplications()
private void registerNacosListenersForApplications() {
if (isRefreshEnabled()) {
for (NacosPropertySource propertySource : NacosPropertySourceRepository
.getAll()) {
...
// 引用的配置文件对应的dataId
String dataId = propertySource.getDataId();
// 注册监听器
registerNacosListener(propertySource.getGroup(), dataId);
}
}
}
NacosContextRefresher#registerNacosListener(String, String)
private void registerNacosListener(final String groupKey, final String dataKey) {
...
try {
// 调用NacosConfigService添加监听器
configService.addListener(dataKey, groupKey, listener);
}
catch (...) {...}
}
沿着NacosContextRefresher的onApplicationEvent方法一路下来,就可以看到调用了NacosConfigService的addListener方法添加监听器。
NacosConfigService#addListener(String, String, Listener)
public void addListener(String dataId, String group, Listener listener) throws NacosException {
// 调用ClientWorker添加监听器
worker.addTenantListeners(dataId, group, Arrays.asList(listener));
}
NacosConfigService的构造方法会创建ClientWorker,NacosConfigService的addListener就是直接调用ClientWorker添加监听器。
ClientWorker#addTenantListeners(String, String, List<? extends Listener>)
public void addTenantListeners(String dataId, String group, List<? extends Listener> listeners)
throws NacosException {
...
// 往ClientWorker的Map中添加一个CacheData并返回
CacheData cache = addCacheDataIfAbsent(dataId, group, tenant);
synchronized (cache) {
for (Listener listener : listeners) {
// 往CacheData中添加指定监听器
cache.addListener(listener);
}
...
}
}
ClientWorker这样的的成员变量:
/**
* groupKey -> cacheData.
*/
private final AtomicReference<Map<String, CacheData>> cacheMap = new AtomicReference<Map<String, CacheData>>(
new HashMap<String, CacheData>());
groupKey就是根据dataId, group, tenant三元组按一定规则拼接而成的字符串,作为Map中该CacheData对应的key,每个dataId, group, tenant三元组对应就是一个配置文件,在Map中都有一个groupKey与CacheData的映射。
ClientWorker的addTenantListeners方法就是创建一个CacheData并放入这个cacheMap中,然后往这个CacheData中添加一个指定的监听器listener。
CacheData
CacheData的结构我们已经分析过,包含了配置文件的dataId、group、tenant(也就是namespace)、配置文件内容content,配置文件MD5,,监听器列表listeners等成员属性。
public class CacheData {
...
public final String dataId;
public final String group;
public final String tenant;
private final CopyOnWriteArrayList<ManagerListenerWrap> listeners;
private volatile String md5;
...
private volatile String content;
...
}
ClientWorker定时任务
public NacosConfigService(Properties properties) throws NacosException {
...
// 创建ClientWorker
this.worker = new ClientWorker(this.configFilterChainManager, serverListManager, properties);
...
}
public ClientWorker(final ConfigFilterChainManager configFilterChainManager, ServerListManager serverListManager,
final Properties properties) throws NacosException {
...
// 创建ConfigRpcTransportClient
agent = new ConfigRpcTransportClient(properties, serverListManager);
...
// 设置定时任务线程池ScheduledExecutorService
agent.setExecutor(executorService);
// 启动定时任务
agent.start();
}
在NacosConfigService的构造方法会创建ClientWorker,ClientWorker的构造方法会创建一个ConfigRpcTransportClient,给这个ConfigRpcTransportClient设置一个定时任务线程池ScheduledExecutorService,然后调用这个ConfigRpcTransportClient的start()方法启动定时任务。
ConfigTransportClient#start()
ConfigTransportClient#start()
public void start() throws NacosException {
...
startInternal();
}
ClientWorker.ConfigRpcTransportClient#startInternal()
public void startInternal() {
executor.schedule(() -> {
while (!executor.isShutdown() && !executor.isTerminated()) {
try {
// 从listenExecutebell这个队列中获取一个元素,阻塞等待5秒,如果没有则返回
// 客户端会在接收到服务端通知时添加一个元素到这个队列中
listenExecutebell.poll(5L, TimeUnit.SECONDS);
...
// 5秒后还是没有从队列中获取到元素,也往下执行
executeConfigListen();
} catch (...) {...}
}
}, 0L, TimeUnit.MILLISECONDS);
}
ConfigTransportClient的startInternal方法调用上面设置的ScheduledExecutorService执行定时任务,定时任务是一个while循环,每一轮循环先从listenExecutebell这个队列中poll一个元素,这个listenExecutebell队列其实会在客户端收到服务端发送的配置变更通知时,往里面offer一个元素,也就是说如果listenExecutebell.poll()在5秒内返回了,说明收到了服务端发送的配置变更通知,如果5秒后还是没有返回,那么也继续往下执行,调用executeConfigListen()方法。
ClientWorker.ConfigRpcTransportClient#executeConfigListen()
public void executeConfigListen() {
...
try {
...
// 请求nacos服务端获取发生配置变更的配置文件信息
ConfigChangeBatchListenResponse configChangeBatchListenResponse = (ConfigChangeBatchListenResponse) requestProxy(rpcClient, configChangeListenRequest);
if (configChangeBatchListenResponse.isSuccess()) {
...
if (!CollectionUtils.isEmpty(configChangeBatchListenResponse.getChangedConfigs())) {
...
// 遍历每个发生变更的配置
for (ConfigChangeBatchListenResponse.ConfigContext changeConfig : configChangeBatchListenResponse
.getChangedConfigs()) {
...
// 刷新配置
refreshContentAndCheck(changeKey, !isInitializing);
}
}
...
}
ConfigTransportClient的executeConfigListen方法会请求服务端获取发生配置变更的配置文件信息,然后遍历每个发生变更的配置,调用refreshContentAndCheck方法刷新配置。
然后进入到ClientWorker的refreshContentAndCheck方法。
ClientWorker#refreshContentAndCheck(java.lang.String, boolean)
private void refreshContentAndCheck(String groupKey, boolean notify) {
// 根据groupKey拿到CacheData
// 这个groupKey也是根据dataId, group, tenant三元组拼接而成的,与放入cacheMap时的拼接规则一样
CacheData cache = cacheMap.get().get(groupKey);
if (cache != null) {
refreshContentAndCheck(cache, notify);
}
}
ClientWorker的refreshContentAndCheck方法从ClientWorker的cacheMap中根据groupKey获取对应的CacheData。这个groupKey也是根据dataId, group, tenant三元组拼接而成的,与放入cacheMap时的拼接规则是一样的。然后调用refreshContentAndCheck(cache, notify),这个cache参数就是CacheData。
ClientWorker#refreshContentAndCheck(CacheData, boolean)
private void refreshContentAndCheck(CacheData cacheData, boolean notify) {
try {
// 发起RPC远程调用,获取变更后的配置文件内容
ConfigResponse response = getServerConfig(cacheData.dataId, cacheData.group, cacheData.tenant, 3000L,
notify);
...
// 变更后的配置文件内容更新到cacheData,同时会更新cacheData中的md5值
cacheData.setContent(response.getContent());
...
// 触发cacheData中的监听器
cacheData.checkListenerMd5();
} catch (...) {...}
}
ClientWorker的refreshContentAndCheck方法首先发起RPC远程调用,获取变更后的配置文件内容,然后把变更后的配置文件内容更新到cacheData,最后调用cacheData.checkListenerMd5()触发cacheData中的监听器。
CacheData#checkListenerMd5()
void checkListenerMd5() {
// 遍历CacheData中的每一个监听器
for (ManagerListenerWrap wrap : listeners) {
// 比对CacheData中新的md5值和监听器中保存的旧md5值
if (!md5.equals(wrap.lastCallMd5)) {
// 比对不相等,执行safeNotifyListener方法
safeNotifyListener(dataId, group, content, type, md5, encryptedDataKey, wrap);
}
}
}
checkListenerMd5方法会遍历CacheData中的每一个监听器,比对CacheData中新的md5值和监听器中保存的旧md5值,如果不相等,执行safeNotifyListener方法。
CacheData#safeNotifyListener(…)
private void safeNotifyListener(..., final ManagerListenerWrap listenerWrap) {
final Listener listener = listenerWrap.listener;
...
// 调用监听器的receiveConfigInfo方法
listener.receiveConfigInfo(contentTmp);
...
// 更新监听器的md5值
listenerWrap.lastCallMd5 = md5;
...
};
AbstractSharedListener#receiveConfigInfo(String)
listener.receiveConfigInfo(contentTmp)里面会调用到innerReceive方法,就进入了在NacosContextRefresher的registerNacosListener方法创建的监听器实现的innerReceive方法。
com.alibaba.cloud.nacos.refresh.NacosContextRefresher#registerNacosListener(String, String)
private void registerNacosListener(final String groupKey, final String dataKey) {
...
Listener listener = listenerMap.computeIfAbsent(key,
lst -> new AbstractSharedListener() {
@Override
public void innerReceive(String dataId, String group,
String configInfo) {
...
// 发布一个RefreshEvent事件
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
...
}
});
...
}
这里的监听器的innerReceive方法被触发,发布一个RefreshEvent事件。
发布的RefreshEvent事件会触发Spring的事件监听机制,被spring-cloud-context包提供的RefreshEventListener监听器接收并处理,后续就是spring-cloud-context里面关于配置刷新的公共逻辑,在下一篇文章中分析。