Spring Cloud 提供了 @RefreshScope 注解来自动刷新配置,但在某些场景下需要手动监听 Nacos 配置变更,比如从 Nacos 拉取一个完整的文本文件内容(如 AI 系统提示词),这种非标准 key-value 的配置用 @RefreshScope 不太好处理。本文介绍如何通过 Nacos 原生 API 手动实现配置热更新。
核心 API
Nacos 客户端提供了两个关键方法:
// 启动时主动拉取配置 String config = configService.getConfig(dataId, group, timeoutMs); // 注册监听器,配置变更时 Nacos 主动推送新内容 configService.addListener(dataId, group, new Listener() { public void receiveConfigInfo(String configInfo) { // configInfo 就是变更后的新配置内容 } });底层原理是 Nacos 客户端与服务端之间维持长轮询连接,服务端检测到配置变更时主动推送给客户端,触发 receiveConfigInfo 回调。
配置属性类
首先定义配置属性类,将 yml 中的配置映射为 Java 对象:
tj: ai: prompt: system: chat: >@Data @Configuration @ConfigurationProperties(prefix = "tj.ai.prompt") public class AIProperties { private System system; @Data public static class System { private Chat chat; @Data public static class Chat { private String dataId; private String group = "DEFAULT_GROUP"; private long timeoutMs = 20000L; } } }@ConfigurationProperties 将 yml 中 tj.ai.prompt 前缀下的配置按层级映射到嵌套的静态内部类字段上。yml 中的>@Slf4j @Getter @Configuration @RequiredArgsConstructor public class SystemPromptConfig { private final NacosConfigManager nacosConfigManager; private final AIProperties aiProperties; // 使用 AtomicReference 保证线程安全 private final AtomicReference<String> chatSystemMessage = new AtomicReference<>(); @PostConstruct public void init() { loadConfig(aiProperties.getSystem().getChat(), chatSystemMessage); } private void loadConfig(AIProperties.System.Chat chatConfig, AtomicReference<String> target) { try { var dataId = chatConfig.getDataId(); var group = chatConfig.getGroup(); var timeoutMs = chatConfig.getTimeoutMs(); // 第一步:启动时从 Nacos 拉取配置 var config = nacosConfigManager.getConfigService() .getConfig(dataId, group, timeoutMs); target.set(config); log.info("读取配置成功,内容为:{}", config); // 第二步:注册监听器,配置变更时自动更新 nacosConfigManager.getConfigService() .addListener(dataId, group, new Listener() { @Override public Executor getExecutor() { return null; } @Override public void receiveConfigInfo(String info) { target.set(info); log.info("配置更新成功,新内容为:{}", info); } }); } catch (Exception e) { log.error("加载配置失败", e); } } }
流程说明
整个热更新分为两个阶段:
启动阶段:@PostConstruct 标注的 init() 方法在 Spring 创建 Bean 后立即执行,调用 getConfig() 从 Nacos 拉取配置内容,存入 AtomicReference。
运行阶段:addListener() 注册了一个监听器。当在 Nacos 控制台修改配置并发布后,Nacos 服务端通过长轮询检测到变更,主动推送新内容给客户端,触发 receiveConfigInfo 回调,将内存中的值更新为新内容。全程无需重启服务。
为什么用 AtomicReference
Spring Boot 的 Web 服务基于 Tomcat 线程池,多个请求线程同时读取 chatSystemMessage,而 Nacos 的监听回调运行在独立的线程上,负责写入新值。这是典型的多线程并发读写同一个共享变量的场景。
AtomicReference 底层基于 CPU 的 CAS(Compare-And-Swap)原子指令,保证 get() 和 set() 操作是原子性的——要么读到旧值,要么读到新值,不会读到写了一半的中间状态,也不需要加锁。
// 线程A(Tomcat工作线程):读取提示词 String prompt = systemPromptConfig.getChatSystemMessage().get(); // 线程B(Nacos回调线程):更新提示词 chatSystemMessage.set(newContent);使用方式
在业务代码中注入 SystemPromptConfig,随时获取最新的配置内容:
@Autowired private SystemPromptConfig systemPromptConfig; public void callAI() { String systemMessage = systemPromptConfig.getChatSystemMessage().get(); // 使用 systemMessage 调用大模型... }在 Nacos 控制台修改 system-chat-message.txt 的内容并发布后,下次调用 get() 拿到的就是更新后的值,无需重启服务。
与 @RefreshScope 的对比
| @RefreshScope | 手动 addListener | |
|---|---|---|
| 适用场景 | yml 中的普通 key-value 配置 | 文本文件、需要额外处理的配置 |
| 实现复杂度 | 加一个注解即可 | 需要手动编写加载和监听逻辑 |
| 灵活性 | 低,自动映射 | 高,可自定义处理逻辑 |
| 线程安全 | Spring 自动处理 | 需自行保证(如 AtomicReference) |
普通配置项优先使用 @RefreshScope,简单高效。当配置内容不是标准的属性格式(如完整的文本文件),或者需要在配置变更时执行额外的处理逻辑时,再考虑手动监听的方式。