自动配置与 Starter:依赖如何触发装配并安全退让
引入一个 Starter 后应用多出几十个 Bean,删除一个看似无关的依赖后数据源不再创建,用户自己声明 Client 后默认配置仍偷偷保留一半。自动配置并不是“Boot 猜测开发者想要什么”,而是一组按 classpath、配置、BeanDefinition、资源和应用类型执行的条件化配置。Starter 只负责把依赖与元数据组织成稳定入口,真正创建对象的是 auto-configuration。
Spring Boot 4.1 的自定义自动配置使用 @AutoConfiguration 和 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 发现候选,具体约束见官方 Creating Your Own Auto-configuration。历史 spring.factories 在旧版本承担过候选发现,升级时必须按目标发行线核对,不能把旧教程的注册方式当作永久协议。
候选发现与条件评估先于普通 Bean 创建
启用自动配置后,ImportSelector 收集候选名称,读取排除项与元数据,应用过滤器和条件,再把命中的配置类注册成 BeanDefinition。候选选择发生在配置类解析阶段,普通单例尚未实例化;条件应基于可在该阶段稳定判断的信息。
@ConditionalOnClass 常用字符串或安全的 annotation value 检查可选库;类条件放在 @Bean 方法上时,JVM 可能在条件判断前就解析方法签名中的缺失类型。更稳妥的做法是把依赖特定库的配置拆成嵌套配置类,让类加载边界与条件一致。
@ConditionalOnProperty 判断 Environment 中的配置值。matchIfMissing=true 表示缺失时启用,只适用于安全默认;安全、远程写入或昂贵能力不应默认打开。集合属性的存在性判断容易误解,复杂配置更适合写专用 Condition 并输出明确原因。
Bean 条件依赖当前已处理的 BeanDefinition 集合,评估顺序会影响可见结果。官方建议只在 auto-configuration 中使用这些条件,并确保被依赖配置先于当前配置。条件里调用 getBean() 会提前实例化并破坏阶段边界。
Back-off 是用户覆盖契约,不是简单的 MissingBean
默认配置的核心承诺是:用户提供等价能力时自动退让。要实现这一点,必须定义退让粒度。一个 Client 可能由 transport、serializer、retry、observation 和 facade 组成;只让 facade back-off,其他默认组件仍可能修改用户 Client;整组退让又可能让用户不得不重建所有基础设施。
ConditionBackoffDemo.java 演示用户 Bean 获得所有权:
javac --release 17 -Xlint:all -Werror examples/backend-development/spring-boot/auto-config-starter/ConditionBackoffDemo.java examples/backend-development/spring-boot/auto-config-starter/AutoConfigOrderingDemo.java
java -cp examples/backend-development/spring-boot/auto-config-starter ConditionBackoffDemolibraryPresent=true userBeanPresent=true autoConfigured=false selected=user-client真实 @ConditionalOnMissingBean 可以按类型、名称、注解、参数化容器和搜索策略判断。按具体实现类退让会忽略用户的接口实现;按过宽接口退让又可能被无关 Bean 阻止。每个公共扩展点应说明替换哪个类型、是否允许多个、Bean name 是否契约、父 context 是否参与搜索。
默认 Bean 可用 @ConditionalOnMissingBean,用户定制更适合 XxxCustomizer/Builder callback。Customizer 允许用户只修改超时或序列化,不必复制整个 auto-configuration;多个 customizer 要有顺序、幂等和不可修改项。把底层 Builder 原样暴露给用户会让必要的安全拦截器被移除,应在最终构建前验证不变量。
Back-off 必须做正反测试:没有用户 Bean 时默认创建;存在用户实现时默认不创建;存在多个候选时给出明确歧义;只覆盖局部组件时其余默认行为仍一致;父子 context 与 test slice 中结果符合契约。conditions endpoint 可以解释命中,但测试才固定兼容性。
自动配置顺序只控制定义处理,不等于 Bean 创建顺序
@AutoConfigureBefore、@AutoConfigureAfter 与 @AutoConfigureOrder 影响自动配置类处理顺序,使 Bean 条件在预期 definition 集合上判断。它们不直接声明对象之间的运行依赖;真正的创建顺序仍由注入、dependsOn 和生命周期决定。
排序实验输出配置选择顺序,并显式区分实例化阶段:
java -cp examples/backend-development/spring-boot/auto-config-starter AutoConfigOrderingDemoautoConfigurationOrder=[property-infrastructure, client-transport, client-observation] beanCreationStartedAfterSelection=true形成排序环时,应重新审视模块边界,而不是继续增加 before/after。公共库 A 与 B 互相声明顺序通常说明它们耦合了同一能力。可以抽出基础配置 C,或让 B 仅在明确 Bean 存在时扩展。相同 order 的候选不应依赖枚举稳定性。
自动配置类本身不应被组件扫描发现,否则可能绕过候选过滤、排除与排序,甚至重复注册。它们放在独立包,通过 imports 文件显式列出。应用主包扫描到库内部配置是 Starter 目录设计错误。
Starter 管理依赖入口,不应偷偷带入运行时能力
Starter 通常是轻量依赖描述模块,聚合 auto-configure 模块、必需 API 和合理默认实现。它不需要包含业务代码。依赖应分为必需、可选与 annotation processor;把所有数据库驱动、日志实现和云 SDK 都作为传递依赖,会扩大镜像、漏洞面、类冲突和自动配置候选。
Starter 命名、坐标和依赖管理是公共契约。组织内 Starter 应使用清晰前缀,避免伪装官方坐标;使用 BOM 管理兼容版本,不在多个 Starter 中各自锁不同底层库。optional 只影响消费者依赖传播,不保证运行时类一定存在,auto-configuration 仍要做 class 条件。
一个成熟 Starter 至少拆分 API、autoconfigure、starter 三层:API 供业务编译;autoconfigure 依赖 Boot 并实现条件配置;starter 提供便捷依赖组合。大型平台还可拆 observation、test support 与 migration 模块。这样业务只使用 API,不直接绑定自动装配实现。
不要在 Starter 中默认执行远程注册、数据库迁移或创建后台线程。对象创建可以由 auto-configuration 完成,外部副作用必须等 context 生命周期明确阶段,并提供关闭、开关和失败策略。一个依赖加入 classpath 就向外发送数据,是供应链和合规风险。
配置属性是 Starter 的长期 API
每个 @ConfigurationProperties 前缀、键、类型、默认值和废弃替代项都属于兼容面。通过 annotation processor 生成 metadata,IDE 才能提示;通过 additional metadata 可描述枚举、provider 与弃用。文档必须解释配置改变的是哪个运行对象,不只列默认值表。
属性重命名要有迁移期。旧键与新键同时出现时定义优先级并告警;不要静默任选一个。类型从毫秒整数升级为 Duration 时,兼容裸数字语义必须明确,否则 30 可能从毫秒变成秒。配置绑定失败应在启动期阻止错误 Client 进入单例池。
敏感属性不应通过 conditions report、toString 或 metadata 默认值泄漏。Secret 只保存引用或受保护值,日志输出 presence/version。若 Starter 支持凭证轮换,必须定义 Client 重建和在途请求行为,而不是只刷新字段。
条件报告是解释工具,不是运行正确性证明
启动 debug 或 Actuator conditions endpoint 可以看到 positive/negative matches 与 unconditional classes。它能回答某条件为什么匹配,却不能证明创建的 Bean 能连接、线程安全或符合业务。报告还可能很大,应按配置类筛选并保护端点。
故障排查沿候选链进行:imports 文件是否打包;候选是否被 exclude;配置类能否加载;每个条件输入是什么;Bean 条件评估时哪些 definition 已存在;最终注册了哪些名称;实例化是否另行失败。不要一看到 NoSuchBeanDefinitionException 就复制一个官方 Bean,可能根因只是可选依赖被排除。
自动配置数量和条件耗时应进入启动基线。一个 Starter 增加数百候选或扫描整个 classpath,必须说明价值。条件要缓存可缓存元数据,避免网络 I/O 与深反射。升级时比较 conditions report 的归一化摘要与关键 BeanDefinition 快照,发现默认实现切换和 back-off 变化。
用 ApplicationContextRunner 固定自动配置契约
框架模块可以用 ApplicationContextRunner 构造最小 context,按需加载目标 auto-configuration、属性和用户配置,并断言 Bean、失败与 condition outcome。Web、reactive 等变体使用匹配 runner,避免每个条件测试都启动完整应用。
测试矩阵覆盖 class present/absent、property on/off/invalid、user bean present、多个候选、excluded、不同应用类型和 ordering。还要用打包测试确认 imports 与 configuration metadata 真正在 jar 内;IDE 源码测试通过不代表发布物资源没有被 shading 或插件丢失。
升级验收要比较 managed dependency、模块坐标、自动配置候选、属性废弃、默认行为和测试 slice。Boot 4 的模块化与框架代际变化可能让旧 Starter 编译通过却运行时找不到类,或条件永远不命中。为支持多条 Boot 主线,优先发布清晰的兼容版本,而不是在一个 jar 里用大量反射猜版本。
自动配置成熟的标志,是没有用户配置时提供安全默认,用户表达所有权时完整退让,条件不命中时能解释,升级时差异可比较,移除 Starter 后不残留外部副作用。做到这些,Starter 才是稳定装配边界,而不是把隐式复杂度塞进 classpath。
