从源码的角度解读 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 实例,下次注入重新构建。
相关推荐
Rain的Java大神之路6 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
需要8267 天前
MySQL 索引 B+ 树与“查询为什么变慢了
java·spring boot·spring·spring cloud·tomcat
需要8267 天前
Arthas 一个命令排查线上问题
java·jvm·spring·spring cloud
苏渡苇8 天前
Spring Insight 里如何把 Span 画成瀑布时间线
后端·spring·spring cloud·springboot·监控·apm
叶总没有会8 天前
Nacos—注册中心
spring cloud·微服务
苏渡苇9 天前
Spring Insight 里如何把 Span 收成一条链路
spring boot·spring·spring cloud·系统监控·apm
叶总没有会9 天前
微服务--分布式基础
java·spring·spring cloud·微服务
孙启超10 天前
【AI开发之Rust】第 7 课:错误处理 —— panic、Result 与 `?`
人工智能·分布式·后端·爬虫·spring cloud·架构·rust
行百里er11 天前
Spring Insight 里如何把 Span 收成一条链路
spring boot·spring cloud·监控
砍材农夫11 天前
spring|spring web|拦截器、过滤器、aop
java·spring boot·spring·spring cloud