主题
Spring Boot 配置体系:配置优先级、Profile 与外部化配置
提出问题
一个 Spring Boot 项目,application.yml 里配了 server.port=8080,启动时加了 --server.port=9090,结果端口是 9090 还是 8080?答案是 9090,因为命令行参数优先级高于配置文件。但如果还设置了环境变量 SERVER_PORT=7070 呢?优先级又变了。
这是 Spring Boot 配置体系中最基础也最容易踩坑的问题——配置来源的优先级。生产环境里,开发在 application.yml 里配了 spring.datasource.url,运维在 application-prod.yml 里覆盖了同一个值,但部署时环境变量 SPRING_DATASOURCE_URL 又被 CI/CD 脚本设置了。三个地方写了同一个配置,最终哪个生效?
再往深了说:@Value 和 @ConfigurationProperties 怎么选?matchIfMissing 默认 false 的坑还记得吗?Profile 多环境怎么做最佳实践?YAML 里的 List 和 Map 怎么绑定?配置不生效时怎么排查?
这些不是问你会不会配,是问你能不能把 Spring Boot 的配置加载全链路说清楚,以及遇到配置覆盖、不回读、绑定失败时怎么排查。
分析问题
配置源的 17 级优先级(从高到低)
Spring Boot 的 Environment 抽象将所有配置源组织成一个 PropertySource 链,按照优先级排序。优先级高的覆盖优先级低的。
完整优先级(从高到低):
1. 命令行参数(--xxx=yyy)
2. 来自 java:comp/env 的 JNDI 属性
3. JVM 系统属性(System.getProperties())
4. 操作系统环境变量
5. RandomValuePropertySource(random.*)
6. application-{profile}.yml(外部 jar 包外)
7. application-{profile}.yml(jar 包内)
8. application.yml(外部 jar 包外)
9. application.yml(jar 包内)
10. @Configuration 类上的 @PropertySource
11. 默认属性(SpringApplication.setDefaultProperties() 设置)这个顺序在实际中经常被误解。一个典型例子:application.yml 里写了 server.port=8080,环境变量 SERVER_PORT=9090,命令行 --server.port=7070,最终生效的是 7070(命令行最高)。但如果不加命令行,环境变量 9090 会覆盖 yml 里的 8080。
为什么环境变量要用大写加下划线? Spring Boot 的 SystemEnvironmentPropertySource 在解析环境变量时,会宽松匹配:SERVER_PORT 可以匹配 server.port 和 server_port。这是 Java 环境变量命名惯例(大写、下划线)和 Spring 属性名(小写、点号)之间的转换规则。环境变量中不能有 .,所以 Spring Boot 用 _ 替代 .,并把值全部转为小写匹配。
注意:RelaxedBinding(宽松绑定)在 Spring Boot 2.0+ 已经被 RelaxedNames 替代,属性名匹配规则有变化:环境变量 SPRING_DATASOURCE_URL 可以匹配 spring.datasource.url,但 SPRING_DATASOURCE_URL 不能匹配 spring.datasource.url 如果配置源是 YAML——因为 YAML 的键名是大小写敏感的。
配置加载顺序的完整流程
Spring Boot 应用启动时,配置加载的具体流程:
SpringApplication.run()
│
├─ [1] 初始化 SpringApplication 对象
│ └─ 读取 spring.factories 中的 ApplicationContextInitializer 和 ApplicationListener
│
├─ [2] 调用 run() 方法
│ ├─ 创建并启动 StopWatch
│ ├─ 创建 DefaultBootstrapContext(3.x 新特性,用于配置加载的前置准备)
│ ├─ 配置 Headless 模式
│ ├─ 启动所有 SpringApplicationRunListener
│ │
│ ├─ [3] prepareEnvironment()
│ │ ├─ 创建 Environment 对象(根据 web 类型选择 StandardServletEnvironment 或 StandardEnvironment)
│ │ ├─ 配置 Environment:设置 activeProfiles、ConversionService
│ │ ├─ 配置 PropertySources:将命令行参数、JNDI、系统属性、环境变量依次加入环境
│ │ ├─ 调用所有 EnvironmentPostProcessor
│ │ │ └─ 重要:ConfigDataEnvironmentPostProcessor 在这里加载 application.yml/application.properties
│ │ │ ├─ 按优先级加载外部配置 → 外部 application-{profile}.yml
│ │ │ ├─ 加载外部 application.yml
│ │ │ ├─ 加载内部配置 → 内部 application-{profile}.yml
│ │ │ ├─ 加载内部 application.yml
│ │ │ └─ 按优先级顺序合并到 PropertySources
│ │ └─ 绑定到 SpringApplication 的各个属性(如 mainApplicationClass、listeners 等)
│ │
│ ├─ [4] printBanner()(可选)
│ │
│ ├─ [5] createApplicationContext()
│ │ └─ 创建 ApplicationContext 实例(如 AnnotationConfigServletWebApplicationContext)
│ │
│ ├─ [6] prepareContext()
│ │ ├─ 设置 context 的 Environment(从 prepareEnvironment 得到的完整配置环境)
│ │ ├─ 执行 ApplicationContextInitializer(包括从 spring.factories 加载的)
│ │ ├─ 发送 ApplicationPreparedEvent
│ │ ├─ 注册 springApplicationArguments(用于 ${} 占位符引用命令行参数)
│ │ └─ 注册所有 spring boot 的 Banner、BeanFactoryPostProcessor 等
│ │
│ ├─ [7] refreshContext()
│ │ └─ AbstractApplicationContext.refresh()
│ │ └─ invokeBeanFactoryPostProcessors()
│ │ └─ PropertySourcesPlaceholderConfigurer.postProcessBeanFactory()
│ │ └─ 解析 ${...} 占位符,从 Environment 中取值
│ │
│ └─ [8] afterRefresh()
│ ├─ 调用 CommandLineRunner 和 ApplicationRunner
│ └─ 发送 ApplicationReadyEvent关键细节:prepareEnvironment() 阶段创建的 Environment 已经包含了所有配置源,但此时 application.yml 中的 spring.profiles.active 还没有被解析——Profile 的激活是在 ConfigDataEnvironmentPostProcessor 内部处理的,它会在加载配置时递归解析 spring.profiles.active 和 spring.profiles.include,然后加载对应的 Profile 文件。
同名字段覆盖规则
多个配置源同名时,"谁覆盖谁"取决于 PropertySource 在链中的位置:
java
// 打印当前 PropertySource 链
@SpringBootApplication
public class DemoApplication implements ApplicationRunner {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Override
public void run(ApplicationArguments args) {
ConfigurableEnvironment env = (ConfigurableEnvironment) ctx.getEnvironment();
for (PropertySource<?> ps : env.getPropertySources()) {
System.out.println(ps.getName() + " : " + ps.getSource());
}
}
}输出示例:
commandLineArgs: {server.port=9090}
servletConfigInitParams: {}
servletContextInitParams: {}
systemProperties: {java.runtime.name=..., server.port=8080}
systemEnvironment: {SERVER_PORT=9090, ...}
...
Config resource 'class path resource [application.yml]' via location 'class path resource [application.yml]': {server.port=8080, ...}
Config resource 'class path resource [application-prod.yml]' via location 'class path resource [application-prod.yml]': {server.port=7070}PropertySources 是一个有序列表,遍历时返回第一个匹配的值。所以排在前面的覆盖后面的。env.getPropertySources().addFirst() 可以在队列头部插入,实现最高优先级。
同 Profile 内的配置覆盖
Profile 配置文件的加载顺序比普通配置更复杂。如果一个配置在 application.yml 和 application-prod.yml 中都出现了,最终以 application-prod.yml 为准——因为 Profile 文件的优先级高于默认文件。
Spring Boot 2.4+ 对配置加载做了重构,引入了 ConfigDataLocation 概念。核心变化:
spring.profiles被废弃,改用spring.config.activate.on-profile- 支持
spring.profiles.group分组激活 - 支持
spring.profiles.include在当前 Profile 基础上包含其他 Profile - 通过
spring.config.import导入外部配置(如configtree:/etc/config/)
示例:
yaml
# application.yml
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:mysql://prod-db:3306/mydb
# 等同于以前的 application-prod.yml(但 on-profile 写在文件内部)多环境配置文件组织方式对比:
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
多文件 application-{profile}.yml | 清晰分离,每个环境独立文件 | 文件数量多,公共配置重复 | 环境差异大 |
| 多文档块(---) | 单文件,配量少时简洁 | 文件一长就难维护 | 小型项目 |
| 单文件 + 外部化 | 配置中心管理,运行时动态 | 依赖外部系统 | 微服务 |
spring.config.import | 可组合多个配置源 | 文件关系隐式,新人难理解 | 中大型项目 |
@Value vs @ConfigurationProperties
两个注解都能从配置文件取值,但侧重点不同。
| 维度 | @Value | @ConfigurationProperties |
|---|---|---|
| 绑定方式 | 逐个字段,字符串直接注入 | 批量绑定到 POJO 类 |
| 类型安全 | 弱,需要手动转换(SpEL 或 Converter) | 强,自动类型转换 + 校验(@Validated) |
| 松散绑定 | 不支持(my-name 不能匹配 myName) | 支持(my-name / myName / my_name 均可) |
| SpEL 支持 | 支持 ${...} 和 #{...} 表达式 | 不支持 SpEL |
| 复杂类型 | 不支持 List/Map 的深度绑定 | 支持 List、Map、嵌套对象绑定 |
| 校验 | 手动校验 | @Validated 自动校验 |
| 元数据支持 | 无 | spring-boot-configuration-processor 生成 IDE 提示 |
| 刷新 | 默认缓存,不刷新 | @RefreshScope 支持运行时刷新 |
适用场景判断:
- 单个配置项,不需要校验,不需要复杂类型 →
@Value - 一组关联配置,需要类型安全、校验、IDE 提示 →
@ConfigurationProperties - 配置需要运行时刷新(配合 Spring Cloud 配置中心) →
@ConfigurationProperties+@RefreshScope - 配置值是 SpEL 表达式计算结果 →
@Value
坑:@Value 在 @Configuration 类中解析时间比 @ConfigurationProperties 晚。如果 @Bean 方法里用了 @Value 参数,而这个参数依赖另一个配置文件,可能出现解析不到的值。@ConfigurationProperties 没有这个问题。
java
// @Value 方式:逐个注入,不推荐用于一组配置
@Component
public class DatabaseConfig {
@Value("${app.database.url}")
private String url;
@Value("${app.database.username}")
private String username;
@Value("${app.database.password}")
private String password;
}
// @ConfigurationProperties 方式:批量绑定,推荐
@ConfigurationProperties(prefix = "app.database")
@Component
public class DatabaseProperties {
private String url;
private String username;
private String password;
private Pool pool = new Pool(); // 嵌套对象
// getters / setters
public static class Pool {
private int maxSize = 10;
private int minIdle = 2;
// getters / setters
}
}配置嵌套绑定与复杂类型
Spring Boot 支持对 YAML 中的嵌套结构进行绑定:
yaml
app:
database:
url: jdbc:mysql://localhost:3306/db
pool:
max-size: 20
min-idle: 5
connections:
- host: primary
port: 3306
- host: secondary
port: 3307java
@ConfigurationProperties(prefix = "app.database")
public class DatabaseProperties {
private String url;
private Pool pool = new Pool();
private List<ConnectionInfo> connections = new ArrayList<>();
// getters/setters
public static class Pool {
private int maxSize;
private int minIdle;
// getters/setters
}
public static class ConnectionInfo {
private String host;
private int port;
// getters/setters
}
}注意事项:
- 嵌套类必须是
public static,否则 Spring 无法实例化 - 必须提供 getter/setter(Spring Boot 3.x 可以用
@ConfigurationProperties的useRecords参数配合 record 类) - 如果嵌套对象没有默认值,必须初始化(如
new Pool()),否则 Spring 不会自动创建实例,字段为 null - List 类型绑定:如果
application.yml中定义了 List,可以为空([]),但不会 null
常见配置踩坑
1. YAML 缩进错误
# 错误:缩进不一致
server:
port: 8080
port: 9090 # 多了一个空格,变成了另一个属性
# 正确
server:
port: 8080YAML 的缩进一旦错乱,不会报错,而是解析成你完全想不到的键值结构。server.port 直接变成 null,新属性 server.port(注意前面有个空格)变成了 server..port。排查了半小时才发现是多了一个空格。
2. List 绑定失败
yaml
# 这样写,@ConfigurationProperties 的 List 字段会绑定失败
my:
items: "a,b,c"
# 正确写法
my:
items:
- a
- b
- c"a,b,c" 是字符串,不是 List。Spring Boot 的 DefaultConversionService 不会自动将逗号分割的字符串转为 List。需要写 YAML 的数组语法。
3. @Value 绑定 List
java
// 这样不行——@Value 只支持简单类型
@Value("${my.items}")
private List<String> items;
// 可以这样——用 SpEL 拆解
@Value("#{'${my.items}'.split(',')}")
private List<String> items;但 SpEL 有个坑:如果 my.items 配置值为空,split(',') 会返回 [""](一个空字符串的 List),而不是空 List。正确做法是加默认值:"${my.items:}",并在 SpEL 中判断空字符串。
4. 配置不生效的排查路径
bash
# 查看所有配置源和值
curl http://localhost:8080/actuator/env
# 查看某个具体配置项
curl http://localhost:8080/actuator/env/server.port
# 查看配置来源
curl http://localhost:8080/actuator/env/server.port | jq '.propertySources'Actuator /env 端点的输出结构:
json
{
"activeProfiles": ["prod"],
"propertySources": [
{
"name": "server.ports",
"properties": {
"local.server.port": {
"value": 8080,
"origin": "Managed Source"
}
}
},
{
"name": "application-prod.yml",
"properties": {
"server.port": {
"value": "9090",
"origin": "class path resource [application-prod.yml]:3:12"
}
}
},
{
"name": "application.yml",
"properties": {
"server.port": {
"value": "8080",
"origin": "class path resource [application.yml]:2:9"
}
}
}
]
}Origin 字段:class path resource [application-prod.yml]:3:12 直接告诉你这个值来自 application-prod.yml 第 3 行第 12 个字符。排查配置覆盖时,这个信息比任何日志都有用。
手动排查路径(没有 Actuator 时):
java
// 注入 Environment 手动查看
@Component
public class ConfigDebugger implements ApplicationRunner {
@Autowired
private Environment env;
@Override
public void run(ApplicationArguments args) {
// 查看某个属性及其来源
System.out.println("server.port = " + env.getProperty("server.port"));
// 查看所有 active profiles
String[] profiles = env.getActiveProfiles();
System.out.println("Active profiles: " + Arrays.toString(profiles));
// 查看所有 PropertySource
if (env instanceof ConfigurableEnvironment ce) {
for (PropertySource<?> ps : ce.getPropertySources()) {
System.out.println(ps.getName() + " -> " + ps.getSource());
}
}
}
}5. @ConditionalOnProperty 的 matchIfMissing 陷阱
前面在条件注解那篇讲过,这里再提一下:
java
@Bean
@ConditionalOnProperty(name = "my.feature.enabled", havingValue = "true")
public MyService myService() {
return new MyService();
}如果 application.yml 里没有配 my.feature.enabled,matchIfMissing 默认 false,这个 Bean 不会创建。正确的做法是明确 matchIfMissing = true 或者确保配置存在。
配置热更新:RefreshScope 与配置中心
Spring Cloud 提供了 @RefreshScope 注解,配合配置中心(Nacos、Apollo、Spring Cloud Config)实现运行时配置刷新:
java
@RefreshScope
@ConfigurationProperties(prefix = "app.feature")
@Component
public class FeatureFlags {
private boolean newPayment = false;
private boolean newCheckout = false;
// getters/setters
}@RefreshScope 的原理:被标注的 Bean 在刷新时会重新创建。@RefreshScope 是一个特殊的 @Scope("refresh"),RefreshScope 实现了 ApplicationContextAware,收到 RefreshScopeRefreshedEvent 时清除 refresh scope 的 Bean 缓存,下次 getBean 时重建。
注意:@RefreshScope 不适用于 @Configuration 类中的 @Bean 方法——@Configuration 类本身是单例,不受 @RefreshScope 影响。要把 @RefreshScope 放在 @Bean 方法上,而不是 @Configuration 类上。
不同配置中心的刷新机制:
| 配置中心 | 刷新机制 | 时效性 | 侵入性 |
|---|---|---|---|
| Spring Cloud Config + Bus | 发送 RefreshRemoteApplicationEvent,广播刷新 | 秒级 | 低(依赖 Bus) |
| Nacos | 长轮询检测配置变更,自动推送 | 秒级 | 低(spring-cloud-starter-alibaba-nacos-config 内置) |
| Apollo | HTTP 长连接推送 | 准实时 | 低(Apollo 客户端自动处理) |
| 手动刷新 | POST /actuator/refresh | 手动触发 | 无 |
Nacos 配置热更新的实现流程:
- 客户端启动时,向 Nacos Server 注册
ConfigService并建立长轮询连接 - 服务端接到客户端的长轮询请求,对比本地配置的 MD5 值
- 如果配置变更,服务端立即返回变更的数据 ID 和 group
- 客户端收到响应后,调用
ConfigService.getConfig()获取最新配置 - 触发
RefreshScope.refreshAll()或EnvironmentChangeEvent发布 - 被
@RefreshScope标注的 Bean 重新创建,获取新配置值
Nacos 的配置热更新走的是长轮询,不是 WebSocket 长连接。长轮询连接数少(每个客户端只维持一个 HTTP 连接)、兼容性好、不受防火墙限制,但延迟比 WebSocket 稍高(约 1-3 秒)。
高级:spring.config.import 与外部配置
Spring Boot 2.4+ 的 spring.config.import 支持从外部位置加载配置:
yaml
# 从文件系统加载
spring:
config:
import: file:/etc/config/myapp.yml
# 从 classpath 加载
spring:
config:
import: classpath:extra-config.yml
# 从配置中心加载(需要对应的 starter)
spring:
config:
import: nacos:myapp.yml?group=DEFAULT_GROUP&refreshEnabled=truespring.config.import 的加载顺序:被 import 的配置按声明顺序依次加载,排在后面的优先级更高。如果 spring.config.import 中的配置和 application.yml 冲突,import 的配置优先。
spring.config.import 的两种模式:
- optional:结尾加
optional:前缀,表示文件不存在时不会报错 - 默认:不加
optional:,文件不存在则启动失败
yaml
spring:
config:
import: optional:file:/etc/config/myapp.ymlProfile 激活与多环境最佳实践
Profile 激活方式(按优先级从高到低):
- 命令行:
--spring.profiles.active=prod - 环境变量:
SPRING_PROFILES_ACTIVE=prod application.yml中的spring.profiles.active属性SpringApplication.setAdditionalProfiles()
Spring Boot 2.4+ 的 Profile 分组:
yaml
spring:
profiles:
group:
dev: dev,dev-db,dev-mq
prod: prod,prod-db,prod-mq激活 dev Profile 时,dev、dev-db、dev-mq 三个 Profile 同时激活。
多环境配置的最佳实践:
- 公共配置放
application.yml,环境差异放application-{profile}.yml - 敏感配置(密码、密钥)不要放配置文件,用环境变量或配置中心
- 使用
spring.profiles.group组合细粒度 Profile - 配置校验:
@ConfigurationProperties+@Validated+hibernate-validator - 配置变更记录:使用 Git 管理配置文件的版本历史
yaml
# application.yml(公共配置)
server:
port: 8080
spring:
jpa:
hibernate:
ddl-auto: none
show-sql: false
# application-dev.yml(开发环境,覆盖公共配置)
server:
port: 8081
spring:
jpa:
show-sql: true
# application-prod.yml(生产环境)
spring:
datasource:
url: ${DB_URL} # 敏感信息用环境变量
username: ${DB_USER}
password: ${DB_PASS}总结
- Spring Boot 配置源有 17 级优先级,从高到低:命令行 > JVM 属性 > 环境变量 > Profile 文件 > 默认配置
- 同名字段覆盖规则:PropertySource 链中排在前面的覆盖后面的,
env.getPropertySources().addFirst()可以动态插入最高优先级 @Value适用于单个简单配置项,@ConfigurationProperties适用于一组关联配置,需要类型安全和校验的场景- 配置嵌套绑定时,嵌套类必须是
public static,必须有 getter/setter,嵌套对象必须初始化 - 排查配置不生效用 Actuator 的
/env端点,看 Origin 字段确定值来源;没有 Actuator 时注入Environment手动打印 - Profile 激活优先级:命令行 > 环境变量 > 配置文件;2.4+ 支持分组和
spring.config.import - 配置热更新依赖
@RefreshScope+ 配置中心,不同配置中心的刷新机制不同(Nacos 长轮询、Apollo 长连接推送) - 敏感配置永远不要放配置文件,用环境变量或配置中心统一管理
参考:Spring Boot Reference Documentation — Externalized Configuration · Spring Boot Actuator /env 端点 · Nacos Spring Cloud 配置管理文档 · Spring Cloud @RefreshScope 源码