Nacos客户端动态监听配置源码解析

Nacos客户端动态监听配置源码

阅读源码,并不说要阅读每一行代码,不放过任何细节,我们的目的是了解其实现的原理以及掌握其实现过程中涉及到的一些重点知识。

2.1问题引入

我们知道,nacos是单独部署的,如果nacos的配置发生了变化,我们的应用程序是如何感知的呢?

我们所熟知的方式无非就下面两种:

  1. nacos主动推送信息到应用程序
  2. 应用程序主动去拉取配置信息

那我们看看nacos客户端是如何监听配置的变化。

2.2创建监听器

每次启动程序的时候,我们都会发现控制台会有如下的一个日志:

在这里插入图片描述

也就是说在程序启动完毕后,监听器就已经被注册并且能够开始工作,我们NacosContextRefresher从这个类中找到监听器,看看它是如何创建一个监听器的

public interface Listener {
    
    Executor getExecutor();
    
    void receiveConfigInfo(final String configInfo);
}

这是一个接口类,里面有两个方法

  1. getExecutor() 方法用于获取执行某个操作的线程池,返回值为 Executor 类型。
  2. 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 并发包中的一个线程安全的哈希表实现,提供了高效的并发读写操作。相比于传统的 HashMapConcurrentHashMap 在并发性能和线程安全性方面都有显著的优势。

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) {
        handle((ApplicationReadyEvent) event);
    }
    else if (event instanceof RefreshEvent) {
        //监听到RefreshEvent,就做后续处理
        handle((RefreshEvent) event);
    }
}

public void handle(ApplicationReadyEvent event) {
    this.ready.compareAndSet(false, true);
}

public void handle(RefreshEvent event) {
    if (this.ready.get()) { // don't handle events before app is ready
        log.debug("Event received " + event.getEventDesc());
        Set<String> keys = this.refresh.refresh();
        //控制台打印的就是这句话
        log.info("Refresh keys changed: " + keys);
    }
}

这里再次强调一下这个方法 onApplicationEvent

onApplicationEvent 用于监听和处理应用程序中发布的事件。它是 ApplicationListener 接口中定义的方法,当应用程序中发布的事件与该监听器所监听的事件类型匹配时,该方法会被调用

2.7监听器设计模式

Java 监听器是一种设计模式,它用于观察和响应对象的状态变化或事件发生。监听器通常由两个组件组成:一个事件源和一个或多个监听器。事件源负责发出事件,而监听器则负责响应事件,并执行相应的操作。

具体来说,Java 监听器的工作流程可以分为以下几个步骤:

  1. 定义事件源:首先,我们需要定义一个事件源,它负责发出事件 。上面的长轮询定时检查配置是否更新就是一个事件源
  2. 注册监听器: 接下来,我们需要注册一个或多个监听器,它们负责监听事件源,响应事件,并执行相应的操作 。 监听器通常是一个实现了监听器接口的对象 ,例如我们上面的Listener,然后我们把监听器注册到事件源中(配置服务)
  3. 发布事件: 当事件源发生事件时,它会通过事件对象将事件信息传递给所有注册的监听器,这些监听器会根据事件类型和事件源来判断是否应该响应该事件。如果监听器需要响应事件,则会执行相应的操作。 例如配置中心定时检查到配置已经更新了,那么就会通知监听器,发布一个事件
  4. 处理事件 : 最后,监听器会根据事件类型和事件源来处理事件 。例如上面说的把日志打印在控制台上也是其后续处理的一种方式。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Nacos可以通过两种方式实现动态刷新配置: 1. 推模式(Push Mode):Nacos Server会主动将配置推送给客户端客户端只需要订阅相应的配置即可。当Nacos Server上的配置发生变化时,Nacos Server会推送最新的配置客户端,从而实现动态刷新配置。 2. 拉模式(Pull Mode):客户端可以通过轮询的方式从Nacos Server获取配置,当Nacos Server上的配置发生变化时,客户端会重新拉取最新的配置,从而实现动态刷新配置。 以上两种方式都可以实现动态刷新配置,其中推模式比拉模式更实时、更高效,但也需要在客户端上增加相应的订阅代码来接收推送的配置。 ### 回答2: Nacos是一个开源的分布式配置中心,可以实现动态刷新配置。在Nacos中,配置信息以数据的形式存储在Nacos的数据库中,客户端可以在应用程序中订阅这些配置信息,并实时获取最新的配置Nacos提供了几种方式来实现动态刷新配置。首先,可以使用Nacos提供的API接口动态更新配置。通过调用API接口,可以实时修改配置的值,并将修改后的值保存到Nacos数据库中。客户端在订阅了该配置后,会收到通知并自动更新本地的配置。 其次,Nacos还提供了命令行工具,可以通过命令行来动态刷新配置。通过执行命令,可以实时修改配置的值,并将修改后的值保存到Nacos数据库中。客户端在订阅了该配置后,会收到通知并自动更新本地的配置。 此外,Nacos还支持通过Web界面来动态刷新配置。通过登录Nacos的Web界面,可以方便地对配置进行修改和保存,客户端在订阅了该配置后,会收到通知并自动更新本地的配置。 总结来说,Nacos可以通过API接口、命令行工具和Web界面来实现动态刷新配置。无论是使用哪种方式,都可以实时修改和保存配置,并让订阅了该配置客户端自动更新本地的配置。这种动态刷新配置的方式可以方便地对应用程序进行灵活的配置管理。 ### 回答3: Nacos是一个分布式配置管理系统,提供了动态刷新配置的功能。使用Nacos实现动态刷新配置的步骤如下: 1. 创建配置:首先,在Nacos控制台或通过API创建一个配置配置可以是键值对的形式,包含了应用程序需要的所有配置项。 2. 注册监听器:在应用程序代码中,注册一个监听器来监听配置变化。这可以通过使用Nacos提供的Java SDK或其他适配器来实现。 3. 获取配置:应用程序启动时,通过调用Nacos的API或使用Java SDK来获取初始的配置。这个初始的配置将会被应用程序使用。 4. 监听配置变化:应用程序中注册的监听器将会收到配置的更新通知。 5. 更新配置:当Nacos中的配置发生变化时,监听器将收到通知。应用程序可以在接收到通知后,根据新的配置进行相应的处理。 6. 动态刷新配置:根据应用程序的需要,可以选择在特定的时间间隔内或在配置发生变化时手动刷新配置。通过调用Nacos提供的API,应用程序可以更新当前的配置。 总之,通过使用Nacos提供的监听器和API,应用程序可以实现动态刷新配置。当Nacos中的配置发生变化时,应用程序可以根据新的配置进行相应的处理,从而实现配置动态刷新。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值