Spring Boot 启动时会去好几个地方翻 application.properties,文件本身还有 .yml 版本、profile 版本,再往上还有命令行参数、环境变量、系统属性。这些来源最后是按固定顺序叠起来的,同一个 key 谁在后谁生效。
有哪些配置文件
| 文件 | 说明 |
|---|---|
application.properties / application.yml / application.yaml |
主配置,默认名字是 application |
application-{profile}.properties / .yml |
profile 专用,-dev、-prod 都是这个套路 |
bootstrap.properties / bootstrap.yml |
Spring Cloud 用的,父上下文,比 application 还早 |
spring.config.import=... 指向的文件 |
手动引进来的普通文件、configtree 目录 |
@PropertySource("classpath:xxx.properties") |
挂在 @Configuration 类上 |
~/.config/spring-boot/spring-boot-devtools.properties |
devtools 的全局配置,对所有项目生效 |
@TestPropertySource / @SpringBootTest(properties = {...}) |
只在测试里生效 |
bootstrap.properties 在新版本里默认已经不加载了。Spring Boot 2.4 之后推荐用 spring.config.import=configserver: 代替,想继续用 bootstrap 得单独加 spring-cloud-starter-bootstrap 这个依赖,否则那个文件放在那儿也不会被读。
默认找哪些位置
不写任何配置的情况下,Boot 会依次去这五个位置找(ConfigDataEnvironment 里写死的):
text
optional:classpath:/
optional:classpath:/config/
optional:file:./
optional:file:./config/
optional:file:./config/*/
file:./ 是运行目录 ,不是 classpath,也不是 IDEA 的模块目录。file:./config/*/ 里的 * 匹配 config/ 下的一级子目录。
越靠后的位置优先级越高。 所以 file:./config/ 里的配置能盖掉 classpath:/ 里的。
在五个位置各放一个 application.properties,每个里面写 loc=xxx,启动后把 Environment 里的属性源打出来:
text
Config resource 'file [config/sub/application.properties]' via location 'optional:file:./config/*/'
Config resource 'file [config/application.properties]' via location 'optional:file:./config/'
Config resource 'file [application.properties]' via location 'optional:file:./'
Config resource 'class path resource [config/application.properties]' via location 'optional:classpath:/config/'
Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'
这个列表是从高到低排的,最后 loc 的值是 file:./config/sub/ 里那个。
file:./config/*/ 是 2.4 才加进来的,* 匹配到的子目录全部 都会被加载(比如 config/a/、config/b/ 各放一份),按目录名排序,同样越靠后的越高。实测两个子目录分别写 sub=a、sub=b,最后生效的是 b:
text
Config resource 'file [config/b/application.properties]' via location 'optional:file:./config/*/'
Config resource 'file [config/a/application.properties]' via location 'optional:file:./config/*/'
同一个目录里的优先级
同一个目录下,能同时存在三种文件:
text
application-{profile}.properties ← 最高
application.properties
application.yml
.properties 压 .yml,profile 专用压普通。这两条都是实测过的,把 application.properties 和 application.yml 都放上:
properties
# application.properties
foo=from-properties
yaml
# application.yml
foo: from-yml
text
>>> foo = from-properties
属性源列表里也是 properties 在前:
text
Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'
Config resource 'class path resource [application.yml]' via location 'optional:classpath:/'
这两个文件不是"二选一",都会被加载。
.yml里独有的 key 照样读得到,只有冲突的 key 才会被 properties 盖掉。
profile 专用文件的优先级
网上很多说法是"application-{profile} 优先级最高",这话只对一半。
在同一个位置 里,application-{profile} 确实压过 application。但跨位置就不一定了。实测四组(Spring Boot 3.4.4,同一个 key 写在两边,看谁生效):
| classpath:/ | classpath:/config/ | file:./ | file:./config/ | file:./config/*/ | 结果 |
|---|---|---|---|---|---|
application-dev.properties |
application.properties |
classpath:/ 的 dev 赢 | |||
application-dev.properties |
application.properties |
file:./config/ 赢 | |||
application-dev.properties |
application.properties |
file:./ 赢 | |||
application-dev.properties |
application.properties |
file:./ 的 dev 赢 |
规律是:profile 专用文件会被提到自己所属的那个根(classpath: 或 file:)里所有普通文件之上,但 file 根整体还是压着 classpath 根。
text
file 根: file:./application-dev.properties
file:./config/*/application.properties
file:./config/application.properties
file:./application.properties
classpath 根:classpath:/application-dev.properties
classpath:/config/application.properties
classpath:/application.properties
classpath:/application.yml
所以 classpath 上的 application-dev.properties 打不过 file:./config/application.properties。打包成 jar 之后 profile 配置就在 classpath 里,file:./config/ 是运维能直接改的地方,想盖掉它只能靠位置更高的普通文件。
完整的属性源顺序
上面只说了配置文件。真正决定"最终生效的是哪个值"的是完整的属性源列表,从低到高:
| # | 来源 |
|---|---|
| 1 | SpringApplication.setDefaultProperties(...) 设的默认值 |
| 2 | @Configuration 类上的 @PropertySource |
| 3 | 配置文件(上面那五个位置找出来的全部) |
| 4 | random.* |
| 5 | 操作系统环境变量 |
| 6 | Java 系统属性(-Dxxx=yyy) |
| 7 | JNDI(java:comp/env) |
| 8 | ServletContext 初始化参数 |
| 9 | ServletConfig 初始化参数 |
| 10 | SPRING_APPLICATION_JSON |
| 11 | 命令行参数(--server.port=8081) |
| 12 | 测试注解的 properties 属性 |
| 13 | 测试里的 @DynamicPropertySource |
| 14 | 测试上的 @TestPropertySource |
| 15 | devtools 全局配置(~/.config/spring-boot/ 下) |
把 Environment 里的属性源打出来大概长这样:
text
configurationProperties
commandLineArgs
systemProperties
systemEnvironment
random
Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'
Config resource 'class path resource [application.yml]' via location 'optional:classpath:/'
applicationInfo
这是非 web 应用的输出,所以 JNDI、ServletContext 那几项没出现。configurationProperties 是 Spring 自己加的包装,不是真实来源。commandLineArgs 压 systemProperties、systemProperties 压 systemEnvironment,和上面表里的顺序对得上。
这张表能直接解释几个常见现象:
--server.port=8081一定能盖掉配置文件里的端口,因为命令行参数排第 11- 容器里配
SPRING_DATASOURCE_URL环境变量能盖掉 jar 里的配置,因为环境变量排第 5,比配置文件高 @PropertySource里的值会被application.properties覆盖,因为它排第 2,比配置文件还低
环境变量名怎么映射到 key 上(SPRING_DATASOURCE_URL → spring.datasource.url)属于 relaxed binding 的内容,这里不展开。
几个容易混的点
spring.profiles.active 能写在哪儿
写在 application.properties 里是可以的,不管这个文件在哪个位置,Boot 在激活 profile 的那一步就会读它,实测生效。
写在 profile 专用文件里不行,直接启动失败:
text
org.springframework.boot.context.config.InvalidConfigDataPropertyException:
Property 'spring.profiles.active' imported from location 'class path resource [application-dev.properties]' is invalid in a profile specific resource
原因是这个属性决定"要加载哪些 profile 专用文件",如果它自己出自一个 profile 专用文件,就变成先有鸡还是先有蛋。同理,spring.profiles.include、spring.config.activate.on-profile 也不能写在这里面。
spring.config.location 和 additional-location
一个替换、一个追加:
spring.config.location |
spring.config.additional-location |
|
|---|---|---|
| 效果 | 替换掉默认的五个位置 | 在默认位置之外追加 |
| 优先级 | 只剩指定的这几个位置 | 指定的位置 > 默认的五个位置 |
写成 spring.config.location=file:./extra/,默认位置就全没了,classpath 里的 application.properties 都不会被加载。实测:
text
>>> foo = null # classpath:/application.properties 里的值读不到了
>>> baz = null # application.yml 一样读不到
>>> ext = from-extra # 只有 file:./extra/application.properties 生效
而且这两个属性不能写在 application.properties 里,写在那里不会生效:
text
>>> ext = null # application.properties 里写了 spring.config.additional-location=file:./extra/,没读
>>> ext = from-extra # 改成命令行 --spring.config.additional-location=file:./extra/ 传,生效
因为"去哪儿找配置文件"这件事必须在读配置文件之前就定下来,它只能来自比配置文件更高优先级的来源:命令行参数、系统属性、环境变量。spring.config.name 同理,想改名只能从这几处改。
spring.config.import
spring.config.import 可以把任意文件引进来,写法和优先级是反直觉的:
properties
# application.properties
spring.config.import=optional:file:./custom.properties
text
Config resource 'file [custom.properties]' via location 'optional:file:./custom.properties'
Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'
被 import 进来的 custom.properties 排在引用它的文件上面 ,也就是说它优先级更高。optional: 前缀表示文件不存在也不报错,不加的话找不到文件直接启动失败:
text
Config data resource 'file [no-such-file.properties]' via location 'file:./no-such-file.properties' does not exist
Action: Check that the value 'file:./no-such-file.properties' at class path resource
[application.properties] - 2:22 is correct, or prefix it with 'optional:'
configtree: 是另一种写法,专门读 K8s 那种一个 key 一个文件的目录:
properties
spring.config.import=configtree:/etc/config/
list 是整体替换
properties
# application.properties
mylist[0]=a1
mylist[1]=a2
mylist[2]=a3
properties
# application-dev.properties
mylist[0]=b1
激活 dev 之后绑出来的结果是:
text
>>> mylist = [b1]
不是 [b1, a2, a3]。列表类型的属性,高优先级来源会把整个列表替换掉 ,@ConfigurationProperties 里的 List、Map 都是这个行为。想让基础配置保留一部分元素,只能把整个列表在同一个地方写全。
.yml 里的多文档
一个 .yml 文件里可以用 --- 分成多段,配合 spring.config.activate.on-profile 做到"一个文件管多个环境":
yaml
server:
port: 8080
---
spring:
config:
activate:
on-profile: dev
server:
port: 8081
这里用的是 spring.config.activate.on-profile,不是 spring.profiles.active。后者只有一种用法,就是指定这次要激活哪些 profile,写在 application.properties、命令行或者环境变量里。
spring.factories 不是配置文件
META-INF/spring.factories(2.7 之前)和 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(2.7 之后)名字里带 spring、也在 META-INF 下,经常被当成配置文件。它们是框架自己的注册表 ,用来登记自动配置类、EnvironmentPostProcessor、PropertySourceLoader 这些东西,和 application.properties 完全不是一回事,也不参与上面那套优先级。