Nacos客户端动态监听配置源码
阅读源码,并不说要阅读每一行代码,不放过任何细节,我们的目的是了解其实现的原理以及掌握其实现过程中涉及到的一些重点知识。
2.1问题引入
我们知道,nacos是单独部署的,如果nacos的配置发生了变化,我们的应用程序是如何感知的呢?
我们所熟知的方式无非就下面两种:
- nacos主动推送信息到应用程序
- 应用程序主动去拉取配置信息
那我们看看nacos客户端是如何监听配置的变化。
2.2创建监听器
每次启动程序的时候,我们都会发现控制台会有如下的一个日志:
也就是说在程序启动完毕后,监听器就已经被注册并且能够开始工作,我们NacosContextRefresher从这个类中找到监听器,看看它是如何创建一个监听器的
public interface Listener {
Executor getExecutor();
void receiveConfigInfo(final String configInfo);
}
这是一个接口类,里面有两个方法
getExecutor()
方法用于获取执行某个操作的线程池,返回值为Executor
类型。receiveConfigInfo()
方法用于接收配置信息,接收到配置信息后会执行相关操作,该方法的参数为一个String
类型的配置信息。
像这样的接口类,一般会有一个抽象类去继承它,我们看看这个抽象类
public abstract class AbstractSharedListener implements Listener {
private volatile String dataId;
private volatile String group;
public final void fillContext(String dataId, String group) {
this.dataId = dataId;
this.group = group;
}
@Override
public final void receiveConfigInfo(String configInfo) {
innerReceive(dataId, group, configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
public abstract void innerReceive(String dataId, String group, String configInfo);
}
这个抽象类简单重写了这两个方法,我们需要注意的是
public abstract void innerReceive(String dataId, String group, String configInfo);
这个抽象方法的实现,后面会讲到,至此一个监听器就这么完成创建。
简单来说就是创建了一个接口类,定义了两个方法。
2.3注册监听器
还是前面讲到的,我们的应用程序启动后就完成了监听器的注册,一般来说在类里面肯定会有这样的一个方法 onApplicationEvent ,我们从NacosContextRefresher找找看,不难发现会有下面的一段代码
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// many Spring context
if (this.ready.compareAndSet(false, true)) {
this.registerNacosListenersForApplications();
}
}
“onApplicationEvent” 是在 Spring 框架中使用的一种方法,用于监听和处理应用程序中发布的事件 。
我们看看这个ApplicationReadyEvent参数
ApplicationReadyEvent
是 Spring 框架中的一个事件,表示应用程序已准备好接收请求并正在运行。当 Spring 应用程序成功启动并且所有初始化过程都已完成时,会触发 ApplicationReadyEvent
事件。
我们再看看registerNacosListenersForApplications这个方法的实现
private void registerNacosListenersForApplications() {
if (isRefreshEnabled()) {
for (NacosPropertySource propertySource : NacosPropertySourceRepository
.getAll()) {
if (!propertySource.isRefreshable()) {
continue;
}
String dataId = propertySource.getDataId();
//这是整个的核心
registerNacosListener(propertySource.getGroup(), dataId);
//这段代码就是控制台打印的日志
log.info("listening config: dataId={}, group={}", dataId, propertySource.getGroup());
}
}
}
private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = listenerMap.computeIfAbsent(key,
lst -> new AbstractSharedListener() {
@Override
public void innerReceive(String dataId, String group,
String configInfo) {
//刷新次数
refreshCountIncrement();
//记录刷新历史
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
//发布刷新事件
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
if (log.isDebugEnabled()) {
log.debug(String.format(
"Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
group, dataId, configInfo));
}
}
});
try {
//向配置服务中添加监听器
configService.addListener(dataKey, groupKey, listener);
}
catch (NacosException e) {
log.warn(String.format(
"register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
groupKey), e);
}
}
我们在创建监听器的时候,抽象类当中有一个这样的innerReceive方法,它的实现就在这段代码。然后把一个完整的监听器添加到配置服务中。
2.4监听器保存到内存
前面说到向配置服务中添加监听器,我们来看看监听器到最后是保存到哪里。我们从这行代码进去
//向配置服务中添加监听器 configService.addListener(dataKey, groupKey, listener);
往下找,来的ClientWorker这个核心类,有这样的一段代码
//定义的一个成员变量 private final ConcurrentHashMap<String, CacheData> cacheMap = new ConcurrentHashMap<String, CacheData>(); public void addTenantListeners(String dataId, String group, List<? extends Listener> listeners) throws NacosException { group = blank2defaultGroup(group); String tenant = agent.getTenant(); CacheData cache = addCacheDataIfAbsent(dataId, group, tenant); for (Listener listener : listeners) { cache.addListener(listener); } } //这段代码就是将数据保存在内存中和从内存中获取数据 public CacheData addCacheDataIfAbsent(String dataId, String group, String tenant) throws NacosException { String key = GroupKey.getKeyTenant(dataId, group, tenant); CacheData cacheData = cacheMap.get(key); if (cacheData != null) { return cacheData; } cacheData = new CacheData(configFilterChainManager, agent.getName(), dataId, group, tenant); // multiple listeners on the same dataid+group and race condition CacheData lastCacheData = cacheMap.putIfAbsent(key, cacheData); if (lastCacheData == null) { //fix issue # 1317 if (enableRemoteSyncConfig) { ConfigResponse response = getServerConfig(dataId, group, tenant, 3000L); cacheData.setContent(response.getContent()); } int taskId = cacheMap.size() / (int) ParamUtil.getPerTaskConfigSize(); cacheData.setTaskId(taskId); lastCacheData = cacheData; } // reset so that server not hang this check lastCacheData.setInitializing(true); LOGGER.info("[{}] [subscribe] {}", agent.getName(), key); MetricsMonitor.getListenConfigCountMonitor().set(cacheMap.size()); return lastCacheData; }
这里最重要的一个知识点是ConcurrentHashMap
ConcurrentHashMap
是 Java 并发包中的一个线程安全的哈希表实现,提供了高效的并发读写操作。相比于传统的 HashMap
,ConcurrentHashMap
在并发性能和线程安全性方面都有显著的优势。
ConcurrentHashMap
的生命周期通常与应用程序的生命周期相同,即当应用程序终止时,ConcurrentHashMap
对象也会被销毁。在应用程序运行期间,ConcurrentHashMap
对象会一直存在于内存中,用于存储和处理数据。
简单的说程序启动了,这个东西会一直存在于内存,你修改它的数据,就是修改内存中的数据。
2.5通知监听器
通知监听器,简单来说就是调用监听器Listener的receiveConfigInfo,这个方法里面会发布一个刷新事件,就是前面注册监听器提到的
Listener listener = listenerMap.computeIfAbsent(key,
lst -> new AbstractSharedListener() {
@Override
public void innerReceive(String dataId, String group,
String configInfo) {
//刷新次数
refreshCountIncrement();
//记录刷新历史
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
//发布刷新事件
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
if (log.isDebugEnabled()) {
log.debug(String.format(
"Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
group, dataId, configInfo));
}
}
});
这里会发布一个RefreshEvent这样的刷新事件,后面程序会监听到这个事件,然后进行后续操作,后面会讲到这部分代码。
接下来重点讲一下它里面它的实现细节
NacosConfigService这个类前面也提到过,我们向配置服务中添加监听器就是调用这个类的方法,我们看看这个类的构造方法
public NacosConfigService(Properties properties) throws NacosException {
ValidatorUtils.checkInitParam(properties);
String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
if (StringUtils.isBlank(encodeTmp)) {
this.encode = Constants.ENCODE;
} else {
this.encode = encodeTmp.trim();
}
initNamespace(properties);
this.configFilterChainManager = new ConfigFilterChainManager(properties);
this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
this.agent.start();
this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
}
重点看ClientWorker这个类,看它的构造方法
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,
final Properties properties) {
this.agent = agent;
this.configFilterChainManager = configFilterChainManager;
// Initialize the timeout parameter
init(properties);
//定义一个调度线程池,只有一个线程还是守护线程
this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
t.setDaemon(true);
return t;
}
});
// 定义一个多个线程的调度线程池,线程个数和CPU 核心数有关,也是守护线程,是一个长轮询
this.executorService = Executors
.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
t.setDaemon(true);
return t;
}
});
// 定义一个定时的调度任务,第一次执行的时候延时1毫秒,后续10毫秒调度一次
this.executor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
//检查配置是否变更
checkConfigInfo();
} catch (Throwable e) {
LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
}
}
}, 1L, 10L, TimeUnit.MILLISECONDS);
}
scheduleWithFixedDelay是 Java 中用于创建定时任务的方法之一,它可以在一个固定延迟时间之后执行任务,并且在上一个任务执行完成之后再次延迟指定时间后执行下一个任务。
从这里我们就可以知道,nacos是通过定时任务,去检查更新信息的
我们看看checkConfigInfo这个方法
public void checkConfigInfo() {
// Dispatch tasks.
int listenerSize = cacheMap.size();
// Round up the longingTaskCount.
int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
if (longingTaskCount > currentLongingTaskCount) {
for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
// The task list is no order.So it maybe has issues when changing.
//利用构造函数创建好的线程池去执行任务
executorService.execute(new LongPollingRunnable(i));
}
currentLongingTaskCount = longingTaskCount;
}
}
这个长轮询任务LongPollingRunnable里面实现了从nacos获取配置(通过API去调用),跟本地的进行比较,如果有变化,就通知监听器,我们就看其里面的最后一步
void checkListenerMd5() {
for (ManagerListenerWrap wrap : listeners) {
if (!md5.equals(wrap.lastCallMd5)) {
//通知监听器
safeNotifyListener(dataId, group, content, type, md5, encryptedDataKey, wrap);
}
}
}
private void safeNotifyListener(final String dataId, final String group, final String content, final String type,
final String md5, final String encryptedDataKey, final ManagerListenerWrap listenerWrap) {
final Listener listener = listenerWrap.listener;
//省略代码
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setGroup(group);
cr.setContent(content);
cr.setEncryptedDataKey(encryptedDataKey);
configFilterChainManager.doFilter(null, cr);
String contentTmp = cr.getContent();
//通知监听器,并执行receiveConfigInfo方法,发布我们前面提到过的监听器
listener.receiveConfigInfo(contentTmp);
// 省略代码
}
2.6订阅刷新事件
前面讲到监听器发布了一个RefreshEvent这样的刷新事件,我们可以看看哪个地方响应了这个刷新事件。我们可以打开nacos,修改一下配置
然后我们会发现控制台会打印如下日志
根据这个信息,我们打开RefreshEventListener,发现下面的方法
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationReadyEvent) {
这里再次强调一下这个方法 onApplicationEvent
onApplicationEvent 用于监听和处理应用程序中发布的事件。它是 ApplicationListener 接口中定义的方法,当应用程序中发布的事件与该监听器所监听的事件类型匹配时,该方法会被调用。
2.7监听器设计模式
Java 监听器是一种设计模式,它用于观察和响应对象的状态变化或事件发生。监听器通常由两个组件组成:一个事件源和一个或多个监听器。事件源负责发出事件,而监听器则负责响应事件,并执行相应的操作。
具体来说,Java 监听器的工作流程可以分为以下几个步骤:
- 定义事件源:首先,我们需要定义一个事件源,它负责发出事件 。上面的长轮询定时检查配置是否更新就是一个事件源
- 注册监听器: 接下来,我们需要注册一个或多个监听器,它们负责监听事件源,响应事件,并执行相应的操作 。 监听器通常是一个实现了监听器接口的对象 ,例如我们上面的Listener,然后我们把监听器注册到事件源中(配置服务)
- 发布事件: 当事件源发生事件时,它会通过事件对象将事件信息传递给所有注册的监听器,这些监听器会根据事件类型和事件源来判断是否应该响应该事件。如果监听器需要响应事件,则会执行相应的操作。 例如配置中心定时检查到配置已经更新了,那么就会通知监听器,发布一个事件
- 处理事件 : 最后,监听器会根据事件类型和事件源来处理事件 。例如上面说的把日志打印在控制台上也是其后续处理的一种方式。