同一个策略类型,为什么加一个 Bean 就变了

订单应用没有声明 OrderPolicy 时,容器里存在一个来源为 auto 的策略。应用新增名叫 customPolicy 的 Bean 后,策略来源变成 user,原先的 orderPolicy 名称也查不到。两者名字不同,Bean definition 覆盖开关保持关闭,应用仍然正常启动。

这个结果说明,自动配置的退让发生在默认定义注册之前。它不能用“后一个同名 Bean 覆盖前一个”解释,也不需要把 Bean 覆盖开关改成允许。判断依据是策略类型与条件评估时已经可见的定义。

Spring Boot 没有重新实现一套依赖注入容器。配置解析、对象创建、后处理器与销毁仍由 Framework 执行。Boot 提供启动流程、配置数据处理、条件化基础设施装配和运行管理。在原生容器里已经定位过的问题,可以继续沿定义来源、候选类型、创建阶段和调用对象追踪;新增的是这些定义为什么被导入。

本篇冻结 Boot 3.5.6,源码提交 23bcd7b4d8969cdef27771f1594b044bb98724a6。独立 boot-lab 使用该版本 parent 的原生依赖管理,实际解析出 Framework 6.2.11、Tomcat 10.1.46、Micrometer 1.15.4、HikariCP 6.3.3、pgJDBC 42.7.7。Java 为 21。这份组合用于复现实验,不代表当前生产版本推荐;尤其 HikariCP 已经不同于前面原生 JDBC 实验,不能混用两边的默认值与日志。

Boot 启动输入到 Framework 对象的装配路径

启动流程先准备输入,再刷新容器

SpringApplication.run 的主路径依次准备 Environment、创建 ApplicationContext、准备上下文、refresh、调用 runner。自动配置主要参与 refresh 中的配置解析阶段,不是 run 返回之后再向运行中的容器追加一批“默认服务”。固定源码同时显示,runner 位于 refresh 之后,runner 失败仍然可以令整个启动失败。单个 Bean 创建完毕并不等于应用已经 ready。

实验入口 Chapter38.App 只组合 @Configuration 与 @EnableAutoConfiguration,没有组件扫描。这样可以排除扫描到实验辅助配置的干扰。策略自动配置来自资源文件中的完整类名:

1
2
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
blog.spring.boot.OrderAutoConfiguration

AutoConfigurationImportSelector 属于延迟导入机制:读取候选、去重、排除和过滤,再参与配置处理。资源文件只是候选入口,不是“文件里的每个类都会产生对象”的保证。类路径条件、属性条件和 Bean 条件还会改变最终注册结果。候选导入源码

作为对照,new AnnotationConfigApplicationContext(User.class) 只加载显式用户配置。虽然 Boot jar 和 imports 文件都在 classpath 上,这个原生上下文没有 @EnableAutoConfiguration 入口,不会因此发现 orderPolicy。是否引入 Boot 依赖和是否执行 Boot 的装配入口是两个不同条件。

实验刻意将 WebApplicationType 设为 NONE;38 的条件结果不依赖启动 HTTP 服务器。下一篇继续复用同一自动配置,第 40 篇才启动真实 Tomcat 对照运行观测。

类型条件怎样实现默认策略退让

完整实现位于 boot-lab/src/main/java/blog/spring/boot/OrderAutoConfiguration.java。声明层的核心内容如下,完整 import 和运行入口随第 00 篇实验工程下载:

1
2
3
4
5
6
7
8
9
10
11
12
@AutoConfiguration
@ConditionalOnClass(name = "org.postgresql.Driver")
@ConditionalOnProperty(prefix = "lab.order", name = "enabled",
havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(OrderProperties.class)
public class OrderAutoConfiguration {
@Bean
@ConditionalOnMissingBean(OrderPolicy.class)
OrderPolicy orderPolicy(OrderProperties properties) {
return new OrderPolicy("auto", properties.getLimit());
}
}

@ConditionalOnMissingBean(OrderPolicy.class) 检查类型,因此用户定义可以叫 customPolicy。实验不仅断言最终只有一个策略,还断言 containsBean("orderPolicy") 为 false。如果只检查 getBean(OrderPolicy.class) 返回用户对象,就可能遗漏“默认对象仍存在、只是用户对象有更高注入优先级”的另一种情况。

条件报告给出的负匹配包含 found beans of type 'blog.spring.boot.OrderPolicy' customPolicy。这条证据把用户定义与默认工厂方法的跳过联系起来。它证明此次装配输入下发生了退让,不能单独证明用户策略的方法行为正确;后者仍需要业务断言。

Bean 条件有时间边界。它检查评估时已经处理到的定义,并不预测未来的注册动作。因此这类默认装配适合放在自动配置阶段,不能随意复制到一组顺序不明的普通配置里,再假定其行为与 Boot 完全一样。官方 ConditionalOnMissingBeanTests 也区分名称、类型、父子上下文和声明顺序;本实验只覆盖单上下文中的策略类型退让,父子搜索策略不能由这一条结果推导。条件测试源码

工厂方法的返回类型也会影响条件能看到的信息。若改成宽泛的 Object,容器在不提前实例化对象时能够预测的类型变少。为了判断策略是否存在而主动创建 Bean,又会引入过早初始化问题。默认组件应尽量暴露可预测的具体返回类型,不把运行时对象猜测当作稳定装配协议。

缺少依赖时,应跳过哪个范围

驱动类条件放在自动配置类上,缺失时整组配置不进入后续定义注册。实验通过 Boot 的 FilteredClassLoader("org.postgresql") 隐藏驱动包,同时保留其他依赖,运行 ApplicationContextRunner。结果是 startupFailure 为 null、策略数量为零,条件报告明确写出缺失 org.postgresql.Driver。

这是类加载可见性实验,没有从磁盘删除 jar,也没有证明 PostgreSQL 网络可达。驱动存在只能说明某个类能加载;连接地址、凭据、服务器状态属于更晚的运行条件。第 40 篇才会实际连接数据库。

类条件的位置同样有意义。把条件只放到一个返回可选第三方类型的 @Bean 方法上,未必能隔离 JVM 对方法签名的解析。可选依赖的配置应放在独立配置类里,让类级条件先控制其加载范围。官方自动配置文档讨论了这一隔离方式;实验使用类名字符串并将返回值保持为本工程自己的 OrderPolicy,因此没有制造一个提前加载可选返回类型的陷阱。自动配置条件说明

属性关闭又是另一条路径。--lab.order.enabled=false 不改变 classpath,驱动条件仍为正匹配,而属性条件变为负匹配,策略仍然不存在。同样的“没有 Bean”可以来自不同输入,诊断必须读取对应的条件原因。

属性来源、条件与绑定对象各有时点

实验为 lab.order.limit 准备三个值:默认属性 20、lab-boot.properties 中的 30、命令行中的 40。运行时显式传入 --spring.config.name=lab-boot。没有命令行覆盖时,配置数据覆盖 defaultProperties,得到 30;加入 --lab.order.limit=40 后得到 40,并在 commandLineArgs PropertySource 中找到该键。

这只验证了这三种来源在当前输入下的优先级。外部文件、profile 文档、环境变量、测试专用属性等还有各自的来源与排序,不能将这个三项实验扩写成完整配置优先级表。实际排障应记录最终值及其来源,避免仅看到某个文件中的声明就断定程序使用该值。

Environment 保存配置来源,OrderProperties 是绑定后的普通对象,OrderPolicy 又是使用该对象当前值创建的不可变 record。三者不能混称为“配置”。本实验通过 @EnableConfigurationProperties 注册绑定基础设施和目标属性类型,再由工厂方法读取 limit。

条件与绑定也不是同一个动作:enabled 决定配置是否参与注册,limit 决定已经选中的工厂如何构造对象。启动后修改属性源,不会自动重新走一遍自动配置导入与单例创建路径。若需要动态变更,应明确后续读取方式、对象更新协议与并发可见性;仅仅增加一个 setter 不能提供整条更新协议。

Boot 默认值要和实验设置分开

38 实际断言了 Boot 启动的 bean factory 默认不允许循环引用、不允许同名定义覆盖。它们是 Boot 在准备上下文时施加的策略,不应倒推成所有原生 Framework 容器的默认值。前面第 09 篇的 setter 循环实验必须保留自己的容器设置。

本工程还有显式设置:关闭 banner、关闭 JVM shutdown hook 注册、将 web 类型设为 NONE、排除 DataSourceAutoConfiguration。这些用于隔离实验,不是 Boot 原生默认。排除 DataSource 自动配置尤其重要:驱动在 classpath 上是策略条件的输入,38 并不需要数据库连接。若让无 URL 的数据源自动配置参与启动,实验可能先因数据源配置失败,掩盖待观察的策略条件。

条件报告可以解释某个自动配置为什么没有进入,不是任意启动失败的完整因果图。候选可能已经匹配,但在属性转换、构造器调用或外部资源初始化时失败;这时应继续读取异常根因与失败 Bean,而不是反复调整条件开关。

复现与预测

从第 00 篇的下载工程进入 examples/spring-framework-lab,使用 JDK 21:

1
2
sh boot-lab/run.sh 38
./mvnw -f boot-lab/pom.xml -B dependency:tree

mvnw 会切换到实验根目录,所以独立模块需要显式 -f boot-lab/pom.xml。只在子目录执行 ../mvnw compile 会编译根模块,不能证明 Boot 模块通过。

38 留下 14 条 PASS、CHAPTER 38 PASS 和退出码 0,原始日志在 evidence/38/local-20261002/。完整依赖树与源码哈希也随工程保存。日志中的条件正负匹配属于预期对照,不是需要清理的失败噪声。

修改练习:把用户 Bean 的名称从 customPolicy 改成 anotherPolicy,保持返回类型不变。预期默认策略继续退让,因为类型条件与名称无关。随后把自动配置上的 matchIfMissing 改成 false,并去掉命令行中的 enabled。预期整个策略配置不再参与注册,单独修改 limit 也不能恢复它。两次修改分别改变 Bean 输入与属性条件,能区分“类型退让”和“功能未启用”。

前置阅读:深入 Spring 03:refresh 如何组织容器启动。容器扩展继续阅读 深入 Spring 39:导入、选择、注册与容器扩展边界。

参考资料