从源码的角度解读 Nacos 是如何动态刷新配置的

一 Nacos 刷新配置的源码阅读

在 ClientWorker 中配置了 定义了一个 的内部类 LongPollingRunnable 并实现了Runnable 接口 直接到 cacheData.checkListenerMd5() 这个方法

java 复制代码
public void run() {
             // 获取定义的Group
            List<CacheData> cacheDatas = new ArrayList<CacheData>();
            List<String> inInitializingCacheList = new ArrayList<String>();
            try {
                // check failover config
                for (CacheData cacheData : cacheMap.values()) {
                    if (cacheData.getTaskId() == taskId) {
                        cacheDatas.add(cacheData);
                        try {
                            checkLocalConfig(cacheData);
                            if (cacheData.isUseLocalConfigInfo()) {
                                cacheData.checkListenerMd5();
                            }
                        } catch (Exception e) {
                            LOGGER.error("get local config info error", e);
                        }
                    }
                }
            
                // check server config
                List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
                if (!CollectionUtils.isEmpty(changedGroupKeys)) {
                    LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
                }
            
                for (String groupKey : changedGroupKeys) {
                    String[] key = GroupKey.parseKey(groupKey);
                    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]);
                        }
                        LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}",
                                agent.getName(), dataId, group, tenant, cache.getMd5(),
                                ContentUtils.truncateContent(ct[0]), ct[1]);
                    } catch (NacosException ioe) {
                        String message = String
                                .format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
                                        agent.getName(), dataId, group, tenant);
                        LOGGER.error(message, ioe);
                    }
                }
                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);
            
            } catch (Throwable e) {
            
                // If the rotation training task is abnormal, the next execution time of the task will be punished
                LOGGER.error("longPolling error : ", e);
                executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
            }
        }
    }

检查当前的md5值是否更改

java 复制代码
void checkListenerMd5() {
        for (ManagerListenerWrap wrap : listeners) {
            if (!md5.equals(wrap.lastCallMd5)) {
              // 如果md5值变了,就发送对应事件通知
                safeNotifyListener(dataId, group, content, type, md5, wrap);
            }
        }
    }

安全的通知监听器配置改变:

java 复制代码
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);
                    LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);
                }

                // 设置线程上下文类加载器为应用加载器(避免多应用部署时,SPI等加载错类)
                Thread.currentThread().setContextClassLoader(appClassLoader);

                // 构造配置响应对象
                ConfigResponse cr = new ConfigResponse();
                cr.setDataId(dataId);
                cr.setGroup(group);
                cr.setContent(content);

                // 通过过滤链处理配置(比如解密、转换等)
                configFilterChainManager.doFilter(null, cr);

                // 获取处理后的配置内容
                String contentTmp = cr.getContent();

                // 调用监听器的 receiveConfigInfo 方法通知变更
                listener.receiveConfigInfo(contentTmp);

                // 如果是支持配置变更事件的监听器,触发对应事件
                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;
                }

                // 更新上一次调用的 MD5 值
                listenerWrap.lastCallMd5 = md5;

                // 打印通知成功日志
                LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,
                        listener);
            } catch (NacosException ex) {
                // 特定 Nacos 异常处理
                LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}",
                        name, dataId, group, md5, listener, ex.getErrCode(), ex.getErrMsg());
            } catch (Throwable t) {
                // 捕获所有其他异常,避免通知失败影响主线程
                LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} tx={}", name, dataId,
                        group, md5, listener, t.getCause());
            } finally {
                // 恢复原始线程类加载器,避免线程池复用带来问题
                Thread.currentThread().setContextClassLoader(myClassLoader);
            }
        }
    };

    // 记录通知开始时间
    final long startNotify = System.currentTimeMillis();
    try {
        // 如果监听器提供了自定义线程池,则用线程池异步执行
        if (null != listener.getExecutor()) {
            listener.getExecutor().execute(job);
        } else {
            // 否则直接当前线程执行
            job.run();
        }
    } catch (Throwable t) {
        // 执行过程出错日志打印
        LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} throwable={}", name, dataId,
                group, md5, listener, t.getCause());
    }

    // 记录通知完成时间
    final long finishNotify = System.currentTimeMillis();
    LOGGER.info("[{}] [notify-listener] time cost={}ms in ClientWorker, dataId={}, group={}, md5={}, listener={} ",
            name, (finishNotify - startNotify), dataId, group, md5, listener);
}

NacosContextRefresher 中 registerNacosListenersForApplications的方法

java 复制代码
/**
 * 为指定的 dataId + group 注册一个 Nacos 配置监听器
 * @param groupKey 配置分组(group)
 * @param dataKey 配置标识(dataId)
 */
private void registerNacosListener(final String groupKey, final String dataKey) {
    // 生成一个唯一 key,用于标识监听器(key = group + "++" + dataId)
    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) {
                // 刷新次数 +1(用于监控/统计)
                refreshCountIncrement();

                // 记录刷新历史
                nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);

                // 发布 Spring 的 RefreshEvent,通知上下文环境配置已变更
                // 注意:这里是全量刷新,
                applicationContext.publishEvent(
                    new RefreshEvent(this, null, "Refresh Nacos config"));

                // 如果开启了 debug 日志,打印变更信息
                if (log.isDebugEnabled()) {
                    log.debug(String.format(
                        "Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
                        group, dataId, configInfo));
                }
            }
        });

    try {
        // 调用 Nacos 客户端 API,注册监听器
        configService.addListener(dataKey, groupKey, listener);
    }
    catch (NacosException e) {
        // 注册失败,记录警告日志
        log.warn(String.format(
            "register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
            groupKey), e);
    }
}

在SpringClould 中的 RefreshEventListener

java 复制代码
public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationReadyEvent) {
			handle((ApplicationReadyEvent) event);
		}
		else if (event instanceof RefreshEvent) {
			handle((RefreshEvent) event);
		}
	}
java 复制代码
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);
	}
}
复制代码

ContextRefresher 中的 refresh 方法刷新所有作用域为 refresh 的bean

java 复制代码
public synchronized Set<String> refresh() {
		Set<String> keys = refreshEnvironment();
        // 刷新所有的
		this.scope.refreshAll();
		return keys;
	}

二 @refreshScope注解

定义

@RefreshScope 是 Spring Cloud 提供的注解,主要用于 ​支持配置的动态刷新 ​,特别是在结合像 Nacos、Consul、Spring Cloud Config 等配置中心时使用。@RefreshScope 使得标注的 Bean 在配置变更并发布刷新事件时,能够被重新实例化,从而实现"​配置热更新​"。

使用背景

Spring Boot 默认的 Bean 是单例的(@Singleton),一旦初始化完成,其属性就不会再变化。如果你想在运行时通过配置中心动态刷新某个 Bean 中的属性,就必须加上 @RefreshScope

与Nacos配合使用demo

1、依赖引入

确保你引入了以下依赖(以 Spring Boot 2.x / Spring Cloud Alibaba 为例):

xml 复制代码
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

2、application.yml 配置

yml 复制代码
server:
  port: 8080

spring:
  application:
    name: nacos-refresh-demo
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
        group: DEFAULT_GROUP
        namespace: public
        refresh-enabled: true

3、编写配置类(使用 @RefreshScope)

java 复制代码
package com.example.nacosdemo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;

@Data
@Component
@RefreshScope // 开启动态刷新
@ConfigurationProperties(prefix = "custom")
public class CustomConfig {
    private String name;
    private Integer age;
}

4、测试 Controller

java 复制代码
package com.example.nacosdemo.controller;

import com.example.nacosdemo.config.CustomConfig;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class TestController {

    private final CustomConfig customConfig;

    @GetMapping("/config")
    public String getConfig() {
        return "name: " + customConfig.getName() + ", age: " + customConfig.getAge();
    }
}

然后更改你的Nacos中的配置,查看是否被更新呢

总结

触发流程

  1. NacosConfigService 内部有 ClientWorker 线程定时轮询配置变化;
  2. 当检测到配置变更后,会回调配置监听器;
  3. NacosContextRefresher 是 Spring Cloud Alibaba 提供的监听器;
  4. 它触发 RefreshEvent 事件;
  5. Spring Cloud Context 的 RefreshScope 监听 RefreshEvent
  6. 清除旧 Bean 实例,下次注入重新构建。
相关推荐
太阳之神aboluo5 小时前
SpringCloud (4) 分布式事务
java·spring·spring cloud
林林code7 小时前
Springboot中的yml为单个的服务配置Feign不生效.md
spring cloud
麦兜*12 小时前
Spring Boot 与 Ollama 集成部署私有LLM服务 的完整避坑指南,涵盖 环境配置、模型管理、性能优化 和 安全加固
java·spring boot·后端·安全·spring cloud·性能优化
麦兜*13 小时前
国产大模型平替方案:Spring Boot通义千问API集成指南
java·spring boot·后端·python·spring cloud·系统架构·springboot
JavaArchJourney13 小时前
Spring Cloud Config 核心原理
spring cloud
程序员陆通1 天前
Spring Cloud微服务中的内存泄漏问题定位与解决方案
java·spring cloud·微服务
麦兜*1 天前
Spring Integration 整合 Web3.0网关:智能合约事件监听与Spring Integration方案
java·spring boot·后端·spring·spring cloud·web3·智能合约
天机️灵韵1 天前
开源医院信息管理系统:基于若依框架的智慧医疗解决方案
java·开发语言·spring boot·spring cloud·github·开源项目
JavaArchJourney1 天前
Spring Cloud Gateway 核心原理(含源码分析)
spring cloud