Spring Boot 配置文件的加载优先级是什么?
简化版
Spring Boot 把命令行参数、环境变量、配置文件等多种配置来源统一收进 Environment,每种来源是一个 PropertySource。同名 key 由优先级决定谁生效——命令行参数通常高于系统属性和环境变量,外部配置数据又高于 jar 内配置数据,高优先级覆盖低优先级。这让你不改包就能在不同环境覆盖默认值。
详细版
配置远不止「读 application.yml」这么简单。启动时 Spring Boot 会收集一长串属性来源。只看常见运行期来源,从高到低可记为:命令行参数 → SPRING_APPLICATION_JSON → Java System Properties → 操作系统环境变量 → Config Data。测试属性与 Devtools 还有更高的专用层级,因此这不是省略上下文后仍能覆盖所有场景的“绝对总表”。
Config Data 内部再按“jar 内普通 → jar 内 profile → jar 外普通 → jar 外 profile”逐级覆盖。相同位置若同时存在 properties 与 YAML,当前官方规则下 properties 优先;工程上最好统一一种格式,避免靠这个细节工作。
解析同一个 key 时,高优先级的值覆盖低优先级——所以生产部署时用命令行参数或环境变量覆盖默认配置是标准做法,不用重新打包。
完整版教学
一、核心心智模型:多个 PropertySource 叠在一起
理解配置优先级,先建立这个模型:Environment 里装着一摞 PropertySource,像一叠透明胶片按优先级从上到下排好。查一个 key 时,从最上面(最高优先级)那张开始找,找到第一个就用,不再往下看。
所以「优先级」的本质就是这摞胶片的顺序。命令行参数在最上面,jar 内配置在最下面。这个模型一旦建立,「为什么命令行能覆盖配置文件」就一目了然了——它在更上层,先被找到。
二、为什么要设计这么多层级——环境差异
一个应用要在开发、测试、生产跑,各环境的数据库地址、端口、密钥都不同。如果配置写死在包里,每换环境就得重新打包,既麻烦又危险。
多来源 + 优先级的设计,让同一个包能适应所有环境:包里放一套默认(开发用),部署到生产时用环境变量/命令行覆盖差异项即可。这正是「一次构建,到处运行」的配置基础,也是云原生/容器部署的标配做法(K8s 通过环境变量注入配置)。
三、Profile:按环境切换整组配置
Profile 是管理环境差异的更结构化手段:
application.yml # 公共配置
application-dev.yml # 开发环境专属
application-prod.yml # 生产环境专属
启动时用 --spring.profiles.active=prod 激活,Spring Boot 就加载 application.yml + application-prod.yml,后者覆盖前者的同名项。
记忆点:用 Profile 切换环境,而不是在代码里写
if (env == "prod")。把环境差异沉淀到配置,代码保持干净、与环境无关。
四、两个实践红线
① 敏感信息不进仓库:数据库密码、Token、密钥绝不能直接写进提交到 Git 的配置文件。应通过环境变量、密钥管理服务(如 Vault)、部署平台注入,并确保日志不会打印完整密码/Token。
② 复杂配置用 @ConfigurationProperties 而非散落的 @Value:
@ConfigurationProperties(prefix = "app.pay")
public class PayProperties {
private String appId;
private Duration timeout;
// getter/setter
}
@ConfigurationProperties 把一组配置类型安全地绑到一个对象上,支持层级结构、支持校验(@Validated)、便于重构;启动时若配置缺失或格式错能早暴露。而 @Value("${...}") 散落在各处,难维护、难校验,只适合极少数单个值。
五、用同名端口算例走一遍覆盖链
假设 jar 内 application.yml 写 server.port=8080,jar 外 application-prod.yml 写 8081,环境变量 SERVER_PORT=8082,启动参数再写 --server.port=8083。激活 prod 后最终端口是 8083;去掉命令行后变成 8082,再去掉环境变量才落到外部 profile 的 8081。这种逐层删除实验比死背一句口诀更容易定位生产值来自哪里。
| 常见来源 | 示例 | 相对优先级 | 适合用途 |
|---|---|---|---|
| 命令行参数 | --server.port=8083 | 高 | 临时覆盖、一次性运行 |
| JSON 属性块 | SPRING_APPLICATION_JSON | 次高 | 受属性名限制的部署环境 |
| Java System Property | -Dserver.port=8082 | 高于环境变量 | JVM 启动级覆盖 |
| OS 环境变量 | SERVER_PORT=8082 | 高于 Config Data | 容器平台注入 |
| jar 外 Config Data | 外部 application-prod.yml | 高于 jar 内 | 环境差异 |
| jar 内 Config Data | 包内 application.yml | 低 | 安全默认值 |
lookup(server.port):
commandLine 8083 → 命中,停止
若删除:SPRING_APPLICATION_JSON → systemProperties → systemEnvironment 8082
若再删除:external profile 8081 → external normal → packaged profile → packaged normal 8080
需要查实际来源时,可提高 org.springframework.boot.context.config 日志级别观察配置文件加载,也可在严格受控的环境使用 env/configprops 端点辅助诊断;这些端点可能暴露敏感线索,不能为排错直接对公网开放。
记忆钩子:先分“属性来源总顺序”,再分“Config Data 内部顺序”;环境变量高于文件,但 Java System Properties 又高于环境变量。
六、常见误区与追问
- 误区:操作系统环境变量高于 Java System Properties。 当前常见 Boot 规则中 System Properties 的优先级更高,二者都高于 Config Data。
- 误区:jar 内配置优先级最高。 包内配置用于提供默认值,jar 外普通与 profile 配置可以覆盖它,方便同一制品跨环境部署。
- 误区:SPRING_APPLICATION_JSON 里的 null 可以清空低优先级值。 解析器把 null 当作缺失,不能依靠它覆盖并删除下层属性。
- 追问:为什么 @PropertySource 改不了某些 logging 或 spring.main 属性? 它在上下文刷新阶段才加入 Environment,对启动早期已经读取的属性而言时机太晚。
- 追问:环境变量 server.port 应怎样写? 常见形式是
SERVER_PORT;复杂键还要遵守点转下划线、去掉短横线和大写等映射规则。 - 追问:profile 文件一定会被加载吗? 只有对应 profile 被激活或通过配置组等机制纳入时才加载,并且仍受位置与来源优先级约束。
七、加强记忆
Spring Boot 把多种配置来源收进 Environment 的一摞 PropertySource,常见运行期来源按“命令行 > JSON 属性块 > System Properties > 环境变量 > 外部 Config Data > jar 内 Config Data”覆盖同名 key,带 profile 的优先于不带的。这套设计让一个包适配多环境;实践上用 Profile 切环境、敏感信息靠环境变量注入、复杂配置用 @ConfigurationProperties 类型安全绑定。