Spring Boot 配置优先级实战:application.yml、环境变量、命令行参数到底谁覆盖谁
Spring Boot 配置优先级实战:application.yml、环境变量、命令行参数到底谁覆盖谁
线上出过这样的事:测试环境好好的,一上生产,数据库连的还是测试库。你在application.yml里明明写了生产地址,kubectl set env也注入了环境变量,可就是没生效。翻遍代码找不到 bug,最后发现是启动脚本里带了个--spring.datasource.url=...命令行参数,把一切都盖掉了。
Spring Boot 的配置来自十几个来源,它们有严格的优先级顺序。搞不清这套顺序,就会反复踩「我明明配了却不生效」的坑。这篇用可复现的例子把顺序讲透。
一个最小复现:同一个 key,三处都配
建一个属性app.mode,分别放到配置文件、环境变量、命令行,看最后谁赢。
// AppController.java@RestControllerpublicclassAppController{@Value("${app.mode}")privateStringmode;@GetMapping("/mode")publicStringmode(){return"app.mode = "+mode;// 打印最终生效的值}}# application.ymlapp:mode:from-yaml先只跑配置文件,访问/mode返回from-yaml。现在加环境变量再启动:
# 环境变量里的点要换成下划线、大写(Relaxed Binding 规则)exportAPP_MODE=from-envjava-jarapp.jar# 访问 /mode -> app.mode = from-env ← 环境变量赢了 yml再叠加命令行参数:
exportAPP_MODE=from-envjava-jarapp.jar--app.mode=from-cli# 访问 /mode -> app.mode = from-cli ← 命令行赢了环境变量结论初现:命令行 > 环境变量 > application.yml。这三者是日常最容易撞车的,记牢它们的相对顺序能解决大部分问题。
完整优先级顺序(高到低,记常用的几档)
Spring Boot 官方定义了一长串 PropertySource,从高到低,高优先级覆盖低优先级。日常真正会用到的是这几档:
- 命令行参数(
--key=value)——最高,谁都盖不过它。 SPRING_APPLICATION_JSON(环境变量里塞一段 JSON)。- 操作系统环境变量(
APP_MODE=...)。 application-{profile}.yml(profile 专属配置,如application-prod.yml)。application.yml(通用配置)——最低的一档常用配置。- jar 包内的默认值 /
@PropertySource。
一句话:越「靠外、越临时」的来源优先级越高。命令行是启动那一刻拍板的,自然最高;打进 jar 的默认值最容易被各种外部值覆盖,最低。
高频坑一:环境变量的名字怎么映射
上面app.mode对应的环境变量是APP_MODE,不是app.mode。这是 Spring 的 **Relaxed Binding(宽松绑定)**规则:
- 点
.和中划线-都替换成下划线_ - 全部大写
举几个例子,别搞错:
spring.datasource.url -> SPRING_DATASOURCE_URL app.max-retry-count -> APP_MAX_RETRY_COUNT server.servlet.context-path -> SERVER_SERVLET_CONTEXT_PATH在 K8s / Docker 里注入配置,靠的就是这条规则。写错一个字母,环境变量就静默不生效,而且不报错——这是「配了没用」的头号原因。
高频坑二:profile 专属配置和通用配置的关系
很多人以为激活了prodprofile,application.yml就不读了。其实两者是叠加的:先加载application.yml,再用application-prod.yml覆盖同名 key,没被覆盖的 key 仍然用通用文件里的。
# application.yml —— 通用,所有环境都读app:mode:defaulttimeout:3000# application-prod.yml —— 激活 prod 时叠加覆盖app:mode:productionjava-jarapp.jar--spring.profiles.active=prod# 最终:app.mode=production(被 prod 覆盖),app.timeout=3000(prod 没写,沿用通用)所以把「所有环境共用的默认值」放application.yml,把「各环境差异项」放application-{profile}.yml,是最省心的组织方式。
高频坑三:@Value 注入的是「解析后的最终值」
有人担心用@Value会不会绕过优先级、读到文件里的原始值。不会。Spring 把所有 PropertySource 合并成一个统一的Environment,@Value和@ConfigurationProperties拿到的都是优先级合并后的最终结果。想在代码里确认某个值到底从哪来,可以直接查Environment:
@ComponentpublicclassConfigDebugger{@AutowiredprivateConfigurableEnvironmentenv;@PostConstructpublicvoiddump(){// 打印最终生效的值System.out.println("final app.mode = "+env.getProperty("app.mode"));// 遍历所有 PropertySource,看它们的优先级顺序(靠前的优先级高)env.getPropertySources().forEach(ps->System.out.println("source: "+ps.getName()));}}启动时这段会按优先级从高到低打印所有配置源。开头那个「生产连了测试库」的事故,用这段代码一眼就能看到commandLineArgs排在最前面,凶手立现。
实战:线上排查「配了不生效」的固定套路
遇到配置不生效,别改代码瞎试,按这个顺序查:
# 1. 确认启动命令有没有夹带命令行参数(最高优先级,最容易被忽略)ps-ef|grepjava# 看完整启动命令里有没有 --xxx=yyy# 2. 确认环境变量名字对不对(Relaxed Binding)env|grep-iAPP# 看实际注入的环境变量# 3. 确认激活了哪个 profile# 日志里搜 "The following profiles are active"再配合上面的ConfigDebugger打印,基本 5 分钟内定位。
小结
- 核心顺序(高到低):命令行参数 > 环境变量 >
application-{profile}.yml>application.yml> jar 内默认值,规律是「越外部越临时,优先级越高」。 - 环境变量遵循Relaxed Binding:点和中划线换下划线、全大写,写错静默失效,是「配了没用」头号原因。
- profile 配置和通用配置是叠加覆盖,不是替换:共用值放
application.yml,差异值放application-{profile}.yml。 @Value/@ConfigurationProperties拿到的是合并后的最终值;排查用Environment.getPropertySources()看真实来源。
记忆点:「配了不生效」先查启动命令有没有夹带--key=value——命令行参数优先级最高,谁都盖不过它。