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

171 阅读4分钟

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

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


一、从一个问题开始

@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 列表,排在前面的优先级更高。

完整的优先级顺序(从高到低):

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():

// 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,且其优先级高于默认配置:

# application.yml
app:
  timeout: 3000

# application-prod.yml
app:
  timeout: 10000  # 生产环境覆盖

加载顺序:先加载 application.yml,再加载 application-prod.yml,profile 配置插入到默认配置前面,所以 profile 的值生效。

2.2 外部配置的覆盖机制

jar 包外的配置文件优先于 jar 包内的,这是 Spring Boot 的设计哲学:运维人员可以通过外部配置覆盖开发时的默认值,无需重新打包。

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 的绑定流程

@Value("${app.timeout:3000}")
        ↓
PropertyResolver.resolveRequiredPlaceholders()
        ↓
PropertySourcesPropertyResolver.findProperty()
        ↓
遍历 PropertySources 列表,找到第一个匹配的值
        ↓
ConversionService 进行类型转换(String → long)
        ↓
AutowiredAnnotationBeanPostProcessor 注入到字段

@Value 的缺点:不支持松散绑定,属性名必须完全匹配。

app:
  time-out: 3000  # kebab-case
@Value("${app.time-out}")  // ✅ 必须完全匹配
@Value("${app.timeOut}")   // ❌ 不行!
private long timeout;

3.2 @ConfigurationProperties 的绑定流程

@ConfigurationProperties 走的是完全不同的绑定路径:

@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:

// 简化:所有格式统一转为 kebab-case 比较
// appTimeout → app-timeout
// APP_TIMEOUT → app-timeout
// app_time_out → app-timeout
绑定 List / Map
app:
  servers:
    - host: 192.168.1.1
      port: 8080
    - host: 192.168.1.2
      port: 8081
  params:
    key1: value1
    key2: value2
@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 常用条件注解一览

@ConditionalOnClass(DataSource.class)        // classpath 中有此类
@ConditionalOnMissingBean(DataSource.class)   // 容器中没有此 Bean
@ConditionalOnProperty("app.feature.enabled")  // 配置项存在且为 true
@ConditionalOnWebApplication                   // 是 Web 应用
@ConditionalOnExpression("${feature.enabled}")  // SpEL 表达式为 true

4.2 条件判断的执行时机

条件判断发生在 Bean 定义注册阶段,而不是 Bean 实例化阶段:

SpringApplication.run()
  → prepareContext()
    → load():扫描 @Configuration 类
      → AnnotatedBeanDefinitionReader.registerBean()
        → ConditionEvaluator.shouldSkip()
          → 遍历 @Configuration 类上的 @Conditional
            → 调用 Condition.matches()
              → 返回 false → 跳过这个 BeanDefinition
              → 返回 true → 注册 BeanDefinition
  → refreshContext()
    → finishBeanFactoryInitialization()
      → 实例化所有非 lazy 的 Bean

关键源码在 ConditionEvaluator:

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 的陷阱

@Configuration
public class MyAutoConfiguration {
    
    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        return new HikariDataSource();
    }
}

问题:如果用户自定义了 DataSource,这个 Bean 不会创建。但前提是 自定义 Bean 的注册必须在此 AutoConfiguration 之前。

Bean 定义注册顺序:

1. @ComponentScan 扫描的用户自定义 Bean
2. @Import 导入的 Bean
3. spring.factories 中的 AutoConfiguration

所以用户 @Component 标注的 DataSource 一定先注册,@ConditionalOnMissingBean 判断时已经能看到了。但如果两个 AutoConfiguration 互相依赖,顺序就很重要。

Spring Boot 通过 @AutoConfigureBefore / @AutoConfigureAfter 控制顺序:

@AutoConfigureBefore(DataSourceAutoConfiguration.class)
public class MyCustomDataSourceAutoConfiguration { ... }

4.4 @ConditionalOnProperty 的匹配逻辑

@Bean
@ConditionalOnProperty(name = "app.cache.enabled", havingValue = "true", matchIfMissing = false)
public CacheManager cacheManager() { ... }
配置值havingValuematchIfMissing结果
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)

@RestController
@RefreshScope
public class ConfigController {
    
    @Value("${app.feature.enabled}")
    private boolean featureEnabled;
}

调用 /actuator/refresh 端点后,@RefreshScope 标注的 Bean 会被销毁重建,下次访问时从配置中心重新拉取。

5.2 @ConfigurationProperties + @RefreshScope

@ConfigurationProperties(prefix = "app")
@RefreshScope
public class AppConfig { ... }

这种方式在 Spring Cloud 配合 Config Server / Nacos 时很常见。

5.3 自研方案:Environment 手动更新

不走 Spring Cloud,也可以直接操作 Environment:

@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 的缩进错误导致配置丢失

app:
  timeout: 3000
  name: my-app
   pool-size: 10  # ← 多了一个空格,pool-size 变成了 app 的兄弟属性

YAML 对缩进极其敏感,IDE 的 YAML 插件可以检测这类问题。

坑 2:@Value 注入为 null

@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 时配置合并的意外覆盖

# application.yml
app:
  timeout: 3000
  retry: 3

# application-prod.yml
app:
  timeout: 10000
  # retry 没写

激活 prod profile 后,app.retry 的值是 3(来自默认配置),不是 null。Spring Boot 对 profile 的处理是 追加覆盖,不是替换。

但如果是 Map 类型的属性:

# application.yml
app:
  servers:
    a: host1
    b: host2

# application-prod.yml
app:
  servers:
    c: host3

最终 app.servers = {a: host1, b: host2, c: host3}——Map 是合并的。


七、全链路流程图

┌─────────────────────────────────────────────────────────────┐
│                     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 可用                             │   │
│  └───────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

八、总结

阶段核心类关键机制
配置加载ConfigFileApplicationListener17 个 PropertySource 按优先级排列
属性绑定(@Value)PropertyResolver占位符解析 + 类型转换
属性绑定(@ConfigurationProperties)Binder松散绑定 + 递归嵌套
条件装配ConditionEvaluatorBeanDefinition 注册前判断
运行时刷新@RefreshScopeBean 销毁重建

掌握这条链路,就能回答面试中关于 Spring Boot 配置的几乎所有问题——从"配置优先级谁高"到"@ConditionalOnMissingBean 为什么不生效"。