本节目标:把「用户定义优先于自动配置」这件事从口号变成机制——讲清退让如何发生、排除的两种入口有何差别、以及五种覆盖手法各自会在什么时候咬你一口。
适用版本:Spring Boot 4.1.x(Java 21)
前两节讲清了自动配置「装什么、按什么顺序装、满足什么条件才装」。本节回答最后一问:装了之后,我作为使用者如何让它让位、如何整体排除、如何只改一小块。
back off:用户 Bean 如何让自动配置退让
退让不是特殊逻辑,而是两个已讲过的机制叠加的必然结果:
AutoConfigurationImportSelector是最后处理的DeferredImportSelector(getOrder()=Integer.MAX_VALUE - 1,见 2.1)。用户自己的@Configuration/@Component在它之前就被解析、Bean 定义已注册。OnBeanCondition在REGISTER_BEAN阶段求值(见 2.2),此时它能看到用户已经注册的 Bean 定义。
于是只要自动配置类在 @Bean 方法上标了 @ConditionalOnMissingBean,用户先注册的同类 Bean 就会让它整段跳过。以数据源为例(javap -v 读出的类级注解):
@AutoConfiguration(before = DataSourceInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceCheckpointRestoreConfiguration.class })
public class DataSourceAutoConfiguration { ... }
只要你自己声明了 DataSource Bean,整个 DataSourceAutoConfiguration 就不会注册它自己的那套。这就是「用户定义优先」的全部秘密。
@ConditionalOnMissingBean 的匹配维度(实测签名):
public interface ConditionalOnMissingBean {
Class<?>[] value(); // 按类型
String[] type(); // 按类型字符串(零编译依赖)
Class<?>[] ignored(); // 排除某些类型
String[] ignoredType();
Class<? extends Annotation>[] annotation();// 按 Bean 上的注解
String[] name(); // 按 Bean 名
SearchStrategy search(); // CURRENT / ANCESTORS / ALL
Class<?>[] parameterizedContainer(); // 识别 List<Foo> 等容器类型
}
search 默认 CURRENT:只看当前上下文,不看父上下文。父子容器(如 Spring Cloud 的 bootstrap 上下文)里,这意味着父上下文里的 Bean 不会让子上下文的自动配置退让——这是一个常见的「为什么没退让」根因。@ConditionalOnSingleCandidate 则要求「恰好一个候选」,用于「有多个就不敢自动配」的场景。
一个最小可跑的对照:下面这个应用不排除数据源自动配置,只是自己声明了一个 DataSource Bean。
import javax.sql.DataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class MyDataSourceConfig {
@Bean
DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:h2:mem:probe");
return ds;
}
}
启动后,--debug 报告里对应的记录大致长这样(示例输出,只演示格式,非本机实测):
Negative matches:
DataSourceAutoConfiguration:
Did not match:
- @ConditionalOnMissingBean (types: javax.sql.DataSource,javax.sql.XADataSource; SearchStrategy: all) found beans of type 'javax.sql.DataSource' dataSource (OnBeanCondition)
括号里直接给出了「为什么」:found beans of type ... dataSource 说明是名为 dataSource 的 Bean 让它退让的。这正是 2.2 里 ConditionOutcome 携带消息的用处——退让不再是玄学,报告会点名。
| 用户做的事 | 自动配置行为 | 依据 |
|---|---|---|
| 自己声明同类型 Bean | 退让(整段跳过) | @ConditionalOnMissingBean |
| 声明多个同类型 Bean | 依赖单一候选的自动配置退让 | @ConditionalOnSingleCandidate |
| 只改属性 | 不退让,自动配置内部按属性调整 | @ConditionalOnProperty / @ConfigurationProperties |
| 什么都不做 | 自动配置生效 | 条件成立 |
排除:属性入口 vs 注解入口
要整体关掉某个自动配置(而不是让它的某段退让),有两个入口,它们在语义上等价、在使用场景上不同。
注解入口:@SpringBootApplication(exclude = ...),其 exclude() / excludeName() 来自组合的 @EnableAutoConfiguration(实测两个注解都有这两个元素)。
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class ProbeApplication {
public static void main(String[] args) {
SpringApplication.run(ProbeApplication.class, args);
}
}
属性入口:spring.autoconfigure.exclude。它的类型在 4.1.1 的配置元数据里是 java.util.List<java.lang.Class>:
unzip -p ~/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/4.1.1/spring-boot-autoconfigure-4.1.1.jar \
META-INF/spring-configuration-metadata.json | rg -o '"name" : "spring\.autoconfigure\.[^"]*"'
"name" : "spring.autoconfigure.exclude"
spring:
autoconfigure:
exclude:
- org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration
两者在 AutoConfigurationImportSelector.getExclusions(...) 里被合并:注解的 exclude/excludeName 与属性值一起进 AutoConfigurationEntry.getExclusions(),随后 recordExclusions(...) 把它们写进 --debug 报告的 Exclusions: 段。
差别在这几处:
| 维度 | @SpringBootApplication(exclude=...) | spring.autoconfigure.exclude |
|---|---|---|
| 生效时机 | 编译期写死,随代码走 | 运行时按 profile / 环境变量可变 |
| 可覆盖性 | 改代码才变 | 运维可在不改包的情况下调整 |
| 非编译依赖 | 需 excludeName(字符串) | 天然是字符串,无编译依赖 |
| 误写后果 | 编译报错(类名错) | 启动即报错(见下) |
| 适用 | 应用确定不需要某能力 | 同一产物在不同环境要不同开关 |
关键安全网:AutoConfigurationImportSelector 有 handleInvalidExcludes(...),当你排除的类不是自动配置类时,它会直接抛异常,消息实测为:
The following classes could not be excluded because they are not auto-configuration classes:%n%s
所以把 exclude 写成「一个普通 @Configuration 类」会启动失败——这是好事,它挡住了一类静默无效的配置。反过来,用 excludeName 写错类名、且该类恰好不存在时,也可能被归入「不是自动配置类」而报错;写错但存在同名类,则可能被静默忽略,所以要对照 jar 核类名。
覆盖自动配置 Bean 的五种做法
「覆盖」的粒度不同,风险天差地别。按推荐度从高到低:
| 做法 | 机制 | 风险 | 何时用 |
|---|---|---|---|
| 声明自己的 Bean | 触发 @ConditionalOnMissingBean 退让 | 低;但会整体丢掉该自动配置的其他 Bean | 想完全替换某类 Bean |
实现 *Customizer | 在自动配置构造 Bean 时回调微调 | 低;只改你指定的部分 | 只想调一小块(推荐首选) |
@Primary | 不排除自动配置,仅让注入优先选你的 | 中;两个 Bean 都在容器里,按名字注入会拿到错的 | 多实现并存、按类型注入 |
spring.main.allow-bean-definition-overriding=true | 后注册的同名 Bean 覆盖先注册的 | 高;顺序敏感、静默覆盖 | 几乎不应使用 |
BeanFactoryPostProcessor 移除定义 | 手动删掉自动配置的 BeanDefinition | 高;依赖内部结构、易随版本失效 | 极端场景 |
Customizer 是 4.x 的首选。以 Jackson 3 为例(javap 实测):
public interface org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer {
public abstract void customize(tools.jackson.databind.json.JsonMapper$Builder);
}
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class JsonConfig {
@Bean
JsonMapperBuilderCustomizer prettyPrintCustomizer() {
return builder -> builder.configure(tools.jackson.databind.SerializationFeature.INDENT_OUTPUT, true);
}
}
注意包名:Jackson 3 的类是 tools.jackson.*(不再是 com.fasterxml.jackson)。这是 4.x 最容易踩的覆盖坑:在 3.x 里,自定义一个 ObjectMapper Bean 就能替换自动配置;在 4.x 里,自动配置改用 JsonMapper / XmlMapper,自定义 ObjectMapper Bean 不再能替换它——必须走 JsonMapperBuilderCustomizer。
@Primary 的坑:它不排除任何 Bean。容器里会同时存在「自动配置的」和「你的」两个同类型 Bean,@Primary 只影响按类型注入时的选择;如果别处按 Bean 名 @Qualifier("xxx") 注入,或者遍历 Map<String, Foo>,你得到的仍可能是自动配置那个。用它之前先确认没有按名注入的调用方。
allow-bean-definition-overriding 的坑:它让「后注册者覆盖先注册者」,而注册顺序又受 AutoConfigurationSorter(见 2.1)与用户配置解析顺序共同影响。改一个 @AutoConfigureAfter 就可能让覆盖方向反转。属性本身在 4.1.1 里存在(spring-boot 模块元数据实测):
unzip -p ~/.m2/repository/org/springframework/boot/spring-boot/4.1.1/spring-boot-4.1.1.jar \
META-INF/spring-configuration-metadata.json | rg -o 'spring\.main\.allow-bean-definition-overriding'
spring.main.allow-bean-definition-overriding
手动移除定义的坑:BeanFactoryPostProcessor 里拿 BeanDefinitionRegistry 删定义,看起来干净,但你依赖的是自动配置的内部结构(哪个类注册了哪个 Bean 名)。上游一次重构就会失效,且失效方式往往是「删除无效但不报错」。若确需如此,加一个断言:启动时校验目标 Bean 确实被替换。
三种手法的一次对照
把「退让 / 排除 / 微调」放进同一个小场景,容器里最终有什么会一目了然。场景:默认由自动配置提供一个 Jackson 3 的 JsonMapper。
| 你的动作 | 自动配置是否加载 | 容器里的 Mapper | 影响面 |
|---|---|---|---|
| 什么都不做 | 加载 | 自动配置那个 | 默认行为 |
exclude = JacksonAutoConfiguration.class | 不加载 | 没有 | 所有依赖它的组件都得自己配 |
追加 JsonMapperBuilderCustomizer | 加载 | 仍是自动配置那个,但被你改过 | 只改你指定的开关 |
// 1) 完全排除:整个 JacksonAutoConfiguration 不再加载
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
@SpringBootApplication(exclude = JacksonAutoConfiguration.class)
public class AppA {
public static void main(String[] args) {
SpringApplication.run(AppA.class, args);
}
}
// 2) 微调:自动配置仍在,追加一个 customizer 改变其行为
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class AppB {
@Bean
JsonMapperBuilderCustomizer prettyPrint() {
return builder -> builder.configure(
tools.jackson.databind.SerializationFeature.INDENT_OUTPUT, true);
}
}
结论很直接:能微调就别排除。排除的代价是「连带丢失」——JacksonAutoConfiguration 一起提供的 JsonMapper、XmlMapper、以及若干基于它们的 Bean 全部消失,之后凡是按类型注入 JsonMapper 的组件都会启动失败;而追加 customizer 的代价接近于零,也不影响任何其他 Bean。
注意这里的类名(4.x 实测):排除项要写 org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration,而不是 3.x 的 org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration。排除字符串写错到「不存在的类」会在启动时被 handleInvalidExcludes 拦下;但若错写成一个恰好存在的其他自动配置类,就会静默排错对象,所以要对着 jar 核。
排障:覆盖/排除为什么没生效
按下面的顺序查,能覆盖绝大多数情况:
- 看
--debug的Exclusions:段:确认spring.autoconfigure.exclude里的类名真的被识别、真的被排除;拼错的类名不会出现在这里。 - 看
Negative matches:段:你的自定义 Bean 有没有让自动配置退让?若自动配置仍matched,说明@ConditionalOnMissingBean没看到你的 Bean——检查search策略(CURRENT不看父上下文)与你的 Bean 是否真的被扫描到。 - 确认类型对得上:
@ConditionalOnMissingBean(DataSource.class)匹配的是类型,如果你的 Bean 是接口的一个实现、但自动配置查的是接口类型,两者能对上;若查的是具体类而你只给了接口,则对不上。 - Jackson 场景特判:4.x 自定义
ObjectMapper不生效是预期行为,改用JsonMapperBuilderCustomizer。 @Primary场景特判:确认没有按名注入的调用方;用ApplicationContext.getBeansOfType(...)打印一下,两个 Bean 是否都在。- 排除 vs 退让混用:
exclude是「这个类根本别加载」,退让是「加载了但这段不生效」。若你exclude了某自动配置,它的全部 Bean 都没了,可能连累其他依赖它的自动配置。
小结
- 退让 = 「用户配置先注册」+ 「
OnBeanCondition在REGISTER_BEAN阶段求值」,两者共同让@ConditionalOnMissingBean看到用户的 Bean 后跳过整段自动配置。 @ConditionalOnMissingBean的search默认CURRENT,不看父上下文;@ConditionalOnSingleCandidate用于「多候选就不自动配」。- 排除有两个入口:
@SpringBootApplication(exclude/excludeName)(编译期、可编译校验)与spring.autoconfigure.exclude(运行时、运维可调、天然字符串)。二者在getExclusions(...)合并,并记入Exclusions:报告段。 - 排除非自动配置类会启动失败(
handleInvalidExcludes的安全网),这是设计而非缺陷。 - 覆盖手法里,
*Customizer与「声明自己的 Bean」最稳;@Primary、allow-bean-definition-overriding、手动移除定义分别有「按名注入拿错」「顺序敏感」「依赖内部结构」的坑。 - 4.x 的 Jackson 3 是覆盖行为的重灾区:自定义
ObjectMapper不再替换自动配置,改用JsonMapperBuilderCustomizer。
到这里,第 2 章从「怎么发现」讲到「怎么排序」「怎么判断」「怎么退让」,自动配置这条主线就闭环了。下一章转入 AOP:Spring 如何决定用 JDK 动态代理还是 CGLIB,以及这个选择会在什么边界上失效。
阅读导航:上一节:2.2 @Conditional 家族实现 · 下一节:3.1 JDK 动态代理与 CGLIB 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。