Spring Boot 配置体系到底怎么加载的?从 application.yml 到 @Conditional 的全链路拆解
Spring Boot 配置体系到底怎么加载的从 application.yml 到 Conditional 的全链路拆解每次写Value(${xxx})的时候有没有想过Spring Boot 到底是怎么把 YAML 里的值绑到字段上的多环境配置的优先级谁高谁低ConditionalOnMissingBean到底在什么时候起作用本文从源码层面一次性讲透。一、从一个问题开始Value(${app.timeout:3000})privatelongtimeout;这三行代码背后Spring Boot 做了这些事找到所有配置源YAML、Properties、环境变量……按优先级合并高优先级覆盖低优先级解析占位符${app.timeout:3000}走PropertyResolver把值注入到字段上整个过程涉及配置加载 → 属性绑定 → 条件装配三个阶段下面逐一拆解。二、配置加载17 个 PropertySource 的优先级Spring Boot 启动时ConfigFileApplicationListener负责加载配置文件。所有配置最终汇入Environment内部维护一个MutablePropertySources列表排在前面的优先级更高。完整的优先级顺序从高到低1. 命令行参数--server.port8081 2. JNDI 属性 3. Java 系统属性System.getProperties() 4. 操作系统环境变量 5. RandomValuePropertySourcerandom.* 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 核心逻辑简化voidpostProcessEnvironment(ConfigurableEnvironmentenvironment,SpringApplicationapplication){// 1. 加载默认属性// 2. 加载 application.yml / application-{profile}.yml// 3. 按优先级插入到 PropertySources 列表}2.1 多环境配置的加载时机当激活了spring.profiles.activeprodSpring Boot 会额外加载application-prod.yml且其优先级高于默认配置# application.ymlapp:timeout:3000# application-prod.ymlapp:timeout:10000# 生产环境覆盖加载顺序先加载application.yml再加载application-prod.ymlprofile 配置插入到默认配置前面所以 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-caseValue(${app.time-out})// ✅ 必须完全匹配Value(${app.timeOut})// ❌ 不行privatelongtimeout;3.2 ConfigurationProperties 的绑定流程ConfigurationProperties走的是完全不同的绑定路径ConfigurationProperties(prefix app) ↓ ConfigurationPropertiesBindingPostProcessor ↓ Binder.bind(app, target) ↓ 遍历 PropertySources使用松散绑定规则匹配 ↓ 通过 ConversionService 自定义 Converter 转换类型 ↓ 设置到 Bean 属性上松散绑定规则Spring Boot 支持四种属性名格式绑定时会自动规范化为 kebab-case 后比较属性文件写法能否匹配ConfigurationProperties(prefixapp.timeout)app.timeout✅ 标准形式app.time-out✅ kebab-case推荐app.timeOut✅ camelCaseapp.time_out✅ snake_case环境变量常用app.TIME_OUT✅ 全大写环境变量源码中规范化逻辑在ConfigurationPropertyName// 简化所有格式统一转为 kebab-case 比较// appTimeout → app-timeout// APP_TIMEOUT → app-timeout// app_time_out → app-timeout绑定 List / Mapapp:servers:-host:192.168.1.1port:8080-host:192.168.1.2port:8081params:key1:value1key2:value2ConfigurationProperties(prefixapp)publicclassAppConfig{privateListServerservers;privateMapString,Stringparams;// getter/setter}Binder内部会自动识别目标类型递归绑定嵌套对象。3.3 两种绑定方式对比维度ValueConfigurationProperties松散绑定❌✅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 表达式为 true4.2 条件判断的执行时机条件判断发生在Bean 定义注册阶段而不是 Bean 实例化阶段SpringApplication.run() → prepareContext() → load()扫描 Configuration 类 → AnnotatedBeanDefinitionReader.registerBean() → ConditionEvaluator.shouldSkip() → 遍历 Configuration 类上的 Conditional → 调用 Condition.matches() → 返回 false → 跳过这个 BeanDefinition → 返回 true → 注册 BeanDefinition → refreshContext() → finishBeanFactoryInitialization() → 实例化所有非 lazy 的 Bean关键源码在ConditionEvaluatorpublicbooleanshouldSkip(AnnotatedTypeMetadatametadata){// 1. 获取所有 Conditional 注解ListConditionconditionscollectConditions(metadata);// 2. 逐个判断for(Conditioncondition:conditions){if(!condition.matches(context,metadata)){returntrue;// 任一条件不满足跳过}}returnfalse;}4.3 ConditionalOnMissingBean 的陷阱ConfigurationpublicclassMyAutoConfiguration{BeanConditionalOnMissingBeanpublicDataSourcedataSource(){returnnewHikariDataSource();}}问题如果用户自定义了 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)publicclassMyCustomDataSourceAutoConfiguration{...}4.4 ConditionalOnProperty 的匹配逻辑BeanConditionalOnProperty(nameapp.cache.enabled,havingValuetrue,matchIfMissingfalse)publicCacheManagercacheManager(){...}配置值havingValuematchIfMissing结果truetruefalse✅ 创建falsetruefalse❌ 不创建未配置truefalse❌ 不创建未配置truetrue✅ 创建默认开启truetruetrue✅ 创建matchIfMissing true是 Spring Boot 实现默认行为的关键——配置不写也开写了 false 才关。五、配置变更监听运行时能不能热刷新默认情况下Spring Boot 的配置在启动时一次性加载运行中修改 YAML 不会生效。但某些场景需要热刷新5.1 RefreshScopeSpring CloudRestControllerRefreshScopepublicclassConfigController{Value(${app.feature.enabled})privatebooleanfeatureEnabled;}调用/actuator/refresh端点后RefreshScope标注的 Bean 会被销毁重建下次访问时从配置中心重新拉取。5.2 ConfigurationProperties RefreshScopeConfigurationProperties(prefixapp)RefreshScopepublicclassAppConfig{...}这种方式在 Spring Cloud 配合 Config Server / Nacos 时很常见。5.3 自研方案Environment 手动更新不走 Spring Cloud也可以直接操作EnvironmentComponentpublicclassConfigRefresher{AutowiredprivateConfigurableEnvironmentenvironment;publicvoidupdateProperty(Stringkey,Stringvalue){MutablePropertySourcessourcesenvironment.getPropertySources();MapString,ObjectmapnewHashMap();map.put(key,value);// 插入到最高优先级sources.addFirst(newMapPropertySource(dynamicConfig,map));}}注意这种方式只对ValueRefreshScope或ConfigurationPropertiesRefreshScope生效普通Value不会自动刷新。六、常见坑与排查指南坑 1YAML 的缩进错误导致配置丢失app:timeout:3000name:my-apppool-size:10# ← 多了一个空格pool-size 变成了 app 的兄弟属性YAML 对缩进极其敏感IDE 的 YAML 插件可以检测这类问题。坑 2Value 注入为 nullComponentpublicclassMyBean{Value(${app.timeout})privatelongtimeout;// ✅ 正确}publicclassMyBean{Value(${app.timeout})privatelongtimeout;// ❌ new 出来的对象不走 Spring 容器Value 不生效}MyBeanbeannewMyBean();// timeout 0原因Value是由AutowiredAnnotationBeanPostProcessor在 Bean 创建阶段注入的手动 new 的对象不经过容器。坑 3多 Profile 时配置合并的意外覆盖# application.ymlapp:timeout:3000retry:3# application-prod.ymlapp:timeout:10000# retry 没写激活prodprofile 后app.retry的值是 3来自默认配置不是 null。Spring Boot 对 profile 的处理是追加覆盖不是替换。但如果是 Map 类型的属性# application.ymlapp:servers:a:host1b:host2# application-prod.ymlapp: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 按优先级排列属性绑定ValuePropertyResolver占位符解析 类型转换属性绑定ConfigurationPropertiesBinder松散绑定 递归嵌套条件装配ConditionEvaluatorBeanDefinition 注册前判断运行时刷新RefreshScopeBean 销毁重建掌握这条链路就能回答面试中关于 Spring Boot 配置的几乎所有问题——从配置优先级谁高到ConditionalOnMissingBean 为什么不生效。