《Spring Boot 高级》2.3 自动配置的覆盖与排除

讲清自动配置的退让(back off)机制:用户 Bean 如何通过 @ConditionalOnMissingBean 让自动配置让位,spring.autoconfigure.exclude 与 @SpringBootApplication(exclude=...) 的差别,以及覆盖自动配置 Bean 的几种做法各自的风险。

本节目标:把「用户定义优先于自动配置」这件事从口号变成机制——讲清退让如何发生、排除的两种入口有何差别、以及五种覆盖手法各自会在什么时候咬你一口。
适用版本:Spring Boot 4.1.x(Java 21)

前两节讲清了自动配置「装什么、按什么顺序装、满足什么条件才装」。本节回答最后一问:装了之后,我作为使用者如何让它让位、如何整体排除、如何只改一小块。

back off:用户 Bean 如何让自动配置退让

退让不是特殊逻辑,而是两个已讲过的机制叠加的必然结果:

  1. AutoConfigurationImportSelector 是最后处理的 DeferredImportSelector(getOrder() = Integer.MAX_VALUE - 1,见 2.1)。用户自己的 @Configuration / @Component 在它之前就被解析、Bean 定义已注册。
  2. 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 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计