2498 字
约 8 分钟
14
S06-X10 · 配置管理与多环境隔离

本机 localhost:3306 跑得好好的,打包上容器就连不上库;测试环境把 feature.enabled 打开过,生产环境忘了同步,同一份代码行为不同;改了一个 application.yml 里的密码,三个月后在 Git 历史里翻出来。这三件事都属于同一类问题:配置没有边界。

配置管理的核心不是"写多少 yml",而是三件事:谁覆盖谁(优先级)、什么该放外面(外置与敏感项)、改了之后谁生效(刷新边界)。


1. 配置来源的优先级

Spring Boot 3.x 的 Environment 是一串 PropertySource 叠加,后面的覆盖前面的。工程里常用的几层,由低到高:

优先级 来源 典型用途 覆盖关系
1(最低) 代码内默认值(@Value 默认、SpringApplication 默认属性) 兜底 被所有外部配置覆盖
2 application.yml / .properties(jar 内) 通用默认 被 profile 文件覆盖
3 application-{profile}.yml 环境差异 被环境变量覆盖
4 操作系统环境变量 容器、CI 注入 被命令行覆盖
5 Java 系统属性(-D) 运维临时改 同上
6(最高) 命令行参数(--key=value) 覆盖单次运行 最终生效

实践上记住两条:

  • 容器环境优先用环境变量:application-prod.yml 打进镜像后,改配置要重建镜像。
  • -D 与 -- 都覆盖文件配置,但 -- 只对当前进程有效,适合排障时临时抬高日志级别。

环境变量命名遵循松散绑定:spring.datasource.url → SPRING_DATASOURCE_URL。带连字符的例外:spring.datasource.hikari.maximum-pool-size 要写成 SPRING_DATASOURCE_HIKARI_MAXIMUMPOOLSIZE(连字符被去掉而非转下划线),这是"环境变量不生效"最常见的原因。


2. profile 的激活与组合

# application.yml
spring:
  profiles:
    active: dev
  config:
    activate:
      on-profile: dev
server:
  port: 8081
---
spring:
  config:
    activate:
      on-profile: prod
server:
  port: 8080
激活方式 写法 适用
配置文件 spring.profiles.active=dev 本地开发
命令行 java -jar app.jar --spring.profiles.active=prod 单次运行
环境变量 SPRING_PROFILES_ACTIVE=prod 容器、CI
组合 spring.profiles.group.prod=prod-db,prod-mq 一组配置一起开

注意 spring.profiles.active 写在 application-{profile}.yml 里无效,会抛 InvalidConfigDataPropertyException,必须放主文档、命令行或环境变量。多文档 --- 时每个文档都要写 spring.config.activate.on-profile;没标注的文档对所有 profile 生效。

@Profile 是 Bean 级开关,与配置文件级的 profile 是两套机制:前者决定 Bean 是否注册,后者决定哪份属性文件被加载。


3. 配置外置的三种形态

形态 做法 适用 代价
外部文件 --spring.config.location=/etc/app/ 或 SPRING_CONFIG_ADDITIONAL_LOCATION 单机部署、容器挂载 ConfigMap 要管文件分发与权限
环境变量 SPRING_* 或自定义前缀 容器、K8s、Serverless 变量多时难维护,敏感值会出现在 docker inspect
配置中心 Nacos / Apollo / Consul + spring.config.import 多实例、需要动态改 引入新依赖与可用性风险

Boot 3.x 引入了统一的 spring.config.import,把"从别处拉配置"变成配置本身的一部分:

spring:
  config:
    import:
      - optional:file:/etc/clarity/application.yml
      - optional:configserver:http://config:8888
      - vault://secret/clarity

optional: 表示"拉不到就跳过",不写则拉不到直接启动失败——生产宁可起不来,也不带错配置上线。同文件里 import 来的配置优先级高于文件自身,低于命令行。


4. 敏感配置怎么放

做法 效果 残留风险
写进 application.yml 提交 Git 最省事 密码进历史,永久可查
环境变量注入 不进仓库 docker inspect、/proc/<pid>/environ 可见
启动参数 不进仓库 ps aux 可见
密钥管理(Vault/KMS)+ spring.config.import 有审计、可轮换 需要网络与权限体系
加密属性(Jasypt 等) 仓库里是密文 主密钥仍要有地方放

底线:仓库里不出现真实凭据。已提交过的按"已泄露"处理——删文件没用,历史还在,必须轮换。

# 检查工作区是否混入凭据
$ git log -p --all -S 'password' -- '*.yml' '*.properties' | head -50
$ grep -rn "password\|secret\|token" src/main/resources/ | grep -v '\${'

允许出现的是占位符 password: ${DB_PASSWORD},且不写默认值:缺失即启动失败,比 ${DB_PASSWORD:root} 静默连上本地库好。


5. 刷新边界

动态刷新常被高估。@RefreshScope 的语义是该 Bean 被重新创建,只有被作用域代理包住的 Bean 才拿到新值。

对象 能否刷新 原因
@RefreshScope 标注的 Bean 能(重建实例) 作用域代理在下次访问时重建
普通单例 Bean 里的 @Value 字段 不能 值在创建时注入,之后不再写
@ConfigurationProperties Bean 能(重新绑定) 但需重新绑定触发,且旧值可能已被缓存
数据源、连接池 风险高 重建会中断现有连接,需评估连接泄漏
静态字段 / 常量 不能 编译期内联,无运行期入口
@RefreshScope
@Component
public class FeatureGateway {
    @Value("${feature.gray.ratio:0}") private int ratio;   // 重建后拿到新值
}

只把"读多写少、重建无副作用"的开关类 Bean 放进 @RefreshScope;连接池改配置走滚动重启。@Value 写在 @RefreshScope 之外的类里不刷新——这个组合错误几乎每个项目犯一次。


6. 多环境一致性与配置校验

漂移类型 表现 治理
同 key 不同值 测试环境 timeout=30,生产 timeout=3 差异项集中在一个文件,CI 做 diff
缺 key 生产少了 mq.topic,启动才报 @ConfigurationProperties + @Validated 强制必填
拼写错误 feature.enable vs feature.enabled 同上,绑定失败即启动失败
@ConfigurationProperties(prefix = "clarity.mq")
@Validated
public record MqProperties(
        @NotBlank String topic,
        @Min(1) @Max(60) int timeoutSeconds,
        @NotNull Duration ackTimeout) { }

record + @Validated 的价值是配置有问题时启动就失败,而不是首次发消息时抛 NullPointerException。


7. 本地与容器环境的差异

差异点 本地 容器内 处理
主机名 localhost / 127.0.0.1 Service 名(如 mysql) 用 profile 分开,或统一走环境变量
端口 直接映射 容器端口 vs Service 端口 server.port 只影响容器内监听
时区 宿主机时区 多为 UTC 显式 TZ=Asia/Shanghai 与 -Duser.timezone
文件路径 相对路径 挂载卷路径 路径走配置,不写死

时区最容易出隐蔽 bug:容器默认 UTC,LocalDateTime.now() 与库里的 NOW() 差 8 小时。要么统一全 UTC(推荐),要么镜像里显式设 TZ,不要一半 UTC 一半本地。


8. 配置审计

需求 手段
谁改的、何时改 Git 提交历史 + CODEOWNERS 审批
改了什么 配置中心的版本对比 / git diff
线上当前值 GET /actuator/env(需鉴权)、/actuator/configprops
敏感项是否外泄 提交前 gitleaks 扫描 + CI 卡口
# 对比两个环境的配置差异(键级)
$ curl -s localhost:8081/actuator/env | jq -r '.propertySources[].properties | keys[]' | sort > /tmp/dev.txt
$ curl -s localhost:8080/actuator/env | jq -r '.propertySources[].properties | keys[]' | sort > /tmp/prod.txt
$ diff /tmp/dev.txt /tmp/prod.txt

9. 排错

现象 根因 处理
jar 外的 application.yml 不生效 用了 spring.config.location 但只给了目录,覆盖了默认位置 用 spring.config.additional-location 追加而非替换
profile 文件被 jar 内同名文件覆盖 打包时把外部配置也打进了 jar 外置配置目录不参与打包,或在启动脚本里显式指定
环境变量没生效 命名不符合松散绑定(连字符未去掉) 按 SPRING_DATASOURCE_HIKARI_MAXIMUMPOOLSIZE 这类规则改
@Value 刷新后仍是旧值 Bean 不在 @RefreshScope 内 加 @RefreshScope,或改用 @ConfigurationProperties
配置中心默认值不生效 中心里存的是空字符串,覆盖了本地默认值 空值视为未配置,在客户端统一处理
启动报 InvalidConfigDataPropertyException 在 profile 文档里写了 spring.profiles.active 移到主文档、命令行或环境变量
密钥进了 Git 历史 早期直接提交过 轮换凭据,历史用 git filter-repo 清理,加 CI 扫描
多实例配置不一致 某实例没挂到同一配置中心命名空间 用 /actuator/env 逐实例比对键集合

10. 边界

场景 更合适的做法 理由
单机小服务 一个 application.yml + 环境变量 上配置中心是负担
需要秒级生效的开关 配置中心 + @RefreshScope 的窄 Bean 全量刷新风险大
多租户差异化配置 数据库表 + 运行时查询 配置文件是部署级,不是租户级
强合规环境 密钥管理服务 + 短期凭据 环境变量有泄露面

一句话:配置文件管"部署差异",配置中心管"运行期开关",密钥管理管"凭据"。三者混在一处,就是事故的起点。


11. 自测

  1. 命令行参数、环境变量、application-prod.yml、application.yml,覆盖顺序是什么?
  2. spring.config.location 与 additional-location 的区别?哪个会顶掉默认配置?
  3. 为什么在 application-prod.yml 里写 spring.profiles.active 会报错?
  4. @RefreshScope 加在类上,类里的 @Value 就会刷新吗?什么情况下不会?
  5. 容器里 SPRING_DATASOURCE_HIKARI_MAXIMUM-POOL-SIZE 为什么无效?正确写法是什么?
  6. 怎么在不暴露敏感值的前提下,比对两个环境的配置差异?
S06-X10 · 配置管理与多环境隔离
https://www.clxhxhhr.top/posts/5846/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。