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-id: system-chat-message.txt
          group: DEFAULT_GROUP
          timeout-ms: 20000
@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 中的 data-id 会自动对应 Java 的 dataId(kebab-case 转 camelCase)。

热更新实现

@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,简单高效。当配置内容不是标准的属性格式(如完整的文本文件),或者需要在配置变更时执行额外的处理逻辑时,再考虑手动监听的方式。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐