Spring Boot 配置体系到底怎么加载的?从 application.yml 到 @Conditional 的全链路拆解

Spring Boot 配置体系到底怎么加载的?从 application.yml 到 @Conditional 的全链路拆解

每次写 @Value("${xxx}") 的时候,有没有想过:Spring Boot 到底是怎么把 YAML 里的值绑到字段上的?多环境配置的优先级谁高谁低?@ConditionalOnMissingBean 到底在什么时候起作用?本文从源码层面一次性讲透。


一、从一个问题开始

java 复制代码
@Value("${app.timeout:3000}")
private long timeout;

这三行代码背后,Spring Boot 做了这些事:

  1. 找到所有配置源(YAML、Properties、环境变量......)
  2. 按优先级合并,高优先级覆盖低优先级
  3. 解析占位符 ${app.timeout:3000},走 PropertyResolver
  4. 把值注入到字段上

整个过程涉及 配置加载 → 属性绑定 → 条件装配 三个阶段,下面逐一拆解。


二、配置加载: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.ymlprofile 配置插入到默认配置前面,所以 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 为什么不生效"。

相关推荐
Bonnie_12151 小时前
08-JUC并发基础-AQS-补充共享锁
java·开发语言
Zane19941 小时前
线程池入门:7 大核心参数与 4 种拒绝策略
java·后端
Zldaisy3d1 小时前
连续纤维增材制造的机翼已飞上天,复材打印在低空飞行器上还需翻过几道坎?
java·前端·数据库
CoderYanger1 小时前
Java EE:9.JVM(课件内容-下篇)
java·jvm·程序人生·面试·职场和发展·java-ee·学习方法
我命由我123451 小时前
Kotlin 面向对象 - Kotlin 类变量与类方法
java·服务器·后端·java-ee·kotlin·android jetpack·android runtime
SimonKing1 小时前
开源免费+AI运维,Netcatty快速上手教程
java·后端·程序员
plainGeekDev1 小时前
Robolectric → 分层测试:测试策略重构
android·java·kotlin
plainGeekDev1 小时前
Instrumentation → Compose Testing
android·java·kotlin
snow@li1 小时前
MapStruct:高性能Bean映射框架全景深入梳理
java