图解+源码讲解 Nacos 客户端动态监听配置机制

3,645 阅读10分钟

图解+源码讲解 Nacos 客户端动态监听配置机制

在人生中第一要紧的是发现自己。为了这个目的,各位时常需要孤独和深思 —— 南森 Nacos 源码分析系列相关文章

  1. 从零开始看 Nacos 源码环境搭建
  2. 图解+源码讲解 Nacos 客户端发起注册流程
  3. 图解+源码讲解 Nacos 服务端处理注册请求逻辑
  4. 图解+源码讲解 Nacos 客户端下线流程
  5. 图解+源码讲解 Nacos 服务端处理下线请求
  6. 图解+源码讲解 Nacos 客户端发起心跳请求
  7. 图解+源码讲解 Nacos 服务端处理心跳请求
  8. 图解+源码讲解 Nacos 服务端处理配置获取请求
  9. 图解+源码讲解 Nacos 客户端动态监听配置机制

NacosConfigAutoConfiguration

    我们看到这里面其实注入了一个 Nacos 配置刷新的关键 NacosContextRefresherBean

@Configuration
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", 
                       matchIfMissing = true)
public class NacosConfigAutoConfiguration {
    // Nacos 配置属性
    @Bean
    public NacosConfigProperties nacosConfigProperties(ApplicationContext context) {
        if (context.getParent() != null
            && BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
                context.getParent(), NacosConfigProperties.class).length > 0) {
            return BeanFactoryUtils.beanOfTypeIncludingAncestors(context.getParent(),
                                 NacosConfigProperties.class);
        }
        return new NacosConfigProperties();
    }
    // Nacos 配置刷新属性
    @Bean
    public NacosRefreshProperties nacosRefreshProperties() {
        return new NacosRefreshProperties();
    }
    // Nacos 刷新历史
    @Bean
    public NacosRefreshHistory nacosRefreshHistory() {
        return new NacosRefreshHistory();
    }
    // Nacos 配置管理
    @Bean
    public NacosConfigManager nacosConfigManager(
        NacosConfigProperties nacosConfigProperties) {
        return new NacosConfigManager(nacosConfigProperties);
    }
    // Nacos 配置刷新
    @Bean
    public NacosContextRefresher nacosContextRefresher(
        NacosConfigManager nacosConfigManager,
        NacosRefreshHistory nacosRefreshHistory) {
        return new NacosContextRefresher(nacosConfigManager, nacosRefreshHistory);
    }
}

NacosContextRefresher 配置中心刷新

public NacosContextRefresher(NacosConfigManager nacosConfigManager,
                             NacosRefreshHistory refreshHistory) {
    // 获取配置属性信息
    this.nacosConfigProperties = nacosConfigManager.getNacosConfigProperties();
    // 刷新历史
    this.nacosRefreshHistory = refreshHistory;
    // 获取配置服务
    this.configService = nacosConfigManager.getConfigService();
    // 是否开启刷新,是true
    this.isRefreshEnabled = this.nacosConfigProperties.isRefreshEnabled();
}

获取配置服务 getConfigService

    nacosConfigManager.getConfigService(),这行代码其实就是为了创建 NcaosConfigService 对象,我们看看你是怎么创建的,其实核心代码就是通过 NacosFactory 反射创建的 NcaosConfigService 对象,这个对象是一个核心对象后续会讲到的

public static ConfigService createConfigService(Properties properties) 
    throws NacosException {
    try {
        // 加载 NacosConfigService 类
        Class<?> driverImplClass = 
            Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
        // 获取构造器
        Constructor constructor = driverImplClass.getConstructor(Properties.class);
        // 创建实例
        ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
        return vendorImpl;
    } catch (Throwable e) {
        throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
    }
}

监听器

    NacosContextRefresher 实现了 ApplicationListener ,一看这就是一个监听器了,我们看看这个在监听器里面做了什么操作

@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
    // 这是一个 CAS 操作,只设置一次
    if (this.ready.compareAndSet(false, true)) {
        // 注册 Nacos 监听器对于应用
        this.registerNacosListenersForApplications();
    }
}

注册 Nacos 监听

/**
* register Nacos Listeners.
    注册Nacos监听器
*/
private void registerNacosListenersForApplications() {
    // 默认是 true
    if (isRefreshEnabled()) {
        // 遍历Nacos属性资源中心
        for (NacosPropertySource propertySource : NacosPropertySourceRepository
             .getAll()) {
            if (!propertySource.isRefreshable()) {
                continue;
            }
            // 获取资源ID 
            String dataId = propertySource.getDataId();
            // 通过组和 dataId 注册 Nacos 监听器
            registerNacosListener(propertySource.getGroup(), dataId);
        }
    }
}

private void registerNacosListener(final String groupKey, final String dataKey) {
    // 构建 Key 信息
    String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
    // 在 listenerMap中放入了 key 对应 AbstractSharedListener 响应的方法
    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"));
                  }
              });
    // 向配置服务中添加监听器
    configService.addListener(dataKey, groupKey, listener);
  
}

向配置服务中添加监听器

    此时调用的是 NacosConfigService 中的 addListener 方法,但是最终执行的还是 ClientWorker 中的 addTenantListeners 方法,后面会进行分析 ClientWorker 这个类的

@Override
public void addListener(String dataId, String group, Listener listener) 
        throws NacosException {
    // 这个 ClientWorker worker 也是一个核心类
    worker.addTenantListeners(dataId, group, Arrays.asList(listener));
}

构建 CacheData 信息

    此时向 ClientWorker 中的 CacheData 中添加数据,之后遍历监听器添加到 CacheData 中

创建 CacheData 对象
public CacheData(ConfigFilterChainManager configFilterChainManager, String name,
                 String dataId, String group,String tenant) {
    // dataId 不能为空
    if (null == dataId || null == group) {
        throw new IllegalArgumentException("dataId=" + dataId + ", group=" + group);
    }
    this.name = name;
    this.configFilterChainManager = configFilterChainManager;
    this.dataId = dataId; // 设置dataId
    this.group = group; // 设置组信息
    this.tenant = tenant; // 设置租户
    listeners = new CopyOnWriteArrayList<ManagerListenerWrap>(); // 装饰器集合
    this.isInitializing = true;
    // 加载缓存数据从本地磁盘
    this.content = loadCacheContentFromDiskLocal(name, dataId, group, tenant);
    // 计算本地缓存信息的MD5
    this.md5 = getMd5String(content);
}
向 CacheData 中添加数据
public void addTenantListeners(String dataId, String group,
                            List<? extends Listener> listeners)throws NacosException {
    // DefaultGroup
    group = null2defaultGroup(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 {
    // 获取Key信息
    String key = GroupKey.getKeyTenant(dataId, group, tenant);
    // 在缓存 Map 中获取缓存数据
    CacheData cacheData = cacheMap.get(key);
    // 如果不为空的情况下那么就返回,如果为空那么就创建一个 CacheData
    if (cacheData != null) {
        return cacheData;
    }
    // 创建一个 CacheData 
    cacheData = new CacheData(configFilterChainManager, agent.getName(), 
                              dataId, group, tenant);
    // 将创建好的 cacheData 放入缓存 Map 中
    CacheData lastCacheData = cacheMap.putIfAbsent(key, cacheData);
    // 如果缓存数据为空的话那么从配置中心拉取,不过此时不为空
    if (lastCacheData == null) {
        //fix issue # 1317
        if (enableRemoteSyncConfig) {
            String[] ct = getServerConfig(dataId, group, tenant, 3000L);
            cacheData.setContent(ct[0]);
        }
        // 计算任务ID
        int taskId = cacheMap.size() / (int) ParamUtil.getPerTaskConfigSize();
        // 设置任务ID
        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;
}

    到这里 CacheData 对象 和 cacheMap 集合已经构建完成了,后续会用到这个数据的

NacosConfigService 分析

    NacosConfigService这个类在创建的时候主要做了什么事情,这这里面创建了一个 ClientWorker对象,这个对象是一个核心的类,有关于配置的一些操作都是归功于 ClientWorker

public NacosConfigService(Properties properties) throws NacosException {
   ......
   this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
   this.agent.start();
   // 核心工作类
   this.worker = new ClientWorker(this.agent,this.configFilterChainManager, properties);
}

核心配置类 ClientWorker

    分析一下这个类都在做什么事情,都有哪些核心方法
image.png
    其实能看到里面有一个构造函数、添加缓存数据、添加监听器、检查配置中心相关方法、获取服务配置、解析数据响应、移除缓存数据、删除监听器以及 shutdown方法

构造函数

    看到这里其实看到了定义了两个调度线程池,一个是用于配置检测的,一个是用于执行长轮询服务

@SuppressWarnings("PMD.ThreadPoolCreationRule")
public ClientWorker(final HttpAgent agent, 
  final ConfigFilterChainManager configFilterChainManager, final Properties properties){
    this.agent = agent;
    this.configFilterChainManager = configFilterChainManager;
    // 初始化操作
    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);
}

检查配置服务方法

    这个 cacheMap 包含了一些任务信息,这里面的任务是怎么来的呢,他是在添加监听器的时候添加的,上面已经分析过了

// 检查配置信息
public void checkConfigInfo() {
    // 调度任务,这个 chcheMap 在 NacosContextRefresher 初始化的时候进行创建的
int listenerSize = cacheMap.size();
    // 计算任务统计
int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
 if (longingTaskCount > currentLongingTaskCount) {
     for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
           // 利用上面创建好的线程池进行任务执行
          executorService.execute(new LongPollingRunnable(i));
     }
     currentLongingTaskCount = longingTaskCount;
  }
}

长轮询任务 LongPollingRunnable

    看看这个长轮询任务里面都做了些什么,我们猜想因该是,对比配置信息进行处理

class LongPollingRunnable implements Runnable {
    private final int taskId;
    public LongPollingRunnable(int taskId) {
        this.taskId = taskId;
    }
    @Override
    public void run() {

        List<CacheData> cacheDatas = new ArrayList<CacheData>();
        List<String> inInitializingCacheList = new ArrayList<String>();
        try {
            // 本地配置检测
            for (CacheData cacheData : cacheMap.values()) {
                // 获取任务ID 信息
                if (cacheData.getTaskId() == taskId) {
                    cacheDatas.add(cacheData);
                    // 检测本地配置服务
                    checkLocalConfig(cacheData);
                    // 是否开启了使用本地配置信息服务
                    if (cacheData.isUseLocalConfigInfo()) {
                        // 检测Md5
                        cacheData.checkListenerMd5();
                    }
                }
            }

            // 检测服务端的配置,检测服务端中有变化的 dataId 信息
            List<String> changedGroupKeys = 
                checkUpdateDataIds(cacheDatas, inInitializingCacheList);
            // 如果有变化那么就进行后续处理
            if (!CollectionUtils.isEmpty(changedGroupKeys)) {
                LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
            }
            // 遍历服务端中有变化的 dataId 信息
            for (String groupKey : changedGroupKeys) {
                // 解析key信息
                String[] key = GroupKey.parseKey(groupKey);
                // 获取数据ID
                String dataId = key[0];
                // 获取组信息
                String group = key[1];
                String tenant = null;
                if (key.length == 3) {
                    tenant = key[2];
                }
                try {
                    // 获取配置中心服务端中的配置信息
                    String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                    // 设置到本地缓存中
                    CacheData cache = cacheMap.
                        get(GroupKey.getKeyTenant(dataId, group, tenant));
                    cache.setContent(ct[0]);
                    if (null != ct[1]) {
                        cache.setType(ct[1]);
                    }
                }
            }
            for (CacheData cacheData : cacheDatas) {
                if (!cacheData.isInitializing() || inInitializingCacheList
                    .contains(GroupKey.getKeyTenant(cacheData.dataId, 
                                cacheData.group, cacheData.tenant))) {
                    // 进行 MD5 检测校验
                    cacheData.checkListenerMd5();
                    cacheData.setInitializing(false);
                }
            }
            inInitializingCacheList.clear();
            executorService.execute(this);
        } 
    }

检测本地配置 checkLocalConfig

private void checkLocalConfig(CacheData cacheData) {
    final String dataId = cacheData.dataId;
    final String group = cacheData.group;
    final String tenant = cacheData.tenant;
    // 获取本地文件
    File path = LocalConfigInfoProcessor.
        getFailoverFile(agent.getName(), dataId, group, tenant);
    // 不使用本地的,但是本地文件存在,那就用本地的,设置为true
    if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
        // 获取内容
        String content = LocalConfigInfoProcessor.
            getFailover(agent.getName(), dataId, group, tenant);
        // 计算MD5
        final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
        cacheData.setUseLocalConfigInfo(true);
        // 设置版本
        cacheData.setLocalConfigInfoVersion(path.lastModified());
        cacheData.setContent(content);
        return;
    }
    // 使用本地的,但是本地不存在,那就设置成不用本地文件,去Nacos服务端获取配置
    if (cacheData.isUseLocalConfigInfo() && !path.exists()) {
        cacheData.setUseLocalConfigInfo(false);
        return;
    }
    // 使用本地的,本地也有,但是版本号不一样,说明有变更,那就把本地的更新到内存里
    if (cacheData.isUseLocalConfigInfo() && path.exists() 
        && cacheData.getLocalConfigInfoVersion() != path.lastModified()) {
        // 获取内容
        String content = LocalConfigInfoProcessor.
            getFailover(agent.getName(), dataId, group, tenant);
        // 计算MD5
        final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
        cacheData.setUseLocalConfigInfo(true);
        cacheData.setLocalConfigInfoVersion(path.lastModified());
         // 设置缓存信息
        cacheData.setContent(content);
    }
}

检测有变化的 DataId checkUpdateDataIds

List<String> checkUpdateDataIds(List<CacheData> cacheDatas, 
            List<String> inInitializingCacheList) throws Exception {
    ......
    boolean isInitializingCacheList = !inInitializingCacheList.isEmpty();
    // 检查更新配置操作
    return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
}
// 检查更新配置操作
List<String> checkUpdateConfigStr(String probeUpdateString, 
                                  boolean isInitializingCacheList) throws Exception {
    // 构建请求参数
    Map<String, String> params = new HashMap<String, String>(2);
    params.put(Constants.PROBE_MODIFY_REQUEST, probeUpdateString);
    Map<String, String> headers = new HashMap<String, String>(2);
    headers.put("Long-Pulling-Timeout", "" + timeout);
    ......
    if (StringUtils.isBlank(probeUpdateString)) {
        return Collections.emptyList();
    }
    long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
    // 向服务端获取有变化的配置信息
    // 请求路径是 /v1/cs/configs/listener
    HttpRestResult<String> result = agent
        .httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", 
                  headers, params, agent.getEncode(),readTimeoutMs);
    // 如果返回OK操作
    if (result.ok()) {
        // 设置服务状态为true
        setHealthServer(true);
        // 解析结果响应
        return parseUpdateDataIdResponse(result.getData());
    } 
    ........
    return Collections.emptyList();
}

校验文件 Md5 改变 checkListenerMd5

void checkListenerMd5() {
    for (ManagerListenerWrap wrap : listeners) {
        if (!md5.equals(wrap.lastCallMd5)) {
            // 安全通知监听
            safeNotifyListener(dataId, group, content, type, md5, wrap);
        }
    }
}

通知监听

private void safeNotifyListener(final String dataId, final String group, 
                            final String content, final String type,
                            final String md5, final ManagerListenerWrap listenerWrap) {
    final Listener listener = listenerWrap.listener;

    Runnable job = new Runnable() {
        @Override
        public void run() {
            ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
            ClassLoader appClassLoader = listener.getClass().getClassLoader();
            try {
                if (listener instanceof AbstractSharedListener) {
                    AbstractSharedListener adapter = (AbstractSharedListener) listener;
                    adapter.fillContext(dataId, group);
                }
                // 执行回调之前先将线程classloader设置为具体webapp的classloader,
                // 以免回调方法中调用spi接口是出现异常或错用(多应用部署才会有该问题)。
                Thread.currentThread().setContextClassLoader(appClassLoader);
                // 创建配置响应信息
                ConfigResponse cr = new ConfigResponse();
                // 设置数据ID                
                cr.setDataId(dataId);
                // 设置组信息
                cr.setGroup(group);
                // 设置内容
                cr.setContent(content);
                configFilterChainManager.doFilter(null, cr);
                // 获取返回的配置信息
                String contentTmp = cr.getContent();
                // 接受配置信息
                listener.receiveConfigInfo(contentTmp);
                // compare lastContent and content
                if (listener instanceof AbstractConfigChangeListener) {
                    Map data = ConfigChangeHandler.getInstance()
                        .parseChangeData(listenerWrap.lastContent, content, type);
                    // 创建配置改变事件
                    ConfigChangeEvent event = new ConfigChangeEvent(data);
                    // 接受配置改变
                   ((AbstractConfigChangeListener) listener).receiveConfigChange(event);
                   listenerWrap.lastContent = content;
                }
                listenerWrap.lastCallMd5 = md5;
            } 
        }
    };
    
    if (null != listener.getExecutor()) {
        listener.getExecutor().execute(job);
    } else {
        // 执行 job 任务
        job.run();
    }
}

监听器执行

@Override
public final void receiveConfigInfo(String configInfo) {
    innerReceive(dataId, group, configInfo);
}

    此时就会调用到我们之前在注册监听器的时候创建的方法
image.png

添加刷新记录
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
public void addRefreshRecord(String dataId, String group, String data) {
    // 在linkedList 中添加一个记录
    records.addFirst(new Record(DATE_FORMAT.get().format(new Date()), dataId, group,
                                md5(data), null));
    //如果当前记录数量已经大于了最大值了那么就删除最后一个记录
    if (records.size() > MAX_SIZE) {
        records.removeLast();
    }
}
发布一个刷新事件通知订阅者
applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config"));
订阅者进行配置刷新

image.png

@Override
public void onApplicationEvent(ApplicationEvent event) {
    ......
    // 如果是刷新事件那么进行处理
    if (event instanceof RefreshEvent) {
        handle((RefreshEvent) event);
    }
}

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);
    }
}

public synchronized Set<String> refresh() {
    // 进行环境的刷新
    Set<String> keys = refreshEnvironment();
    this.scope.refreshAll();
    return keys;
}

小结

  1. 创建 NacosContextRefresher 对象,里面进行了ConfigService 的获取,以及监听器的注册,接受配置改变的事件
  2. 通过反射机制加载 NacosConfigService
  3. 在创建 NacosConfigService时候进行了 ClientWorker的构造
  4. 后续的很多操作都是通过 ClientWorker进行执行的,比如访问配置,以及长轮询任务操作
  5. ClientWorker的构造的时候创建了了两个线程池,一个是进行 checkConfigInfo的操作,一个是用于进行长轮询任务调度的比对的
  6. 如果配置发生变化那么会通知之前注册好的监听器方法,进行历史配置记录,以及发送一个刷新事件
  7. 订阅这个事件的如果识别出来是一个配置刷新事件那么就进行环境和属性的刷新操作