概述
上一篇把 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 绑定结果,用于排查 |
官方文档
- Nacos 配置管理
- Spring Cloud Alibaba Nacos Config
- Spring Cloud Context: Refresh Scope
- Spring Boot: Type-safe Configuration Properties
总结
- 配置拉取发生在启动阶段,
@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,注解包路径以上表为准。