Java框架 SpringCloud 快速入门: Nacos 配置热更新

概述

上一篇把 userservice-dev.yaml 交给了 Nacos 管理,服务启动时能拉到配置了。但改一次配置就要重启一次服务,在生产环境里重启等于一次服务中断。配置中心最值钱的能力之一就是"改完不重启就生效",这篇把它做出来,并且把两种实现方式的取舍讲清楚。

纲要

  • 问题定位:为什么改完 Nacos 配置,服务里读到的还是旧值
  • 方式一 :@RefreshScope + @Value,完整 Controller 代码 + 验证接口
  • 方式二 :@ConfigurationProperties,完整属性类 + 注入改造
  • 选型对比表:刷新机制 / 批量绑定 / 类型安全 / 松散绑定 / 推荐场景
  • 底层原理:Nacos 长轮询(long polling)如何把变更推回来,为什么延迟在秒级
  • @RefreshScope 的三个副作用 :代理对象、@PostConstruct 重跑、与 AOP / @Scheduled 叠加
  • 可复现验证流程:启动 → 看初始值 → 控制台改值并发布 → 不重启看新值
  • 刷新范围:全量刷新与局部控制的边界
  • 常见坑清单 :忘加注解、忘点发布、加错位置、static 字段

为什么改完配置不生效

先看配置是怎么进到应用里的。bootstrap.yaml 比 application.yml 先加载,靠它才知道 Nacos 地址,然后按

text 复制代码
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

拼出 Data ID 去拉配置,本例是 userservice-dev.yaml,拉回来后与本地 application.yml 合并,再启动容器。

关键点在"合并",合并发生在启动阶段 。@Value 注入是一次性的:Spring 在创建 Bean 时把 ${pattern.dateformat} 解析成字符串塞进字段,之后这个字段就再也没人碰了。Nacos 那边配置改了,本地这个字段不知道,也没人通知它。
#mermaid-svg-M0nggnDm6kJuHeRy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-M0nggnDm6kJuHeRy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-M0nggnDm6kJuHeRy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-M0nggnDm6kJuHeRy .error-icon{fill:#552222;}#mermaid-svg-M0nggnDm6kJuHeRy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-M0nggnDm6kJuHeRy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-M0nggnDm6kJuHeRy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-M0nggnDm6kJuHeRy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-M0nggnDm6kJuHeRy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-M0nggnDm6kJuHeRy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-M0nggnDm6kJuHeRy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-M0nggnDm6kJuHeRy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-M0nggnDm6kJuHeRy .marker.cross{stroke:#333333;}#mermaid-svg-M0nggnDm6kJuHeRy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-M0nggnDm6kJuHeRy p{margin:0;}#mermaid-svg-M0nggnDm6kJuHeRy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-M0nggnDm6kJuHeRy .cluster-label text{fill:#333;}#mermaid-svg-M0nggnDm6kJuHeRy .cluster-label span{color:#333;}#mermaid-svg-M0nggnDm6kJuHeRy .cluster-label span p{background-color:transparent;}#mermaid-svg-M0nggnDm6kJuHeRy .label text,#mermaid-svg-M0nggnDm6kJuHeRy span{fill:#333;color:#333;}#mermaid-svg-M0nggnDm6kJuHeRy .node rect,#mermaid-svg-M0nggnDm6kJuHeRy .node circle,#mermaid-svg-M0nggnDm6kJuHeRy .node ellipse,#mermaid-svg-M0nggnDm6kJuHeRy .node polygon,#mermaid-svg-M0nggnDm6kJuHeRy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-M0nggnDm6kJuHeRy .rough-node .label text,#mermaid-svg-M0nggnDm6kJuHeRy .node .label text,#mermaid-svg-M0nggnDm6kJuHeRy .image-shape .label,#mermaid-svg-M0nggnDm6kJuHeRy .icon-shape .label{text-anchor:middle;}#mermaid-svg-M0nggnDm6kJuHeRy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-M0nggnDm6kJuHeRy .rough-node .label,#mermaid-svg-M0nggnDm6kJuHeRy .node .label,#mermaid-svg-M0nggnDm6kJuHeRy .image-shape .label,#mermaid-svg-M0nggnDm6kJuHeRy .icon-shape .label{text-align:center;}#mermaid-svg-M0nggnDm6kJuHeRy .node.clickable{cursor:pointer;}#mermaid-svg-M0nggnDm6kJuHeRy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-M0nggnDm6kJuHeRy .arrowheadPath{fill:#333333;}#mermaid-svg-M0nggnDm6kJuHeRy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-M0nggnDm6kJuHeRy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-M0nggnDm6kJuHeRy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M0nggnDm6kJuHeRy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-M0nggnDm6kJuHeRy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M0nggnDm6kJuHeRy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-M0nggnDm6kJuHeRy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-M0nggnDm6kJuHeRy .cluster text{fill:#333;}#mermaid-svg-M0nggnDm6kJuHeRy .cluster span{color:#333;}#mermaid-svg-M0nggnDm6kJuHeRy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-M0nggnDm6kJuHeRy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-M0nggnDm6kJuHeRy rect.text{fill:none;stroke-width:0;}#mermaid-svg-M0nggnDm6kJuHeRy .icon-shape,#mermaid-svg-M0nggnDm6kJuHeRy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M0nggnDm6kJuHeRy .icon-shape p,#mermaid-svg-M0nggnDm6kJuHeRy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-M0nggnDm6kJuHeRy .icon-shape .label rect,#mermaid-svg-M0nggnDm6kJuHeRy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M0nggnDm6kJuHeRy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-M0nggnDm6kJuHeRy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-M0nggnDm6kJuHeRy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 没有监听,不通知
应用启动
加载 bootstrap.yaml
按 Data ID 拉取 Nacos 配置
与 application.yml 合并
创建 Bean,@Value 解析并注入
字段值固定
Nacos 配置变更

实测现象就是这样:控制台把 pattern.dateformat 从 yyyy-MM-dd 改成 yyyy年MM月dd日 并发布,浏览器刷新 /user/now,返回值照旧是旧格式。不是配置没发布,是应用根本没重新读。

方式一:@RefreshScope + @Value

思路很直接:既然 @Value 是启动时解析的,那就让这个 Bean 在配置变更时被重建一次 。@RefreshScope 干的就是这件事。

规则一句话:加在 @Value 所处的类上 ,不是加在调用方。改造 user-service 的 UserController:

java 复制代码
package cn.itcast.user.web;

import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope // 关键:属性刷新开关,缺了它 @Value 永远是启动时的旧值
public class UserController {

    @Autowired
    private UserService userService;

    @Value("${pattern.dateformat}")
    private String dateformat;

    /**
     * 验证接口:返回按当前 dateformat 格式化的当前时间。
     * 改了 Nacos 配置后不用重启,再调一次就能看到格式变化。
     */
    @GetMapping("now")
    public String now() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id,
                          @RequestHeader(value = "Truth", required = false) String truth) {
        return userService.queryById(id);
    }
}

启动两个实例(8081、8082)之后,在 Nacos 控制台把 dateformat 从 yyyy-MM-dd 改成 yyyy年MM月dd日,点发布,两个实例的 /user/now 都会在几秒内变成 2026年09月30日 这种输出,全程没有重启。

这里有个对照要记住:同一段代码去掉 @RefreshScope,其余一个字不改,配置就不会刷新。这是新手最常踩的一个,排查时先看注解在不在。

方式二:@ConfigurationProperties

@Value 只是属性注入的手段之一。Spring Boot 还提供了 @ConfigurationProperties,把一组同前缀的配置整体绑定到一个 Java 对象上。它的刷新行为比 @Value 好------不需要 @RefreshScope。

在 user-service 里新建属性类:

java 复制代码
package cn.itcast.user.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * pattern.* 前缀下的配置绑定类。
 * 靠"约定大于配置":prefix + 字段名拼出的键与 Nacos 里的配置键一致即可自动注入。
 * 注意 @Component 不能省,否则这个类不会被扫描注册成 Bean。
 */
@Data
@Component
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
    private String dateformat;
    private String envSharedValue;
    private String name;
}

然后把它注入 Controller,替换掉 @Value 字段:

java 复制代码
package cn.itcast.user.web;

import cn.itcast.user.config.PatternProperties;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Slf4j
@RestController
@RequestMapping("/user")
// 这里没有 @RefreshScope,依然能刷新
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private PatternProperties properties;

    /**
     * 排查用接口:直接把绑定结果整个吐出来,
     * 改配置后调它就能确认字段是否已更新。
     */
    @GetMapping("prop")
    public PatternProperties properties() {
        return properties;
    }

    @GetMapping("now")
    public String now() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));
    }

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id,
                          @RequestHeader(value = "Truth", required = false) String truth) {
        return userService.queryById(id);
    }
}

为什么它自己就能刷新

@Value 和 @ConfigurationProperties 的差别在容器眼里是两种东西。

@Value 生成的是一次性字符串,注入完就与配置源断了联系。@ConfigurationProperties 绑定出来的 Bean 会被 Spring Cloud 登记到 ConfigurationPropertiesRebinder 的管辖范围内。配置刷新事件一到,这个 rebinder 会找出所有以 @ConfigurationProperties 标注的 Bean,对它们重新做一次属性绑定------不需要销毁重建 Bean,也不依赖 @RefreshScope 的代理机制。
#mermaid-svg-NwYh2Hc3x7SEKWXQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NwYh2Hc3x7SEKWXQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .error-icon{fill:#552222;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .marker.cross{stroke:#333333;}#mermaid-svg-NwYh2Hc3x7SEKWXQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NwYh2Hc3x7SEKWXQ p{margin:0;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .cluster-label text{fill:#333;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .cluster-label span{color:#333;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .cluster-label span p{background-color:transparent;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .label text,#mermaid-svg-NwYh2Hc3x7SEKWXQ span{fill:#333;color:#333;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .node rect,#mermaid-svg-NwYh2Hc3x7SEKWXQ .node circle,#mermaid-svg-NwYh2Hc3x7SEKWXQ .node ellipse,#mermaid-svg-NwYh2Hc3x7SEKWXQ .node polygon,#mermaid-svg-NwYh2Hc3x7SEKWXQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .rough-node .label text,#mermaid-svg-NwYh2Hc3x7SEKWXQ .node .label text,#mermaid-svg-NwYh2Hc3x7SEKWXQ .image-shape .label,#mermaid-svg-NwYh2Hc3x7SEKWXQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .rough-node .label,#mermaid-svg-NwYh2Hc3x7SEKWXQ .node .label,#mermaid-svg-NwYh2Hc3x7SEKWXQ .image-shape .label,#mermaid-svg-NwYh2Hc3x7SEKWXQ .icon-shape .label{text-align:center;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .node.clickable{cursor:pointer;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .arrowheadPath{fill:#333333;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NwYh2Hc3x7SEKWXQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NwYh2Hc3x7SEKWXQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NwYh2Hc3x7SEKWXQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .cluster text{fill:#333;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .cluster span{color:#333;}#mermaid-svg-NwYh2Hc3x7SEKWXQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NwYh2Hc3x7SEKWXQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .icon-shape,#mermaid-svg-NwYh2Hc3x7SEKWXQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .icon-shape p,#mermaid-svg-NwYh2Hc3x7SEKWXQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .icon-shape .label rect,#mermaid-svg-NwYh2Hc3x7SEKWXQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NwYh2Hc3x7SEKWXQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NwYh2Hc3x7SEKWXQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NwYh2Hc3x7SEKWXQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Nacos 配置变更
客户端拉取到新配置
发布 RefreshEvent
RefreshScope.refreshAll 销毁并重建 @RefreshScope Bean
ConfigurationPropertiesRebinder 重新绑定 @ConfigurationProperties Bean
@Value 字段拿到新值
属性类字段拿到新值

两条路径独立工作,所以两种方式可以共存,也可以混用。

两种方式怎么选

维度 @RefreshScope + @Value @ConfigurationProperties
是否需额外注解 需要,且必须加在 @Value 所在类上 不需要,@Component + 注解本身即可
是否支持批量绑定 不支持,一个字段一条 @Value 支持,一个前缀下所有字段一次绑定
类型安全 弱,全是字符串,类型转换在运行期出错 强,能绑 int / boolean / List / Map / 嵌套对象,绑不上启动就报错
松散绑定 不支持,键名必须严格一致 支持,max-conn、maxConn、MAX_CONN 都能绑到 maxConn
刷新机制 配置变更时销毁并重建整个 Bean ConfigurationPropertiesRebinder 重新绑定字段
推荐场景 单个开关、单个值,临时验证 一组业务配置,字段多、有类型、要校验

结论很明确:一个字段的小场景用 @Value 加 @RefreshScope 就行;只要超过两三个字段、或者字段本身不是字符串,一律用 @ConfigurationProperties。

理由不只是"少写几行 @Value"。真正的原因是刷新代价:@RefreshScope 走的是销毁 + 重建整个 Bean 的路子,字段越多、Bean 里初始化逻辑越重,重建的副作用越大;而 @ConfigurationProperties 只做重新绑定,不动 Bean 的生命周期。另外 @Value 写错键名只会在启动时报一个难读的解析失败,@ConfigurationProperties 配合 @Validated 能在启动阶段就把非法配置挡下来。

@RefreshScope 的三个副作用

@RefreshScope 用起来简单,但它不是"无害的一个注解"。被它标注的 Bean 在容器里不再是原来的对象,而是一个作用域代理。理解这一点,后面三个坑就都串起来了。
#mermaid-svg-rx7R8adnwO5Hb2dc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rx7R8adnwO5Hb2dc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rx7R8adnwO5Hb2dc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rx7R8adnwO5Hb2dc .error-icon{fill:#552222;}#mermaid-svg-rx7R8adnwO5Hb2dc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rx7R8adnwO5Hb2dc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rx7R8adnwO5Hb2dc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rx7R8adnwO5Hb2dc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rx7R8adnwO5Hb2dc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rx7R8adnwO5Hb2dc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rx7R8adnwO5Hb2dc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rx7R8adnwO5Hb2dc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rx7R8adnwO5Hb2dc .marker.cross{stroke:#333333;}#mermaid-svg-rx7R8adnwO5Hb2dc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rx7R8adnwO5Hb2dc p{margin:0;}#mermaid-svg-rx7R8adnwO5Hb2dc g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-rx7R8adnwO5Hb2dc g.classGroup text .title{font-weight:bolder;}#mermaid-svg-rx7R8adnwO5Hb2dc .cluster-label text{fill:#333;}#mermaid-svg-rx7R8adnwO5Hb2dc .cluster-label span{color:#333;}#mermaid-svg-rx7R8adnwO5Hb2dc .cluster-label span p{background-color:transparent;}#mermaid-svg-rx7R8adnwO5Hb2dc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rx7R8adnwO5Hb2dc .cluster text{fill:#333;}#mermaid-svg-rx7R8adnwO5Hb2dc .cluster span{color:#333;}#mermaid-svg-rx7R8adnwO5Hb2dc .nodeLabel,#mermaid-svg-rx7R8adnwO5Hb2dc .edgeLabel{color:#131300;}#mermaid-svg-rx7R8adnwO5Hb2dc .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-rx7R8adnwO5Hb2dc .label text{fill:#131300;}#mermaid-svg-rx7R8adnwO5Hb2dc .labelBkg{background:#ECECFF;}#mermaid-svg-rx7R8adnwO5Hb2dc .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-rx7R8adnwO5Hb2dc .classTitle{font-weight:bolder;}#mermaid-svg-rx7R8adnwO5Hb2dc .node rect,#mermaid-svg-rx7R8adnwO5Hb2dc .node circle,#mermaid-svg-rx7R8adnwO5Hb2dc .node ellipse,#mermaid-svg-rx7R8adnwO5Hb2dc .node polygon,#mermaid-svg-rx7R8adnwO5Hb2dc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rx7R8adnwO5Hb2dc .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-rx7R8adnwO5Hb2dc g.clickable{cursor:pointer;}#mermaid-svg-rx7R8adnwO5Hb2dc g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-rx7R8adnwO5Hb2dc g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-rx7R8adnwO5Hb2dc .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-rx7R8adnwO5Hb2dc .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-rx7R8adnwO5Hb2dc .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-rx7R8adnwO5Hb2dc .dashed-line{stroke-dasharray:3;}#mermaid-svg-rx7R8adnwO5Hb2dc .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-rx7R8adnwO5Hb2dc #compositionStart,#mermaid-svg-rx7R8adnwO5Hb2dc .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rx7R8adnwO5Hb2dc #compositionEnd,#mermaid-svg-rx7R8adnwO5Hb2dc .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rx7R8adnwO5Hb2dc #dependencyStart,#mermaid-svg-rx7R8adnwO5Hb2dc .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rx7R8adnwO5Hb2dc #dependencyStart,#mermaid-svg-rx7R8adnwO5Hb2dc .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rx7R8adnwO5Hb2dc #extensionStart,#mermaid-svg-rx7R8adnwO5Hb2dc .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rx7R8adnwO5Hb2dc #extensionEnd,#mermaid-svg-rx7R8adnwO5Hb2dc .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rx7R8adnwO5Hb2dc #aggregationStart,#mermaid-svg-rx7R8adnwO5Hb2dc .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rx7R8adnwO5Hb2dc #aggregationEnd,#mermaid-svg-rx7R8adnwO5Hb2dc .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rx7R8adnwO5Hb2dc #lollipopStart,#mermaid-svg-rx7R8adnwO5Hb2dc .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rx7R8adnwO5Hb2dc #lollipopEnd,#mermaid-svg-rx7R8adnwO5Hb2dc .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rx7R8adnwO5Hb2dc .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-rx7R8adnwO5Hb2dc .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rx7R8adnwO5Hb2dc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rx7R8adnwO5Hb2dc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rx7R8adnwO5Hb2dc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 刷新时销毁并重建
所有方法调用先找作用域
<<interface-like 代理>>
UserController
RefreshScope
+get(String name, ObjectFactory factory)
+destroy()
真实UserController实例
-String dateformat
-UserService userService

一、依赖它的 @Autowired 注入语义变了。 别的组件注入进来的是代理,不是真实实例。用 controller.getClass() 或 toString() 打出来的类名带 $$EnhancerBySpringCGLIB$$ 一类后缀,调试时会让人一头雾水。如果哪里用 == 比较两个引用、或者把实例塞进 Map 当 key,刷新之后身份就对不上了。

二、@PostConstruct 会在每次刷新后重新执行。 这个最容易出事:如果 @PostConstruct 里做了建连接池、注册监听器、加载缓存、上报指标这类事,每次改配置就会重跑一遍,连接池泄漏、监听器重复注册都由此而来。有这类初始化逻辑的类,要么挪到别处,要么做幂等保护。

三、和 AOP、@Scheduled 叠加时行为不好预测。 代理对象上再叠一层事务或切面代理,方法拦截顺序会变得难以推理;@Scheduled 写在这个 Bean 上时,定时任务实例可能在刷新后处于不好预期的状态。遇到"配置刷新之后就出怪事"的问题,先把 @RefreshScope 的分布范围收一收再定位。

刷新是怎么触发的:Nacos 长轮询

配置改动能自动生效,靠的不是 Nacos 主动推,而是客户端的长轮询(long polling)。

客户端不是每秒无脑请求一次,那样在几百个服务实例的规模下会把 Nacos 打崩。它的做法是:发起一个 HTTP 请求并挂起,服务端不立刻返回,把这个请求按住,最长约 30 秒。这 30 秒内如果这个 Data ID 的配置被改了,服务端立刻返回,客户端拿到新配置;30 秒到了还没人改,服务端返回一个空响应,客户端马上再发起下一轮挂起请求。
配置存储 Nacos 服务端 Nacos 客户端 (user-service) 配置存储 Nacos 服务端 Nacos 客户端 (user-service) #mermaid-svg-hT0xy9Fymw4Hu8r6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hT0xy9Fymw4Hu8r6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .error-icon{fill:#552222;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .marker.cross{stroke:#333333;}#mermaid-svg-hT0xy9Fymw4Hu8r6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hT0xy9Fymw4Hu8r6 p{margin:0;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hT0xy9Fymw4Hu8r6 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-hT0xy9Fymw4Hu8r6 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-hT0xy9Fymw4Hu8r6 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .sequenceNumber{fill:white;}#mermaid-svg-hT0xy9Fymw4Hu8r6 #sequencenumber{fill:#333;}#mermaid-svg-hT0xy9Fymw4Hu8r6 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .messageText{fill:#333;stroke:none;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .labelText,#mermaid-svg-hT0xy9Fymw4Hu8r6 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .loopText,#mermaid-svg-hT0xy9Fymw4Hu8r6 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-hT0xy9Fymw4Hu8r6 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .noteText,#mermaid-svg-hT0xy9Fymw4Hu8r6 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .actorPopupMenu{position:absolute;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-hT0xy9Fymw4Hu8r6 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hT0xy9Fymw4Hu8r6 .actor-man circle,#mermaid-svg-hT0xy9Fymw4Hu8r6 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-hT0xy9Fymw4Hu8r6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求挂起,最多等待约 30 秒 @RefreshScope Bean 重建 / @ConfigurationProperties 重新绑定 长轮询请求(携带 Data ID 与当前 MD5) 管理员在控制台修改配置并发布 比对 MD5,发现与客户端上报的不一致 立即返回变更的 Data ID 拉取 userservice-dev.yaml 最新内容 返回新配置 发布 RefreshEvent 发起下一轮长轮询请求

这套机制的效果是:正常情况下延迟就是一两秒到几秒,因为变更发生时挂起的请求会被立即唤醒;最差情况是刚好在两次轮询之间,那就要等当前这轮 30 秒超时。所以实际感受是"点完发布,刷新一下页面基本就变了",偶尔慢一拍也正常,不是配置没生效。

顺带解释一个高频疑问:为什么必须点"发布"? 控制台的编辑框里改完,只要不点发布,服务端存储里的内容一个字节都没变,长轮询比对的 MD5 自然也不会变,客户端认为"没有变更"。草稿只在编辑框里,不出这个浏览器窗口。配置改了没生效,先确认发布按钮点没点、是不是点在了另一个环境的配置文件上。

工程结构

改造落在 user-service 模块里,两个文件,一新增一修改:

tree 复制代码
cloud-demo
├── user-service
│   └── src/main
│       ├── java/cn/itcast/user
│       │   ├── config
│       │   │   └── PatternProperties.java   # 新增:@ConfigurationProperties 属性类
│       │   ├── web
│       │   │   └── UserController.java      # 修改:注入属性类 / 或加 @RefreshScope
│       │   ├── pojo/User.java
│       │   ├── mapper/UserMapper.java
│       │   ├── service/UserService.java
│       │   └── UserApplication.java
│       └── resources
│           ├── bootstrap.yml                # Nacos 地址、服务名、profile、文件后缀
│           └── application.yml              # 本地配置与 datasource
├── order-service
├── feign-api
├── eureka-server
└── gateway

bootstrap.yml 的实际内容,就是上一篇配好的那份:

yaml 复制代码
spring:
  application:
    name: userservice # 服务名,参与拼接 Data ID
  profiles:
    active: dev # 环境,决定读 userservice-dev.yaml
  cloud:
    nacos:
      server-addr: localhost:8848 # Nacos 地址
      config:
        file-extension: yaml # 配置文件后缀

讲义里 server-addr 写的是 localhost:8848;工程终态为了容器化把地址改成了 nacos:8848,这是 Docker 网络里的服务名。本地跑还是按讲义写 localhost:8848,别混。

Nacos 上 userservice-dev.yaml 的内容大致是:

yaml 复制代码
pattern:
  dateformat: yyyy-MM-dd
  envSharedValue: 环境共享值

把 dateformat 改成 yyyy年MM月dd日 就是下面验证流程里要做的操作。

验证流程

按下面的顺序走一遍,改完配置不重启就能看到结果:

bash 复制代码
# 启动两个 userservice 实例,端口 8081 与 8082
java -jar user-service/target/app.jar --server.port=8081
java -jar user-service/target/app.jar --server.port=8082

# 看初始值(此时 Nacos 里是 yyyy-MM-dd)
curl http://localhost:8081/user/now
curl http://localhost:8082/user/now

初始输出,两个端口一致:

text 复制代码
2026-09-30
2026-09-30

接着到 Nacos 控制台 → 配置管理 → userservice-dev.yaml → 编辑,把 dateformat 改为 yyyy年MM月dd日 → 发布 → 确认。不要重启任何服务,直接再调:

bash 复制代码
# 不重启,等两三秒后再调
curl http://localhost:8081/user/now
curl http://localhost:8082/user/now

预期输出:

text 复制代码
2026年09月30日
2026年09月30日

如果用的是方式二,/user/prop 会直接返回绑定后的对象,字段值一眼可见:

text 复制代码
{"dateformat":"yyyy年MM月dd日","envSharedValue":"环境共享值","name":"本地环境local"}

顺带说一个容易看漏的地方:刷新过程里 IDE 控制台会刷出一批日志,内容是客户端发现远端配置与本地不一致、拉取并更新某个 Data ID。日志变多不是异常,那是长轮询在正常工作的证据;真出问题时反而看不到这些日志。刷新成功后,应用侧通常也会打印一条配置已刷新的提示。

刷新范围

Nacos 客户端默认是全量刷新 ------一次配置变更触发 RefreshEvent,容器里所有 @RefreshScope Bean 都会被销毁重建,不只是这次真正改动了的那个配置对应的 Bean。服务规模小的时候无感,Bean 多、初始化重的时候就有成本,而且重建带来的副作用是全局的。

想更精细地控制,可以自己监听 RefreshEvent,在监听器里判断本次变更的 Data ID 是不是自己关心的,再做局部处理,或者直接调用 ContextRefresher.refresh() 的替代逻辑。这个属于进阶用法,本篇不展开,知道"默认全量、有粒度需求时另说"就够了。

常见坑

现象 原因 处理
改了配置,服务里还是旧值 @Value 所在类漏了 @RefreshScope 补注解;或改用 @ConfigurationProperties
启动直接报找不到配置 / 读不到值 属性类漏了 @Component,或不在启动类扫描范围内 补 @Component,确认包路径在 @SpringBootApplication 之下
发布后毫无反应 只改了草稿没点发布,或发到了别的 namespace / group 确认已发布,确认 Data ID 与环境后缀一致
@RefreshScope 加了但没用 加在了注入方(调用方)而不是被注入的 Bean 上 挪到真正持有 @Value 字段的类上
多实例读到不同版本 各实例长轮询周期不同步 秒级延迟属正常,稍等再验证;对时序敏感的场景不要依赖瞬时一致
静态字段不跟着变 static 字段不走 Spring 依赖注入,@Value 与 @ConfigurationProperties 都不管它 改成实例字段,或用 setter 注入
刷新之后连接池/监听器重复 @RefreshScope Bean 的 @PostConstruct 重跑 初始化逻辑移出该 Bean,或做成幂等

API 速览

注解 / 接口 位置 作用
@RefreshScope org.springframework.cloud.context.config.annotation 标记 Bean 为可刷新,配置变更时销毁重建
@Value org.springframework.beans.factory.annotation 单值注入,需配合 @RefreshScope 才刷新
@ConfigurationProperties org.springframework.boot.context.properties 前缀批量绑定,自动刷新,无需 @RefreshScope
@Component org.springframework.stereotype 把属性类注册为 Bean
ConfigurationPropertiesRebinder org.springframework.cloud.context.properties 刷新事件触发后重新绑定 @ConfigurationProperties Bean
RefreshEvent org.springframework.cloud.context.refresh 配置刷新事件,可自定义监听做局部控制
GET /user/now 业务接口 返回按当前 dateformat 格式化的时间,用于验证
GET /user/prop 业务接口 返回 PatternProperties 绑定结果,用于排查

官方文档

总结

  • 配置拉取发生在启动阶段,@Value 注入是一次性的,这就是"改了不生效"的根因。
  • 方式一 @RefreshScope + @Value:注解加在持有 @Value 字段的类上;漏掉注解,刷新就完全不发生。
  • 方式二 @ConfigurationProperties:靠 ConfigurationPropertiesRebinder 重新绑定,不需要 @RefreshScope,字段多、有类型的场景优先选它。
  • @RefreshScope 让 Bean 变成作用域代理,代价是重新构建整 Bean,会带来注入语义变化、@PostConstruct 重跑、与 AOP / @Scheduled 叠加异常三类问题。
  • 生效延迟在秒级,背后是约 30 秒的超时长轮询机制,不是服务端主动推送。
  • 改完记得点"发布",草稿不生效;刷新默认是全量的,粒度需求高时另做处理。
  • 本项目的依赖口径是 Spring Boot 2.3.9.RELEASE、Spring Cloud Hoxton.SR10、Spring Cloud Alibaba 2.2.5.RELEASE,注解包路径以上表为准。
相关推荐
西索斯coding1 小时前
Kimi k2.7-code-highspeed 接入 Cline 教程:baseURL 路由变更 + model_id 正确写法
java·服务器·数据库·ai
用户094248568031 小时前
第29章:【OpenJDK中级篇综合实战】百万长连接网关的JVM稳定性工程
java·jvm
吠品2 小时前
HTTPS 真身:HTTP 套了层 TLS
java·服务器·前端
钝挫力PROGRAMER2 小时前
Java 日常开发常用 API 总结与示例
java
小鹿的周先生2 小时前
第16章-RAG
java·人工智能·ai编程
夜不会漫长2 小时前
C++:内存管理
java·开发语言·c++
vipxieliang2 小时前
ValidX 财务系统发票验证:发票号、税号、金额校验
java·spring boot
天空鸟_时光不老2 小时前
06-给AI流程加一道人工闸门
java·人工智能·spring boot·后端·spring·spring cloud·架构
夜之眷属2 小时前
JVM实战:服务器堆外内存去哪了(NMT实测)
java·服务器·jvm·后端·性能优化