" />
文章

Spring Boot 自动装配机制的原理

Spring Boot 自动装配机制的原理

面试指北 · 蔚来

题意

面试官问「Spring Boot 自动装配的原理」,表面上是在问一个注解,实际上想确认三件事:

  1. 你是否知道 @SpringBootApplication 这个注解到底拆成了哪几个注解,各自负责什么;
  2. 你是否清楚「引入一个 starter 依赖,就能直接用上里面的 Bean」这条链路是怎么走通的,配置文件在哪里、由谁读取、什么时候读取;
  3. 你是否理解「条件注解 + 顺序控制」这套设计,为什么它能做到「你有就用你的,你没有我用默认的」。

换句话说,这道题的完整回答应该覆盖:入口注解 → 候选配置的发现 → 候选配置的过滤 → 配置类里 Bean 的注册 → 用户自定义的覆盖优先级。原文给出的三条(依赖注入 / 自动配置 / 组件扫描)是这条链路上的三个点,下面把它们串成一条线。

结论

先给一句能在面试里直接说出口的总结:

Spring Boot 的自动装配,本质上是把「需要哪些配置类」这件事从「开发者手写 @Configuration」变成了「框架根据 classpath 上有什么依赖,用 SPI 的方式把一批候选配置类捞出来,再用条件注解现场筛一遍,最后 @Import 进容器」。

拆开就是四个关键词:

  • 入口:@SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan;
  • 发现:@EnableAutoConfiguration 通过 @Import(AutoConfigurationImportSelector.class) 引入选择器,选择器从 META-INF/spring.factories(Boot 2.7 之前)或 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Boot 2.7+ / 3.x)里读出候选配置类的全限定名列表;
  • 过滤:候选配置类上的一堆 @ConditionalOnXxx 条件注解,在启动时逐个求值,不满足的直接跳过,所以「引入了一百多个自动配置类」并不会真的注册一百多个 Bean;
  • 覆盖:用户自己声明了同类型 Bean,@ConditionalOnMissingBean 就让自动配置退让,这就是「约定优于配置 + 可覆盖」。

至于原文提到的依赖注入(@Autowired / @Qualifier)和组件扫描(@ComponentScan),它们解决的是你自己的代码怎么进容器;自动装配解决的是第三方 jar 包里的代码怎么进容器。这是两件不同的事,面试时一定要分开讲,混在一起说会被追问到答不上来。

要点展开

1. 从入口注解拆起

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

@SpringBootApplication 是一个组合注解,核心是三个:

@SpringBootConfiguration   // 本质是 @Configuration,标记这是一个配置类
@EnableAutoConfiguration   // 自动装配的总开关
@ComponentScan             // 扫描当前类所在包及其子包

三者分工很清楚:

注解 负责什么 作用范围
@ComponentScan 扫描业务代码里的 @Component / @Service / @Repository / @Configuration 主类所在包及其子包
@Autowired / @Qualifier 在容器已有的 Bean 之间建立依赖关系 容器内
@EnableAutoConfiguration 把第三方 jar 里的配置类批量导入 classpath 上所有 jar

注意 @ComponentScan 的默认范围是主启动类所在的包。这也是为什么「把启动类放到最外层包」是 Spring Boot 的约定——放偏了,业务类就扫不到。但它扫不到别的 jar 包,别的 jar 也不会按你的包路径来组织代码,所以必须另有一套机制,这就是自动装配存在的根本原因。

2. @EnableAutoConfiguration 干了什么

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage                       // 把主类所在包注册成一个 AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)  // 关键:引入选择器
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

这里有两个细节值得说:

  • @AutoConfigurationPackage 把主类所在包记录到一个 BasePackages 里,JPA 的实体扫描、MyBatis 的 Mapper 扫描都依赖它,可以理解为「告诉第三方框架你的业务代码在哪个包」。
  • @Import 引入的是 AutoConfigurationImportSelector,它实现了 ImportSelector 接口,作用是返回一批类的全限定名,让 Spring 把这些类当成配置类处理。

AutoConfigurationImportSelector 的实现(简化):

public class AutoConfigurationImportSelector
        implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware,
                   BeanFactoryAware, EnvironmentAware, Ordered {

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        }
        AutoConfigurationEntry entry = getAutoConfigurationEntry(annotationMetadata);
        return StringUtils.toStringArray(entry.getConfigurations());
    }

    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata metadata) {
        // 1. 读取候选配置类
        List<String> configurations = getCandidateConfigurations(metadata, null);
        // 2. 去重
        configurations = removeDuplicates(configurations);
        // 3. 去掉 @SpringBootApplication(exclude=...) 和 spring.autoconfigure.exclude 里声明的
        Set<String> exclusions = getExclusions(metadata, null);
        configurations.removeAll(exclusions);
        // 4. 用 AutoConfigurationImportFilter 做一次快速过滤(OnClassCondition / OnBeanCondition / OnWebApplicationCondition)
        configurations = getConfigurationClassFilter().filter(configurations);
        // 5. 触发 AutoConfigurationImportEvent 事件
        fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationEntry(configurations, exclusions);
    }
}

这里有个极易被忽略但很加分的点:它实现的是 DeferredImportSelector 而不是普通的 ImportSelector。

DeferredImportSelector 的「延迟」体现在:它要等所有用户自己写的 @Configuration 都处理完之后才执行。正因为如此,自动配置类里的 @ConditionalOnMissingBean 才能准确判断「用户到底有没有定义这个 Bean」——如果它和用户配置同时处理,顺序就不可控,判断结果也就不可靠。

这就是「用户配置优先于自动配置」这条规则的技术底座。

3. 候选配置类从哪来:spring.factories → AutoConfiguration.imports

早期(Spring Boot 2.6 及以前)用的是 Spring 自己的 SPI 机制:

# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.starter.FooAutoConfiguration,\
com.example.starter.BarAutoConfiguration

SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, classLoader) 会把 classpath 上所有 jar 里这个 key 对应的值汇总成一个列表。

Spring Boot 2.7 开始引入了新文件,3.0 起彻底移除了 spring.factories 中对 EnableAutoConfiguration 的支持:

# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.starter.FooAutoConfiguration
com.example.starter.BarAutoConfiguration

一行一个类名,可读性更好,也不用担心 properties 里的转义和换行。

对应的注解也从 @Configuration 换成了 @AutoConfiguration:

@AutoConfiguration(after = DataSourceAutoConfiguration.class)
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(FooProperties.class)
public class FooAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public FooService fooService(FooProperties properties) {
        return new FooService(properties.getPrefix());
    }
}

@AutoConfiguration 本质是 @Configuration(proxyBeanMethods = false) 加上 before / after 两个排序属性,是给自动配置类专用的语义化注解。

面试表述建议:「Spring Boot 2.7 之前是 spring.factories + SpringFactoriesLoader,2.7 引入了 AutoConfiguration.imports,3.0 之后自动配置只认新文件;但 spring.factories 本身没废弃,像 EnvironmentPostProcessor、ApplicationContextInitializer 这些扩展点仍然用它。」这样说既准确又能体现你关注版本差异。

4. 条件注解:按需装配的关键

候选列表可能有上百个类,但绝大多数都不该生效。真正做「筛」的动作,是配置类上的条件注解:

注解 生效条件 典型场景
@ConditionalOnClass classpath 上存在指定类 有 DataSource 才装配数据源相关配置
@ConditionalOnMissingClass classpath 上不存在指定类 排除某实现
@ConditionalOnBean 容器中已有指定 Bean 依赖其他配置的产物
@ConditionalOnMissingBean 容器中没有指定 Bean 给用户留出覆盖口
@ConditionalOnProperty 配置项等于指定值 / 不为 false @ConditionalOnProperty(prefix="foo", name="enabled", havingValue="true")
@ConditionalOnWebApplication 当前是 Web 应用(可细分 SERVLET / REACTIVE) Web 相关组件
@ConditionalOnResource 存在指定资源文件 有配置文件才生效
@ConditionalOnExpression SpEL 表达式为真 复杂条件

判断结果会被记录进 ConditionEvaluationReport,启动时加 --debug 或者打开 actuator 的 /actuator/conditions 端点就能看到「哪些配置类生效了、哪些没生效、为什么没生效」——这是排查「我的 Bean 怎么没注进来」最快的手段,比瞎猜强得多。

5. 排序与覆盖:谁先谁后、谁说了算

排序用三个注解(Boot 3 里也可以用 @AutoConfiguration(before/after)):

@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@AutoConfigureAfter(JacksonAutoConfiguration.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)

覆盖则靠两件事配合:

  1. 自动配置类里的 @Bean 方法上普遍带 @ConditionalOnMissingBean;
  2. AutoConfigurationImportSelector 是 DeferredImportSelector,天然后处理。

所以:

// 用户自己的配置
@Configuration
public class MyConfig {
    @Bean
    public FooService fooService() {
        return new MyFooServiceImpl();   // 只要这个存在
    }
}

自动配置里的 fooService() 就不会再注册,用户实现直接胜出。这就是「约定优于配置,但配置可被覆盖」的落地方式。

6. 组件扫描与依赖注入各自的活

这两点和自动装配经常被混为一谈,单独拎出来说清楚:

  • @ComponentScan:按包路径找类,只能扫到启动类所在包及子包,也就是「你自己的代码」。
  • 自动装配:按类全名列表导入,不受包路径限制,能覆盖 classpath 上任意 jar 里的配置类。
  • @Autowired / @Qualifier / @Resource:Bean 已经在容器里了,它只负责建立引用关系。类型冲突时用 @Qualifier("beanName") 指定名称,或者用 @Primary 标记默认实现。

一句话:扫描和自动装配是「往容器里放东西」,依赖注入是「在容器里找东西」。

7. 自己写一个 starter 的骨架

面试里如果被追问「你写过 starter 吗」,可以讲结构而不必编造业务细节:

foo-spring-boot-starter/
  └── pom.xml                     # 只做依赖聚合,不写代码
foo-spring-boot-autoconfigure/
  ├── src/main/java/.../FooProperties.java
  ├── src/main/java/.../FooAutoConfiguration.java
  └── src/main/resources/META-INF/spring/
        org.springframework.boot.autoconfigure.AutoConfiguration.imports
@AutoConfiguration
@ConditionalOnClass(FooClient.class)
@EnableConfigurationProperties(FooProperties.class)
public class FooAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public FooClient fooClient(FooProperties properties) {
        return new FooClient(properties.getEndpoint(), properties.getTimeout());
    }
}

FooProperties 用 @ConfigurationProperties(prefix = "foo") 绑定 application.yml 里的配置,配合 @EnableConfigurationProperties 注册。

三个设计要点:starter 只做依赖聚合、autoconfigure 里全用条件注解、所有 @Bean 都加 @ConditionalOnMissingBean 留出覆盖口。

易错点与对比

1. 「自动装配」和「自动配置」不是一回事。
严格说,AutoConfiguration 是自动配置(往容器里注册 Bean),而「装配」在 Spring 语境里指的是 Bean 之间的依赖注入(wiring)。面试时如果面试官用词模糊,你可以先点一句「我理解您问的是自动配置 auto-configuration 这部分」,显得你概念是清楚的。

2. @ConditionalOnMissingBean 放错位置会失效。
它只在「被判断的 Bean 一定已经处理过」时才可靠。放在自动配置类里是安全的(延迟导入保证后处理),但如果随手写在用户自己的 @Configuration 中,两个配置类的处理顺序不确定,结果就不可靠。Spring 官方文档明确建议:@ConditionalOnMissingBean 只用在自动配置类里。

3. @ConditionalOnClass 引用缺失类时的坑。
如果注解所在的方法返回类型直接写了那个缺失的类,JVM 加载方法签名时就会 NoClassDefFoundError。安全写法是把条件注解放到类上,或者用 name 属性写字符串:

@ConditionalOnClass(name = "com.example.MaybeMissingClass")

4. 条件注解是「启动时一次性判断」,不是运行时开关。
想动态切换 Bean 得用 @Profile、@ConditionalOnProperty 配合重启,或者 @Scope("prototype") / 工厂模式,别指望条件注解能热更新。

5. 版本差异别答错。

版本 自动配置的注册文件 推荐注解
Boot 2.6 及以前 META-INF/spring.factories @Configuration
Boot 2.7 ~ 2.x 两者都支持,spring.factories 方式被标记废弃 @AutoConfiguration
Boot 3.x 只认 AutoConfiguration.imports @AutoConfiguration

6. 与 SPI 的关系。
spring.factories 本质就是 Java SPI 思想的一个变种——「接口 + 约定路径的配置文件 + 加载器」,只是 Spring 做成了 key-value 形式,支持多个实现按 key 聚合。把这个类比说出来,能体现你有抽象能力。

7. 三种「让 Bean 进容器」的方式对比。

方式 触发依据 能否跨 jar 典型用途
@ComponentScan 包路径 不能(除非包名恰好匹配) 自己的业务组件
@Import / 自动配置 类全名列表 能 三方 starter
@Bean 方法 手写 能 精细控制第三方类

小结

把整条链路压缩成一句话,方便面试时一口气说完:

@SpringBootApplication 里的 @EnableAutoConfiguration 通过 @Import 引入了 AutoConfigurationImportSelector,这个选择器作为 DeferredImportSelector 延迟到用户配置处理完之后,从 AutoConfiguration.imports(旧版是 spring.factories)中读出所有候选配置类,先按 exclude 和 AutoConfigurationImportFilter 做粗筛,再在解析配置类的过程中用 @ConditionalOnClass、@ConditionalOnMissingBean 等条件注解逐个精筛,最终把满足条件的 @Bean 注册进容器;同时因为自动配置最后处理,用户自定义的 Bean 天然拥有更高优先级,这就是「开箱即用 + 可覆盖」的实现原理。

答题时建议按 入口注解 → 候选发现 → 条件过滤 → 覆盖机制 的顺序讲,中途主动提一句 DeferredImportSelector 和 Spring Boot 3 的文件变更,基本就能把这道题答到「超出预期」的位置。

License:  CC BY 4.0