Spring Boot 配置体系到底怎么加载的?从 application.yml 到 @Conditional 的全链路拆解
每次写
@Value("${xxx}")的时候,有没有想过:Spring Boot 到底是怎么把 YAML 里的值绑到字段上的?多环境配置的优先级谁高谁低?@ConditionalOnMissingBean到底在什么时候起作用?本文从源码层面一次性讲透。
一、从一个问题开始
java
@Value("${app.timeout:3000}")
private long timeout;
这三行代码背后,Spring Boot 做了这些事:
- 找到所有配置源(YAML、Properties、环境变量......)
- 按优先级合并,高优先级覆盖低优先级
- 解析占位符
${app.timeout:3000},走PropertyResolver - 把值注入到字段上
整个过程涉及 配置加载 → 属性绑定 → 条件装配 三个阶段,下面逐一拆解。
二、配置加载:17 个 PropertySource 的优先级
Spring Boot 启动时,ConfigFileApplicationListener 负责加载配置文件。所有配置最终汇入 Environment,内部维护一个 MutablePropertySources 列表,排在前面的优先级更高。
完整的优先级顺序(从高到低):
markdown
1. 命令行参数(--server.port=8081)
2. JNDI 属性
3. Java 系统属性(System.getProperties())
4. 操作系统环境变量
5. RandomValuePropertySource(random.*)
6. jar 包外 application-{profile}.yml
7. jar 包内 application-{profile}.yml
8. jar 包外 application.yml
9. jar 包内 application.yml
10. @PropertySource 标注的属性
11. 默认属性(SpringApplication.setDefaultProperties)
核心记忆:命令行 > 系统属性 > 环境变量 > 外部配置文件 > 内部配置文件
源码入口在 SpringApplication.run() → prepareEnvironment() → ConfigFileApplicationListener.postProcessEnvironment():
java
// ConfigFileApplicationListener 核心逻辑简化
void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
// 1. 加载默认属性
// 2. 加载 application.yml / application-{profile}.yml
// 3. 按优先级插入到 PropertySources 列表
}
2.1 多环境配置的加载时机
当激活了 spring.profiles.active=prod,Spring Boot 会额外加载 application-prod.yml,且其优先级高于默认配置:
yaml
# application.yml
app:
timeout: 3000
# application-prod.yml
app:
timeout: 10000 # 生产环境覆盖
加载顺序:先加载 application.yml,再加载 application-prod.yml,profile 配置插入到默认配置前面,所以 profile 的值生效。
2.2 外部配置的覆盖机制
jar 包外的配置文件优先于 jar 包内的,这是 Spring Boot 的设计哲学:运维人员可以通过外部配置覆盖开发时的默认值,无需重新打包。
arduino
project/
├── config/
│ └── application.yml ← 优先级最高(jar包外config目录)
├── application.yml ← 次之(jar包外根目录)
└── app.jar
└── BOOT-INF/classes/
└── application.yml ← 最低(jar包内)
三、属性绑定:从字符串到 Java 对象
配置值本质都是字符串,Spring Boot 需要把它们转换成目标类型。这个过程分两条路径:@Value 和 @ConfigurationProperties。
3.1 @Value 的绑定流程
kotlin
@Value("${app.timeout:3000}")
↓
PropertyResolver.resolveRequiredPlaceholders()
↓
PropertySourcesPropertyResolver.findProperty()
↓
遍历 PropertySources 列表,找到第一个匹配的值
↓
ConversionService 进行类型转换(String → long)
↓
AutowiredAnnotationBeanPostProcessor 注入到字段
@Value 的缺点:不支持松散绑定,属性名必须完全匹配。
yaml
app:
time-out: 3000 # kebab-case
java
@Value("${app.time-out}") // ✅ 必须完全匹配
@Value("${app.timeOut}") // ❌ 不行!
private long timeout;
3.2 @ConfigurationProperties 的绑定流程
@ConfigurationProperties 走的是完全不同的绑定路径:
kotlin
@ConfigurationProperties(prefix = "app")
↓
ConfigurationPropertiesBindingPostProcessor
↓
Binder.bind("app", target)
↓
遍历 PropertySources,使用松散绑定规则匹配
↓
通过 ConversionService + 自定义 Converter 转换类型
↓
设置到 Bean 属性上
松散绑定规则
Spring Boot 支持四种属性名格式,绑定时会自动规范化为 kebab-case 后比较:
| 属性文件写法 | 能否匹配 @ConfigurationProperties(prefix="app.timeout") |
|---|---|
app.timeout |
✅ 标准形式 |
app.time-out |
✅ kebab-case(推荐) |
app.timeOut |
✅ camelCase |
app.time_out |
✅ snake_case(环境变量常用) |
app.TIME_OUT |
✅ 全大写(环境变量) |
源码中规范化逻辑在 ConfigurationPropertyName:
java
// 简化:所有格式统一转为 kebab-case 比较
// appTimeout → app-timeout
// APP_TIMEOUT → app-timeout
// app_time_out → app-timeout
绑定 List / Map
yaml
app:
servers:
- host: 192.168.1.1
port: 8080
- host: 192.168.1.2
port: 8081
params:
key1: value1
key2: value2
java
@ConfigurationProperties(prefix = "app")
public class AppConfig {
private List<Server> servers;
private Map<String, String> params;
// getter/setter
}
Binder 内部会自动识别目标类型,递归绑定嵌套对象。
3.3 两种绑定方式对比
| 维度 | @Value | @ConfigurationProperties |
|---|---|---|
| 松散绑定 | ❌ | ✅ |
| SpEL 表达式 | ✅ | ❌ |
| 类型转换 | 基础类型 | 任意类型(含 List/Map/嵌套对象) |
| JSR-303 校验 | ❌ | ✅(配合 @Validated) |
| 适用场景 | 单个值注入 | 一组相关配置的批量绑定 |
四、条件装配:@Conditional 家族的底层机制
@ConfigurationProperties 让配置值进入 Bean,而 @Conditional 决定 Bean 是否该创建。两者配合构成了 Spring Boot "约定优于配置" 的核心。
4.1 常用条件注解一览
less
@ConditionalOnClass(DataSource.class) // classpath 中有此类
@ConditionalOnMissingBean(DataSource.class) // 容器中没有此 Bean
@ConditionalOnProperty("app.feature.enabled") // 配置项存在且为 true
@ConditionalOnWebApplication // 是 Web 应用
@ConditionalOnExpression("${feature.enabled}") // SpEL 表达式为 true
4.2 条件判断的执行时机
条件判断发生在 Bean 定义注册阶段,而不是 Bean 实例化阶段:
scss
SpringApplication.run()
→ prepareContext()
→ load():扫描 @Configuration 类
→ AnnotatedBeanDefinitionReader.registerBean()
→ ConditionEvaluator.shouldSkip()
→ 遍历 @Configuration 类上的 @Conditional
→ 调用 Condition.matches()
→ 返回 false → 跳过这个 BeanDefinition
→ 返回 true → 注册 BeanDefinition
→ refreshContext()
→ finishBeanFactoryInitialization()
→ 实例化所有非 lazy 的 Bean
关键源码在 ConditionEvaluator:
java
public boolean shouldSkip(AnnotatedTypeMetadata metadata) {
// 1. 获取所有 @Conditional 注解
List<Condition> conditions = collectConditions(metadata);
// 2. 逐个判断
for (Condition condition : conditions) {
if (!condition.matches(context, metadata)) {
return true; // 任一条件不满足,跳过
}
}
return false;
}
4.3 @ConditionalOnMissingBean 的陷阱
java
@Configuration
public class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource() {
return new HikariDataSource();
}
}
问题 :如果用户自定义了 DataSource,这个 Bean 不会创建。但前提是 自定义 Bean 的注册必须在此 AutoConfiguration 之前。
Bean 定义注册顺序:
markdown
1. @ComponentScan 扫描的用户自定义 Bean
2. @Import 导入的 Bean
3. spring.factories 中的 AutoConfiguration
所以用户 @Component 标注的 DataSource 一定先注册,@ConditionalOnMissingBean 判断时已经能看到了。但如果两个 AutoConfiguration 互相依赖,顺序就很重要。
Spring Boot 通过 @AutoConfigureBefore / @AutoConfigureAfter 控制顺序:
java
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
public class MyCustomDataSourceAutoConfiguration { ... }
4.4 @ConditionalOnProperty 的匹配逻辑
java
@Bean
@ConditionalOnProperty(name = "app.cache.enabled", havingValue = "true", matchIfMissing = false)
public CacheManager cacheManager() { ... }
| 配置值 | havingValue | matchIfMissing | 结果 |
|---|---|---|---|
true |
"true" |
false |
✅ 创建 |
false |
"true" |
false |
❌ 不创建 |
| 未配置 | "true" |
false |
❌ 不创建 |
| 未配置 | "true" |
true |
✅ 创建(默认开启) |
true |
"true" |
true |
✅ 创建 |
matchIfMissing = true 是 Spring Boot 实现"默认行为"的关键------配置不写也开,写了 false 才关。
五、配置变更监听:运行时能不能热刷新?
默认情况下,Spring Boot 的配置在启动时一次性加载,运行中修改 YAML 不会生效。但某些场景需要热刷新:
5.1 @RefreshScope(Spring Cloud)
java
@RestController
@RefreshScope
public class ConfigController {
@Value("${app.feature.enabled}")
private boolean featureEnabled;
}
调用 /actuator/refresh 端点后,@RefreshScope 标注的 Bean 会被销毁重建,下次访问时从配置中心重新拉取。
5.2 @ConfigurationProperties + @RefreshScope
java
@ConfigurationProperties(prefix = "app")
@RefreshScope
public class AppConfig { ... }
这种方式在 Spring Cloud 配合 Config Server / Nacos 时很常见。
5.3 自研方案:Environment 手动更新
不走 Spring Cloud,也可以直接操作 Environment:
java
@Component
public class ConfigRefresher {
@Autowired
private ConfigurableEnvironment environment;
public void updateProperty(String key, String value) {
MutablePropertySources sources = environment.getPropertySources();
Map<String, Object> map = new HashMap<>();
map.put(key, value);
// 插入到最高优先级
sources.addFirst(new MapPropertySource("dynamicConfig", map));
}
}
注意 :这种方式只对
@Value+@RefreshScope或@ConfigurationProperties+@RefreshScope生效,普通@Value不会自动刷新。
六、常见坑与排查指南
坑 1:YAML 的缩进错误导致配置丢失
yaml
app:
timeout: 3000
name: my-app
pool-size: 10 # ← 多了一个空格,pool-size 变成了 app 的兄弟属性
YAML 对缩进极其敏感,IDE 的 YAML 插件可以检测这类问题。
坑 2:@Value 注入为 null
java
@Component
public class MyBean {
@Value("${app.timeout}")
private long timeout; // ✅ 正确
}
public class MyBean {
@Value("${app.timeout}")
private long timeout; // ❌ new 出来的对象不走 Spring 容器,@Value 不生效
}
MyBean bean = new MyBean(); // timeout = 0
原因 :@Value 是由 AutowiredAnnotationBeanPostProcessor 在 Bean 创建阶段注入的,手动 new 的对象不经过容器。
坑 3:多 Profile 时配置合并的意外覆盖
yaml
# application.yml
app:
timeout: 3000
retry: 3
# application-prod.yml
app:
timeout: 10000
# retry 没写
激活 prod profile 后,app.retry 的值是 3(来自默认配置),不是 null。Spring Boot 对 profile 的处理是 追加覆盖,不是替换。
但如果是 Map 类型的属性:
yaml
# application.yml
app:
servers:
a: host1
b: host2
# application-prod.yml
app:
servers:
c: host3
最终 app.servers = {a: host1, b: host2, c: host3}------Map 是合并的。
七、全链路流程图
scss
┌─────────────────────────────────────────────────────────────┐
│ Spring Boot 启动 │
└───────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. 配置加载阶段 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Environment.create() │ │
│ │ → 命令行参数 → 系统属性 → 环境变量 → YAML → 默认值 │ │
│ │ → 按优先级插入 MutablePropertySources │ │
│ │ → 激活 Profile,追加 application-{profile}.yml │ │
│ └───────────────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. 条件装配阶段 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ AnnotatedBeanDefinitionReader.registerBean() │ │
│ │ → ConditionEvaluator.shouldSkip() │ │
│ │ → @ConditionalOnClass / @ConditionalOnMissingBean │ │
│ │ → @ConditionalOnProperty / @ConditionalOnWebApp │ │
│ │ → 不满足 → 跳过 BeanDefinition │ │
│ └───────────────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. 属性绑定阶段 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ @Value: PropertyResolver → 占位符解析 → 类型转换 │ │
│ │ @ConfigurationProperties: │ │
│ │ → Binder.bind() → 松散绑定匹配 → 递归绑定嵌套对象 │ │
│ │ → ConversionService 类型转换 → JSR-303 校验 │ │
│ └───────────────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. Bean 实例化阶段 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ finishBeanFactoryInitialization() │ │
│ │ → 遍历所有 BeanDefinition │ │
│ │ → 实例化 → 属性注入 → 初始化回调 │ │
│ │ → 配置值已就绪,Bean 可用 │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
八、总结
| 阶段 | 核心类 | 关键机制 |
|---|---|---|
| 配置加载 | ConfigFileApplicationListener |
17 个 PropertySource 按优先级排列 |
| 属性绑定(@Value) | PropertyResolver |
占位符解析 + 类型转换 |
| 属性绑定(@ConfigurationProperties) | Binder |
松散绑定 + 递归嵌套 |
| 条件装配 | ConditionEvaluator |
BeanDefinition 注册前判断 |
| 运行时刷新 | @RefreshScope |
Bean 销毁重建 |
掌握这条链路,就能回答面试中关于 Spring Boot 配置的几乎所有问题------从"配置优先级谁高"到"@ConditionalOnMissingBean 为什么不生效"。