拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot yml配置加载顺序与优先级实战解析

Spring Boot yml配置加载顺序与优先级实战解析

1. yml文件的"藏身之处":默认搜索路径与优先级

刚接触Spring Boot的时候,我犯过一个很蠢的错误:在IDEA里面改了application.yml,重启应用,配置纹丝不动。后来才反应过来,jar包部署和本地run的配置加载路径根本不是一回事。Spring Boot搜索yml文件是有固定套路的,搞不清楚这套规则,改配置就永远像在隔山打牛。

1.1 四个默认位置与"外部优先"原则

Spring Boot启动时,会按照下面四个位置依次搜索application.yml(或者application.properties),找到就加载,没找到就继续往下找:

  1. file:./config/—— 当前目录下的config子目录
  2. file:./—— 当前目录(也就是java -jar执行时所在的目录)
  3. classpath:/config/—— classpath根目录下的config子目录
  4. classpath:/—— classpath根目录

优先级从上到下递减,也就是说,越靠外的位置,优先级越高。这里有个非常容易忽略的点:file:./config/是最高优先级,它排在jar包内部的classpath:/前面。这就意味着,如果你在jar包同级的config目录放了一份application.yml,它就能覆盖打包进jar里的那份配置。

生产中常见的做法是:把配置文件放到jar包旁边的config/目录里,替换掉打包进去的默认配置。运维同学改配置只需要编辑外部文件,不用重新解包jar,也不用重新构建项目。

1.2 spring.config.name:连文件名都换掉

默认配置文件名是application,但很多项目会遇到"不想用application命名"的场景。比如接入了Nacos,本地兜底配置可能叫bootstrap.yml;或者公司规范要求所有服务用service-{name}-config.yml这种命名。

这时候用spring.config.name参数直接指定文件名前缀就行:

java -jar app.jar --spring.config.name=myapp

Spring Boot会去找myapp.yml或者myapp.properties。注意,spring.config.name是可以指定多个名字的,用逗号隔开。但它和spring.config.location有区别:它改的是文件名,而spring.config.location改的是搜索目录,两者不要混淆。

1.3 location参数:目录与文件的精确控制

如果你希望配置文件的搜索范围完全由自己掌控,可以用spring.config.location。这个参数会替换掉默认的那四个位置:

java -jar app.jar --spring.config.location=file:/etc/app/config/

注意路径最后一定要带斜杠,否则Spring Boot会认为你指定的是一个具体的配置文件名,而不是目录。

还有一种更稳妥的姿势是spring.config.additional-location,它不会替换默认搜索位置,而是在默认位置基础上追加新的目录,追加的目录优先级更高:

java -jar app.jar --spring.config.additional-location=file:/opt/config/

我给一个实战建议:线上部署时,优先用additional-location,而不是location。原因很简单——location一旦写了,默认的四个位置全部失效,万一新目录里配置文件写漏了某个参数,应用就会用默认值启动,容易出事故。additional-location则是"既有默认兜底,又能外部覆盖",保留了回退的能力。

注意:Spring Boot 2.4之后,spring.config.location的语义有了调整,新增了optional:前缀概念。不带optional:时,如果指定目录下没有配置文件,应用会启动失败;带上optional:前缀,找不到配置也能继续启动。这对容器化部署很有用。

2. 一张表格看懂全部配置来源的高低排序

application.yml只是整个配置体系中的一环。Spring Boot真正读取配置时,会同时从命令行、系统属性、环境变量、各种外部配置文件等多个来源取值,这些来源之间有严格的优先级顺序。我在这个环节上栽过的跟头,比改错路径多得多。

2.1 最重要的十几个属性源排位

Spring Boot官方定义了完整的外部化配置加载顺序,从高到低简化成一张表,平时够用了:

优先级配置来源示例
最高命令行参数--server.port=8081
高SPRING_APPLICATION_JSON环境变量或系统属性里的JSON
较高ServletConfig/ServletContext参数Web容器初始化参数
中高Java系统属性-Dserver.port=8081
中OS环境变量SERVER_PORT=8081
中低jar包外部的application-{profile}.ymlconfig/application-prod.yml
低jar包内部的application-{profile}.ymlclasspath里的profile配置
更低jar包外部的application.yml外部基础配置
最低jar包内部的application.yml打包进jar的基础配置
兜底SpringApplication默认属性setDefaultProperties

这条从高到低的顺序,核心记忆点只有一句话:离程序运行环境越远的东西,优先级越高。命令行参数是运维在启动时现场敲的,当然要能压过一切配置文件;环境变量是部署平台(比如K8s)注入的,也应该覆盖打包到jar里面的默认值。

2.2 命令行和环境变量是怎么"压过"yml的

我举一个自己真实遇到的场景。开发环境的application.yml里写的是:

server: port: 8080

但测试环境用Docker部署,docker run命令里加了一个环境变量:

docker run -e SERVER_PORT=8081 app

因为环境变量的优先级高于jar包内部的yml,最终应用监听的是8081,不是yml里的8080。一行yml都没改,端口却变了,这就是外部化配置的妙处。

命令行参数的优先级连环境变量都压得过。调试时想临时改一下数据源,又不想污染公共配置文件,直接:

java -jar app.jar --spring.datasource.url=jdbc:mysql://192.168.1.10:3306/testdb --spring.datasource.username=readonly

这样改只对本次启动生效,重启后自动恢复原配置,特别适合应急排查。

2.3 profile专属配置与基础配置,谁覆盖谁

同样两份配置文件,application.yml和application-dev.yml,如果两项配置都定义了server.port,最终生效的是application-dev.yml里的值。规则是profile专属配置覆盖基础配置。

Spring Boot的逻辑是:先把application.yml基础配置加载成一份PropertySource,然后把application-{profile}.yml也加载进来,作为优先级更高的属性源。所以profile文件里的相同key会覆盖基础配置。

这就衍生出一个挺实用的技巧:公共配置放application.yml,环境差异化配置放application-dev.yml、application-prod.yml。比如日志级别、超时时间这些不同环境差异很大的参数,不需要复制粘贴到每个环境,只需要激活对应profile就行。

3. 修改yml配置的实操:环境拆解、参数覆盖与加密

知道了配置文件从哪里来、谁先谁后,终于可以聊"改"了。修改yml不是只有编辑文件这一条路,不同场景有不同改法,选对了效率翻倍。

3.1 最基础的改法:直接编辑与多环境拆分

单体项目最直观的改法就是编辑application.yml然后重启。但项目一多、环境一变,单文件方案立刻不够用。拆分多环境Profile是最常见的做法:

# application.yml spring: application: name: order-service profiles: active: dev --- spring: config: activate: on-profile: dev server: port: 8080 --- spring: config: activate: on-profile: prod server: port: 8080

注意Spring Boot 2.4之后,推荐用spring.config.activate.on-profile来声明Profile片段,旧版的spring.profiles写法在新版本里不再建议使用。启动时用--spring.profiles.active=prod切环境,切换时零代码修改。

3.2 运行时用命令行参数临时覆盖

线上环境改配置文件需要重新发布,或者至少动到服务器文件。但如果只是想临时调整一个参数,命令行覆盖是成本最低的方案:

java -jar app.jar --server.port=8082 --logging.level.com.example=DEBUG

这里有一个很多人会搞混的细节:--server.port是Spring Boot特有的"宽松绑定"形式,它是命令行参数,不是Java系统属性。区别在哪?

# 系统属性,必须放在-jar前面 java -Dserver.port=8082 -jar app.jar # 命令行参数,必须放在jar包后面 java -jar app.jar --server.port=8082

两者优先级也不同:命令行参数 > Java系统属性。如果你的测试环境里既有系统属性-Dserver.port=8082,又有命令行参数--server.port=8083,最终生效的是8083。

3.3 yml敏感信息加密:jasypt实战

yml里最让人头疼的就是数据库密码、Redis密码、第三方密钥这类敏感信息。明文提交到Git仓库就是事故。我用的最多的方案是jasypt-spring-boot-starter。

第一步加依赖:

<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.5</version> </dependency>

第二步配置yml:

jasypt: encryptor: password: ${JASYPT_ENCRYPTOR_PASSWORD} algorithm: PBEWITHHMACSHA512ANDFORGETTING_256 iv-generator-classname: org.jasypt.iv.NoIvGenerator spring: datasource: password: ENC(x7G2t5R9kQ==)

第三步用工具类生成密文:

StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor(); encryptor.setPassword("你的密钥"); encryptor.setAlgorithm("PBEWITHHMACSHA512ANDFORGETTING_256"); String encPwd = encryptor.encrypt("真实密码"); System.out.println(encPwd);

用ENC()包裹密文写入yml。启动时要求环境变量JASYPT_ENCRYPTOR_PASSWORD存在,否则解密失败。

几个必须避开的坑:

  • 加密密钥绝对不能写进yml或git,用环境变量或K8s Secret注入
  • 不同jasypt版本支持的算法不一样,老项目升版本后经常报PBEWithMD5AndTripleDES不支持
  • Spring Boot 3.x必须用jasypt-spring-boot-starter 3.0.4以上版本,否则启动直接报NoClassDefFoundError

3.4 随机端口配置与获取实际端口

随机端口是热搜词里反复出现的场景,也是挺容易踩坑的一个点。yml里写:

server: port: ${random.int[8080,9000]}

每次启动都会从8080到9000之间随机选一个端口。这个${random.int[a,b]}是Spring Boot内建的RandomValuePropertySource提供的占位符能力,不需要额外引入东西。

如果想让操作系统完全随机分配端口,可以写server.port: 0。但有个坑:写成0之后,Spring Boot并没有暴露"我实际监听了哪个端口",日志里也只显示0。做服务注册或者联调时,你需要一份代码去拿真实端口:

@Configuration public class PortReporter { @EventListener public void onWebServerInitialized(WebServerInitializedEvent event) { int actualPort = event.getWebServer().getPort(); log.info("当前实际端口:{}", actualPort); } }

提示:随机端口和固定端口混用时要特别小心。如果你的环境变量里设置了SERVER_PORT=8081,那yml里的${random.int[8080,9000]}根本不会生效,因为环境变量优先级压过了yml里的随机占位符。这个现象我见过很多次,排查时先看一下环境变量。

4. 覆盖规则反直觉的三个经典踩坑现场

加载顺序的规则本身不复杂,复杂的是规则和现实交织之后的那些"反直觉"瞬间。下面这三次踩坑经历,基本能代表配置覆盖问题上九成的情况。

4.1 随机端口被"固定"的诡异问题

有一次我把生产环境的yml改成server.port: ${random.int[8080,9000]},目的是想让多实例在裸机上启动时自动错开端口。结果第一个实例启动后端口是8080,第二个实例启动后端口也是8080,直接端口冲突。

排查过程很典型:我先看jar包里的application.yml,确实是随机配置,没问题;再看启动命令,没指定端口;最后查Docker环境变量——SERVER_PORT=8080躺在容器的环境变量列表里。

原因就是前面说的优先级规则:OS环境变量SERVER_PORT的优先级高于jar包内部yml里的${random.int[8080,9000]}。Spring Boot把环境变量绑定成了server.port=8080,根本不看yml里的随机值。

解法很简单:把环境变量里的SERVER_PORT删掉,或者改成可覆盖的写法,在yml里用占位符加默认值兜底:

server: port: ${SERVER_PORT:${random.int[8080,9000]}}

这样如果环境变量存在,就尊重环境变量;如果没有,就走随机。

4.2 profile专属文件里写spring.profiles.active为什么不生效

另一个让我困惑了很久的问题:在application-prod.yml里写了spring.profiles.active: prod,结果启动时没激活prod。后来才搞明白,profile专属文件是在激活了对应profile之后才会被加载的,你指望一个"结果产物"去决定"产生条件",逻辑上就说不通。

Spring Boot 2.4之后的版本更严格:如果把spring.profiles.active写在profile专属文件里,启动时会直接报错。正确做法是把profile激活信息放在最基础的application.yml里,或者放在启动命令和环境变量里:

java -jar app.jar --spring.profiles.active=prod

容器化部署推荐用环境变量:

docker run -e SPRING_PROFILES_ACTIVE=prod app

4.3 用actuator的/env快速定位配置来源

遇到配置覆盖说不清的时候,与其猜,不如直接让Spring Boot自己交代。只要引入actuator依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>

然后访问GET /actuator/env,接口会把当前环境中所有PropertySource按优先级排序返回,每个属性后面还会标注origin,直接告诉你这个值来自哪个文件哪一行。范围缩小、找到来源、确认优先级,三步走完,问题通常就水落石出了。

如果只想看某一个具体属性,可以用:

curl http://localhost:8080/actuator/env/server.port

返回里会列出server.port在所有PropertySource中的值,以及最终生效值。这个接口在生产环境必须配置好权限,最好用Spring Security限制外部访问,否则配置信息泄露风险很高。

5. 生产级配置管理:从yml到配置中心的演进经验

单机版yml管理配置,跑一两个服务完全OK。但当服务数量上到十几二十个之后,配置管理的痛点就另一码事了。这里聊一下我对配置管理演进的实战体会。

5.1 占位符与默认值:给外部覆盖留一条路

yml配置里,用占位符加默认值的方式,比直接写死值更灵活。比如:

server: port: ${SERVER_PORT:8080} spring: datasource: url: ${DB_URL:jdbc:mysql://localhost:3306/order_db} username: ${DB_USERNAME:order_user} password: ${DB_PASSWORD:order_pwd}

这套写法的好处是:默认情况直接启动就能跑,不需要任何外部变量;一旦部署环境注入同名环境变量,立即覆盖默认值。本机开发、测试环境、生产环境,同一份yml不用改一个字。

这里也有个度的问题:不是所有配置都需要用占位符。只有那些"可能被外部覆盖"的属性才需要,比如端口、数据源地址、Redis地址、第三方服务URL。像spring.application.name这种很少变的,写死就好,全用占位符反而增加阅读成本。

5.2 多环境与敏感配置的管理技巧

多环境配置说白了就是两难:既要环境隔离,又怕重复维护。我的建议是遵循"三块式"结构:

  • application.yml:公共配置,所有环境共享,只放非敏感的通用项
  • application-{profile}.yml:环境差异配置,按需拆dev/test/prod
  • 敏感信息:用jasypt加密或直接引用环境变量

另外一定要盯住Git仓库权限。我见过不止一次,有人把生产数据库密码以明文形式提交到GitLab,然后被扫描工具告警。养成习惯:提交前搜索一下password=、jdbc:mysql这些关键字。

5.3 什么时候该上配置中心

当几个服务共享同一份配置时,yml的"各自为政"模式就尴尬了。改了公共配置要逐个服务改一遍,漏一个就出问题。这时候可以考虑Nacos、Consul、Spring Cloud Config这类配置中心。

我用Nacos之后的感受是:配置支持动态刷新,不需要重启应用,对存量服务平滑很多。从技术的角度,yaml文件依旧是基础,本地yml充当兜底,配置中心作为更高优先级来源。这样即使配置中心挂了,应用还能用本地配置启动。

不过上配置中心不要一步到位。我的建议是:先让所有服务的基础配置规范化,确保本地yml可以独立启动,再引入配置中心管理公共配置。否则配置中心一出问题,全部服务跟着宕机,压力会非常大。

6. 面试高频题:加载顺序怎么答才能让面试官点头

这个话题出现在热搜词不是没道理的,配置加载顺序确实是Java面试里被问烂了又不烂的经典题。关键区别在于:有的人能背出顺序,有的人能讲清楚为什么和怎么排查。后者才是面试官想要的。

6.1 让优先级排序好记的一句话

完整背出官方十七项顺序对大多数人来说不现实,也没必要。面试的时候能说出核心原则再加几个关键节点就够了。

核心原则就是:外部覆盖内部,运行时覆盖静态,具体覆盖通用。

展开说是四个要点:

  • 命令行参数优先级最高,因为它代表"现场意图"
  • 环境变量高于配置文件,因为部署平台可以通过环境变量动态调整
  • 外部(jar包外部)优先级高于内部(classpath内),方便运维在不重新打包的前提下改配置
  • profile专属配置高于基础配置,application-dev.yml覆盖application.yml

把这四点讲清楚,面试官基本就会认可你对这套机制的理解,而不只是背下来。剩下的细节,用到再查就行。

6.2 常见的两个面试场景推演

第一个场景:application.yml里配了server.port: 8080,环境变量配了SERVER_PORT=8081,命令行传了--server.port=8082,问最终端口是多少?

答案是8082。命令行参数最高优先级。如果在Docker里配了SERVER_PORT=8081,但启动命令里也传了--server.port=8082,最终也是8082。

第二个场景:application.yml里配了spring.profiles.active=dev,application-dev.yml里配了server.port: 8081,application.yml里配了server.port: 8080,问最终端口?

答案是8081。dev这个profile被激活了,profile专属配置的优先级更高。注意面试官可能会追问:application-dev.yml里能不能配置spring.profiles.active?前面讲过了,不能,至少Spring Boot 2.4之后不允许,会直接报错。

最后一个我个人的习惯性建议:面试里回答这类机制性问题,别急着背答案,先花五秒钟想一下你实际操作中遇到的问题。面试官想听的不是课本原文,而是你踩坑之后总结出来的那种理解。把真实场景带进去,说服力会强很多。

返回列表