大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
Cloud Config 配置中心
这一课你将能做到
- 搭起一个 Config Server(本地文件模式和 Git 模式都会配),让 problem-service 启动时自动拉取远程配置
- 用
@RefreshScope实现「改配置不重启」:改一个限流开关,调 refresh 端点立刻生效 - 说清配置刷新的底层原理和
spring.config.import的加载顺序
为什么要学
三个微服务时每人一份 application.yml 还行;十个服务 × dev/test/prod 三环境就是 30 份文件。改一个 Redis 地址要改 10 份再逐个重启,漏一份就是线上事故。配置中心把配置从代码仓库剥离、集中到一处:多环境隔离、按服务命名、支持动态刷新。
Spring Cloud Config 是官方方案:配置存在 Git 仓库(天然有版本历史和审批流),Config Server 对外提供读取,各服务作为 Config Client 启动时拉取。国内 Nacos Config 也是同类方案(带 UI、推送更快),原理相通——学懂 Config,「配置如何集中、如何刷新」这套心智模型直接平移。
先建立图像
┌─────────────────────────────┐
│ Git 仓库 config-repo │
│ application.yml │ ← 所有服务共享的默认配置
│ problem-service.yml │ ← 服务专属配置
│ problem-service-dev.yml │ ← 服务 + 环境专属配置
└──────────────┬──────────────┘
│ Server 拉取(可缓存)
┌──────────────▼──────────────┐
│ Config Server :8888 │
└──────────────┬──────────────┘
│ Client 启动时 HTTP 拉取 /actuator/refresh 时重拉
┌───────────┼───────────┐
▼ ▼ ▼
user-svc problem-svc judge-svc
Client 端加载优先级(高 → 低):problem-service-dev.yml > problem-service.yml > application-dev.yml > application.yml > 本地 application.yml。远程配置默认覆盖本地同名项。
跟着做
步骤 1:搭建 Config Server
新建模块 config-server,pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>cloud-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>config-server</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
</dependencies>
</project>
启动类:
package com.example.config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
本地演示先用 native 模式(直接读本地目录,不依赖 Git),application.yml:
server:
port: 8888
spring:
application:
name: config-server
profiles:
active: native
cloud:
config:
server:
native:
search-locations: file:///D:/work/config-repo # Windows 路径;Linux 用 file:///opt/config-repo
步骤 2:准备配置仓库
新建目录 config-repo,放两个文件。
problem-service-dev.yml:
judge:
daily-limit: 20 # 每日提交上限
enable-new-engine: false # 判题引擎灰度开关
server:
port: 8082
application.yml(所有服务共享):
management:
endpoints:
web:
exposure:
include: health,refresh
启动 Config Server,验证读取:
curl http://localhost:8888/problem-service/dev
返回 JSON 里能看到 propertySources 按「服务+环境 → 服务 → 全局」顺序排列,后面的覆盖前面的。
切 Git 模式只需替换后端配置:
spring:
profiles:
active: git
cloud:
config:
server:
git:
uri: https://github.com/your-org/config-repo
default-label: main
# 私有仓库追加:
# username: ${GIT_USER}
# password: ${GIT_TOKEN}
生产用 Git 模式:每次改配置是一次 commit,有历史、可回滚、可审计。
步骤 3:Client 接入
problem-service 加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
problem-service 的 application.yml(Boot 3.x / Cloud 2023.x 用 spring.config.import,旧版 bootstrap.yml 已弃用):
spring:
application:
name: problem-service
profiles:
active: dev
config:
import: optional:configserver:http://localhost:8888
optional:表示 Config Server 挂了也能用本地配置启动(演示友好;生产可去掉 optional 强依赖)。- Client 会按
{应用名}-{profile}.yml的规则去 Server 拉取。
写一个读配置的接口验证:
package com.example.problem.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FeatureController {
@Value("${judge.daily-limit:0}")
private int dailyLimit;
@Value("${judge.enable-new-engine:false}")
private boolean newEngine;
@GetMapping("/features")
public String features() {
return "dailyLimit=" + dailyLimit + ", newEngine=" + newEngine;
}
}
访问 http://localhost:8082/features,能看到配置中心下发的值。
步骤 4:动态刷新(不重启)
给 Bean 加 @RefreshScope:
package com.example.problem.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@Component
@RefreshScope
@ConfigurationProperties(prefix = "judge")
public class JudgeProperties {
private int dailyLimit;
private boolean enableNewEngine;
public int getDailyLimit() { return dailyLimit; }
public void setDailyLimit(int dailyLimit) { this.dailyLimit = dailyLimit; }
public boolean isEnableNewEngine() { return enableNewEngine; }
public void setEnableNewEngine(boolean enableNewEngine) { this.enableNewEngine = enableNewEngine; }
}
修改 config-repo 里的 judge.daily-limit 为 50 并保存(Git 模式下是提交并推送),然后触发刷新:
curl -X POST http://localhost:8082/actuator/refresh
再访问 /features:值已变成 50,服务没有重启。注意 actuator 需要在依赖里(spring-boot-starter-actuator),且 exposure 包含 refresh。
讲清楚要点
- 两个角色:Config Server 读后端存储(Git/本地/ SVN)对外提供 HTTP 读取;Config Client 启动时拉取并合并进自己的 Environment。
- 命名规则决定拉什么:
{application}、{application}-{profile}、{application}/{label}(label 即 Git 分支);最常用的组合是problem-service-dev.yml。 - 刷新原理(面试高频):
POST /actuator/refresh→ Client 重新从 Server 拉取 Environment → 发布EnvironmentChangeEvent→@ConfigurationPropertiesBean 重新绑定 →@RefreshScope的 Bean 被销毁,下次使用时懒加载重建,于是拿到新值。 @RefreshScope是白名单机制:没打这个注解的 Bean(比如普通@Value注入的单例)刷新后仍是旧值——这是最容易踩的坑。- 启动期配置刷新不了:数据源连接、端口这类在启动阶段就固定的配置,改了必须重启;适合刷新的是业务开关、阈值、白名单等运行期参数。
- Spring Cloud Bus 是广播手段:单实例手动 curl 可以,几十个实例就要用 Bus——向 MQ 发一条刷新事件,所有订阅实例各自触发 refresh,配合 Git webhook 实现「一次提交全集群生效」。
你可能会懵的地方
-
为什么不直接用环境变量? 十二因子应用确实推荐环境变量,但它只适合少量启动期配置(镜像里不可变);配置中心的价值在「集中 + 多环境 + 动态刷新 + 版本审计」。两者不冲突:启动项用环境变量,业务开关进配置中心。
-
@Value 和 @ConfigurationProperties 哪个配合刷新更好?
@ConfigurationProperties+@RefreshScope。@ConfigurationProperties支持宽松绑定与校验,且 EnvironmentChangeEvent 本身就会触发它重新绑定;@Value必须连带 RefreshScope 才会更新。 -
Config Server 挂了服务还能启动吗? 用了
optional:configserver:前缀可以(回退本地配置);生产建议:Client 本地缓存上次拉到的配置(spring.cloud.config.fail-fast与重试配置),Server 多实例部署在网关后面。 -
配置里有密码怎么办? Config 支持对称/非对称加密:把
{cipher}密文存进 Git,Server 启动时配 encrypt key,返回给 Client 前自动解密。也可交给 Vault/Nacos 的加密能力。密码永远不要明文进 Git。 -
Nacos Config 和 Config 怎么选? Nacos:长轮询推送、秒级生效、带控制台、注册配置一体,国内新项目主流;Config:Git 审计流天然强、与官方栈零成本。选型看团队:要 UI 和推送选 Nacos,要 Git 流程选 Config,模型一致不用慌。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
Client 写死 http://localhost:8888 进镜像 | 环境迁移即失效 | configserver:${CONFIG_URL:...} 环境变量注入 |
| 改了配置只在 Server 侧改,Client 不动 | Client 只有启动/refresh 时才拉取 | 改完必须触发 /actuator/refresh 或走 Bus |
| 给数据源 Bean 加 @RefreshScope 期待热更新 | 连接池启动期已固定,刷新无效反而生乱 | 启动期配置接受重启;只热更业务开关 |
| 所有配置都堆 application.yml 共享文件 | 服务间互相覆盖,职责不清 | 共享放 application.yml,服务差异放 {服务名}-{profile}.yml |
| refresh 端点对公网开放 | 任何人可改运行时行为 | 网关不路由 actuator,或加 Spring Security 保护 |
卡点急救
- Client 启动报
Config data resource ... does not exist或直接失败:去掉了optional:但 Server 没起;或 Server 侧 search-locations 路径写错。先用curl http://localhost:8888/problem-service/dev验证 Server 是否能返回内容。 - refresh 后值没变:①Bean 没加
@RefreshScope;②actuator 没暴露 refresh 端点;③native 模式下文件没保存生效。三处逐一排查,/actuator/refresh的响应体会列出哪些 key 变了。 - 配置文件中文乱码:config-repo 里的 yml 必须是 UTF-8 编码;Windows 下用记事本编辑常被存成 GBK,换 VS Code 并确认右下角编码。
面试常问
-
配置动态刷新的原理?(必背) ①Client 收到
POST /actuator/refresh→ ②重新向 Config Server 拉取配置构造新 Environment → ③发布 EnvironmentChangeEvent,对比前后差异 → ④所有@ConfigurationPropertiesBean 重新绑定 → ⑤@RefreshScope标注的 Bean 从容器销毁,下次注入时用新 Environment 懒创建 → ⑥普通单例 Bean 不受影响(保持旧值)。追问 Bus 就补「通过 MQ 广播 RefreshRemoteApplicationEvent,避免逐台 curl」。 -
多个服务共享配置怎么设计? 三层:application.yml 放全局默认(日志格式、actuator 暴露)→ {service}.yml 放服务专属 → {service}-{profile}.yml 放环境差异。优先级由高到低加载,同名键高层覆盖低层。能讲出「为什么密码不放共享层」更好(最小可见性)。
-
bootstrap.yml 和 spring.config.import 的区别? Cloud 2020 前用 bootstrap.yml 在「父上下文」先加载 Config Server 地址再拉配置;2020+ 改为标准
spring.config.import=configserver:机制,把远程配置纳入 Spring Boot 统一的配置导入流程。新项目一律用 import,见到 bootstrap 意识到是老项目即可。
自测三问
- Client 端从配置中心拉取的优先级顺序是什么?
/actuator/refresh触发后容器里发生了哪几件事?- 为什么
@RefreshScope之外的 Bean 刷新后还是旧值?
小结一句话
配置中心把配置从代码剥离集中到 Git:Server 统一供读、Client 启动拉取、@RefreshScope 配合 refresh 端点实现不重启的热更新。
延伸
- Spring Cloud Config 官方文档
- 下一课:OpenFeign 服务调用——像调本地方法一样调别的服务。