Spring Boot 配置体系
配置的消费端(@Value、@ConfigurationProperties 怎么用)见常用注解,Environment 在启动流程中的位置见启动原理。本文讲来源端:配置从哪来、谁覆盖谁、绑定规则是什么。代码基于 Spring Boot 4.x——配置体系自 2.4 定型后未再变动,3.x/4.x 通用。
Environment 抽象:一条有序链表
Spring 把所有配置来源统一抽象为 PropertySource:配置文件、环境变量、系统属性、命令行参数,各自包装成一个 PropertySource,按优先级排成一条有序链表,挂到 Environment 上。查询语义只有一条规则:
Environment.getProperty(key)从链表头开始遍历,先查到的赢。
优先级没有独立的规则引擎——链表顺序就是优先级。启动后可以直接验证:
1 | |
理解这条链表,下文所有覆盖行为都是它的推论。
配置来源与优先级
官方文档列出十几级来源,工程上只需记住这张精简表(高 → 低):
| 优先级 | 来源 | 示例 |
|---|---|---|
| 1 | 命令行参数 | --server.port=9001 |
| 2 | Java 系统属性 | -Dserver.port=9001 |
| 3 | OS 环境变量 | SERVER_PORT=9001 |
| 4 | profile 配置文件 | application-prod.yml |
| 5 | 主配置文件 | application.yml |
| 6 | @PropertySource | 注解标注的额外文件 |
| 7 | 代码默认值 | setDefaultProperties / 绑定默认 |
两条记忆规则覆盖全部细节:
- 越外越高:命令行 > 系统属性 > 环境变量 > jar 外文件 > jar 内文件。离代码越远、离部署越近的来源越优先——改配置不需要重新打包。
- 越具体越高:profile 文件 > 主文件;同一文件内,后面声明的属性覆盖前面的。
测试场景另有 @TestPropertySource、@SpringBootTest(properties = ...) 两级,插在整个链表最前面,不影响生产语义。
配置文件的加载
application.yml 按以下位置扫描,后加载的覆盖先加载的(低 → 高):
classpath:/classpath:/config/- 当前目录
./ - 当前目录
./config/ - 当前目录
./config/*/(每个子目录)
同位置同时存在 .properties 和 .yml 时 .properties 优先。两个格式混用没有收益,只留 .yml。
需要引入扫描位置之外的文件,用 spring.config.import 显式声明:
1 | |
configtree 直接对齐 K8s 挂载 ConfigMap/Secret 的目录结构,是容器环境注入配置的标准入口。
Profile 机制
profile 是"按环境叠加配置"的机制,不是"切换配置文件"。主文件总是加载,激活的 profile 文件在其上叠加并覆盖同名属性。
激活方式(三种等价,按优先级表生效):
1 | |
多 profile 用逗号分隔,后面的覆盖前面的:--spring.profiles.active=common,prod 中 prod 的属性覆盖 common。这个顺序语义经常被忽略,两个 profile 定义了同名 key 时结果就是反直觉的。
yml 单文件内可用多文档块内联 profile,与拆成 application-prod.yml 完全等价:
1 | |
一组 profile 总是同时激活时,用 group 收敛为一个名字:
1 | |
--spring.profiles.active=prod 实际激活 proddb 与 prodmq,且组内顺序保留"后覆盖前"语义。
绑定机制
Relaxed Binding
@ConfigurationProperties 绑定时不要求 key 精确匹配,四种写法等价:
| 来源 | 写法 | 绑定到 |
|---|---|---|
| yml / properties | app.main-name(推荐 kebab-case) | appProperties.mainName |
| yml / properties | app.mainName | 同上 |
| 系统属性 | -Dapp.main-name=x | 同上 |
| 环境变量 | APP_MAIN_NAME | 同上 |
环境变量的换算规则:点转下划线、全大写、去掉连字符。这是部署层覆盖任意配置项的通用通道——不用改任何文件,APP_MAIN_NAME=xyz 即可覆盖 app.main-name。
注意边界:relaxed binding 只作用于 @ConfigurationProperties。@Value("${app.mainName}") 走占位符解析,key 必须与环境中的属性名精确一致。
构造器绑定与校验
绑定目标首选 record:不可变、构造器绑定、配合 @Validated 在启动期校验:
1 | |
字段缺失或越界时应用在启动期直接失败,附字段名与来源定位——比运行期空指针早暴露一个生命周期。
@Value 的三个边界
- 无 relaxed binding,key 必须精确;
- 无校验能力,拼错 key 只能靠默认值兜底:
@Value("${app.mail.host:localhost}"); - 标量散值用它可以,成组配置必须走
@ConfigurationProperties——两者的选择标准见常用注解。
多环境管理实践
主文件放默认值,profile 文件只放差异。 application.yml 承载所有环境共享的配置与本地开发默认值;application-prod.yml 只写差异项——端口、数据源地址、特性开关。review 配置变更时 diff 出来的就是环境差异本身。
敏感配置不进仓库。 密码、密钥通过环境变量或 configtree 注入,由部署平台(K8s Secret、CI 变量)保管。代码库里出现敏感信息即视为泄露,轮换与清理 Git 历史比"下次注意"便宜。
一次构建,多处部署。 jar 构建产物在所有环境相同,环境差异全部来自外部配置——这是优先级设计"越外越高"的意图,也是 12-Factor 的配置原则。为每个环境打一个包等于放弃了整条优先级链。
常见坑
profile 文件里声明 spring.profiles.active:2.4 起直接启动失败,抛 InvalidConfigDataPropertyException。profile 文件只能由外部激活,不能自我激活。
环境变量名写错:server.port 对应 SERVER_PORT,写成 SERVER.PORT 或 server_port 都不会绑定。换算规则只有一条:点转下划线再全大写。
改了 jar 内配置不生效:部署目录存在同名 jar 外配置文件,覆盖了 jar 内的修改。排查先看 ./config/ 与 ./ 下有没有遗留文件。
占位符在 @PropertySource 文件中找不到 key:@PropertySource 加载的属性不参与 @ConfigurationProperties 的 relaxed binding,且在 Environment 链表中位置靠后(第 6 级),容易被主配置文件覆盖。
yml 与 properties 同存:同位置 .properties 优先于 .yml,两个文件各改一处,生效的是 properties——症状是"改了 yml 没反应"。
检查清单
| 检查项 | 标准 |
|---|---|
| 格式 | 只用 yml,不与 properties 混用 |
| 分层 | 主文件放默认值与共享项,profile 文件只放差异 |
| 覆盖 | 临时覆盖用命令行或环境变量,不改文件 |
| 绑定 | 成组配置用 @ConfigurationProperties + record + @Validated |
| 敏感项 | 环境变量 / configtree 注入,不进仓库 |
| profile | 用 group 收敛组合;不在 profile 文件里激活 profile |
相关阅读
- Spring Boot 常用注解:
@Value与@ConfigurationProperties的用法对比 - Spring Boot Startup:Environment 准备在启动流程中的位置与扩展事件
- Spring Boot Bean 使用规范:
@ConditionalOnProperty与条件化装配



