Skip to content
Go back

Spring Boot自动装配——@SpringBootApplication背后的秘密

Spring Boot 自动装配:引入依赖即可用的奥秘

一句话结论(30s)

Spring Boot 自动装配的本质是「全量候选 + 条件筛选」——先把约 150 个候选自动配置类全部加载进来,再用 @ConditionalOnXxx 逐个判断是否生效。其关键设计是用 @Conditional 把「自动」和「可控」组合起来,因为框架既提供开箱即用的默认能力,又随时允许用户的 @ConditionalOnMissingBean 自定义 Bean 覆盖默认,让「约定优于配置」不走向「配置不可控」的极端。

核心原理(2min)

主流程:@SpringBootApplication@SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan 三合一,其中 @EnableAutoConfiguration 通过 @Import(AutoConfigurationImportSelector) 触发装配。selectImports() 读取 META-INF/spring/...AutoConfiguration.imports(3.x)或 spring.factories(2.x)得到候选配置类列表,然后对每个候选类做 @ConditionalOnClass(jar 是否在 classpath)、@ConditionalOnBean@ConditionalOnMissingBean(用户没自定义同类 Bean)、@ConditionalOnProperty(配置开关)等条件判断,满足则注册该类中的 @Bean,不满足则跳过。关键机制是 @ConditionalOnMissingBean 的判断顺序取决于配置类处理顺序,顺序不当会导致自动配置与用户 Bean 同类型并存而抛 NoUniqueBeanDefinitionException,需用 @AutoConfigureBefore/@AutoConfigureAfter@Primary 防坑。

底层深入(5-10min)

@SpringBootApplication 的三合一

@SpringBootApplication@SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan 的复合注解。自动装配的入口在 @EnableAutoConfiguration,它的关键只有一行——@Import(AutoConfigurationImportSelector.class),把「装配哪些类」的决定权委托给一个 DeferredImportSelector 实现。真实源码(Spring Boot 4.x):

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

	Class<?>[] exclude() default {};

	String[] excludeName() default {};

}

@Import 导入的不是普通配置类,而是一个 ImportSelector:Spring 解析到 @EnableAutoConfiguration 时不会立即注册 Bean,而是调用 selectImports() 拿到候选类名列表再逐个处理。ENABLED_OVERRIDE_PROPERTY 对应 spring.boot.enableautoconfiguration 开关,设为 false 可整体关闭自动装配;exclude/excludeName 属性则用来按类名或全限定名排除指定自动配置。

思考一下:自动装配为什么用 SPI(@Import + ImportSelector),而不是直接扫包? 因为候选配置类分散在几十个 starter 的 jar 里,包名各不相同,根本不在你项目的包路径下,@ComponentScan 扫不到;而 @Import 的类名又是编译期写死的,框架无法预知用户将来会引入哪些依赖。SPI 的思路是:约定一个固定的资源文件位置(META-INF/spring/...imports),谁提供能力谁就把自己的类名写进去,框架启动时统一去 classpath 收集,从而实现「引入依赖即可被装配」。

自动装配的完整流程

1. @EnableAutoConfiguration
   → @Import(AutoConfigurationImportSelector)

2. AutoConfigurationImportSelector.selectImports()
   → 读取 META-INF/spring/org.springframework.boot.autoconfigure.
     AutoConfiguration.imports (3.x) 或 spring.factories (2.x)
   → 得到候选自动配置类列表(~150个)

3. 对每个候选类做 @ConditionalOnXxx 条件判断:
   - @ConditionalOnClass: 相关 jar 在 classpath 上?
   - @ConditionalOnBean: 需要的 Bean 已存在?
   - @ConditionalOnMissingBean: 用户没自定义同类 Bean?
   - @ConditionalOnProperty: 配置文件中有对应开关属性?

4. 条件满足 → 注册该配置类中的 @Bean
   条件不满 → 跳过

思考一下:为什么要「全量加载 ~150 个候选类再过滤」,而不是用到哪个才加载哪个? 因为「哪个该用」的判断依据本身很便宜——@ConditionalOnClass 只是检查 classpath 上有没有某个类;而「精确地只装配所需」需要理解用户意图,成本极高且不可靠。全量加载 + 条件筛选的取舍是:多读一份 imports 文件几乎零成本,却换来了极简的「约定」——starter 作者只需声明「有 A 类时我生效」,框架自动完成剩下的判断。

selectImports:装配入口

selectImports() 本身很薄,只做两件事:判断开关是否关闭(关闭直接返回空数组),否则调用 getAutoConfigurationEntry() 拿到结果并转成 String[] 返回给容器。真实源码:

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

真正的流程在 getAutoConfigurationEntry() 里:加载候选类 → 去重 → 汇总 exclude 排除项 → 用条件过滤器粗筛 → 广播事件。其中 getConfigurationClassFilter().filter(...) 是「条件筛选」的第一道粗筛,用 OnClassConditionAutoConfigurationImportFilter 先砍掉 classpath 上没有相关依赖的候选类:

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
	if (!isEnabled(annotationMetadata)) {
		return EMPTY_ENTRY;
	}
	AnnotationAttributes attributes = getAttributes(annotationMetadata);
	List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
	configurations = removeDuplicates(configurations);
	Set<String> exclusions = getExclusions(annotationMetadata, attributes);
	checkExcludedClasses(configurations, exclusions);
	configurations.removeAll(exclusions);
	configurations = getConfigurationClassFilter().filter(configurations);
	fireAutoConfigurationImportEvents(configurations, exclusions);
	return new AutoConfigurationEntry(configurations, exclusions);
}

候选类从哪来:读 .imports 文件

候选类来自 ImportCandidates.load()。Spring Boot 3.x 起用 META-INF/spring/<注解全限定名>.imports 取代 2.x 的 spring.factories;这里 autoConfigurationAnnotation 默认是 AutoConfiguration.class,所以读的文件是 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
		@Nullable AnnotationAttributes attributes) {
	ImportCandidates importCandidates = ImportCandidates.load(this.autoConfigurationAnnotation,
			getBeanClassLoader());
	List<String> configurations = importCandidates.getCandidates();
	Assert.state(!CollectionUtils.isEmpty(configurations),
			"No auto configuration classes found in " + "META-INF/spring/"
					+ this.autoConfigurationAnnotation.getName() + ".imports. If you "
					+ "are using a custom packaging, make sure that file is correct.");
	return configurations;
}

ImportCandidates.load() 的读取逻辑很朴素——classLoader.getResources(location) 会命中 classpath 上所有 jar 里同名的 .imports 文件并合并,然后逐行读取,每行一个候选类全限定名,# 开头是注释、空行跳过:

private static final String LOCATION = "META-INF/spring/%s.imports";

public static ImportCandidates load(Class<?> annotation, @Nullable ClassLoader classLoader) {
	Assert.notNull(annotation, "'annotation' must not be null");
	ClassLoader classLoaderToUse = decideClassloader(classLoader);
	String location = String.format(LOCATION, annotation.getName());
	Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
	List<String> importCandidates = new ArrayList<>();
	while (urls.hasMoreElements()) {
		URL url = urls.nextElement();
		importCandidates.addAll(readCandidateConfigurations(url));
	}
	return new ImportCandidates(importCandidates);
}

private static List<String> readCandidateConfigurations(URL url) {
	try (BufferedReader reader = new BufferedReader(
			new InputStreamReader(new UrlResource(url).getInputStream(), StandardCharsets.UTF_8))) {
		List<String> candidates = new ArrayList<>();
		String line;
		while ((line = reader.readLine()) != null) {
			line = stripComment(line);
			line = line.trim();
			if (line.isEmpty()) {
				continue;
			}
			candidates.add(line);
		}
		return candidates;
	}
	catch (IOException ex) {
		throw new IllegalArgumentException("Unable to load configurations from location [" + url + "]", ex);
	}
}

每个 starter 的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 里就是一行一个自动配置类名,spring-boot-autoconfigure 模块自身的文件开头几行长这样:

org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
...

启动加 --debug 可以看到完整的 Condition Evaluation Report:

@ConditionalOnMissingBean 的坑

@Configuration
public class DataSourceAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        return new HikariDataSource(...);
    }
}

这个条件判断的语义:“如果容器中还没有 DataSource 类型的 Bean,我这个才生效。”

:条件判断的顺序取决于配置类被处理的顺序。如果自动配置类在用户配置类之前被处理,@ConditionalOnMissingBean 检查时还没看到用户的 Bean → 条件满足 → 自动配置的 DataSource 注册。随后用户的 DataSource 也被注册 → 两个同类型 Bean 冲突 → NoUniqueBeanDefinitionException

防坑@AutoConfigureBefore/@AutoConfigureAfter 声明配置类的顺序,确保用户自定义 Bean 先于自动配置被处理。或用户配置类加 @Primary

思考一下:为什么 @ConditionalOnMissingBean 不改成「用户 Bean 永远优先」,一劳永逸? 因为 Spring 逐类处理时,只能看到「当前已注册」的 Bean,无法预知未来会不会冒出用户的 Bean。要保证用户优先,只能靠 @AutoConfigureBefore/After 显式声明顺序,或用户自己加 @Primary。这也解释了为什么自动配置类都老老实实标了 @AutoConfigureOrder/@AutoConfigureBefore——顺序是这套机制里必须被显式管理的一等公民。

从 spring.factories 到 AutoConfiguration.imports

Spring Boot 2.x 用 META-INF/spring.factories(一个大的 properties 文件),解析时需要加载所有文件并解析。3.x 改为 META-INF/spring/...AutoConfiguration.imports(每行一个类名),直接按行读取,性能更好。

思考一下:从 properties 改成「每行一个类名」的 imports 文件,只是快一点吗? 本质是职责简化spring.factories 是通用 properties,需要完整解析 key-value 并把同名 key 合并;而 .imports 只承载「类名列表」这一种职责,读文件、剥注释、trim、按行收集即可。当文件格式与它的唯一用途完全对齐时,解析逻辑自然更简单、更快。

自定义 Starter

// 1. 自动配置类
@Configuration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public MyService myService(MyProperties props) {
        return new MyServiceImpl(props);
    }
}

// 2. META-INF/spring/...AutoConfiguration.imports
// 文件中加一行: com.example.MyAutoConfiguration

// 3. 用户只需引入 jar + application.yml 配置即可用

总结

Spring Boot 自动装配的精髓:用 @Conditional 把”自动”和”可控”组合起来——框架提供默认能力(你不需要显式配置),但随时可以被你的自定义 Bean 覆盖(通过 @ConditionalOnMissingBean)。防御性的自动配置让”约定优于配置”不走向”配置不可控”的极端。

章末提问

1. 自动装配的原理用一句话概括?

结论先行:全量候选 + 条件筛选。

因为:框架先把 classpath 上所有 META-INF/spring/...AutoConfiguration.imports 里声明的候选配置类(约 150 个)全部读进来,再用 @ConditionalOnXxx 逐个判断「该不该生效」——@ConditionalOnClass 看 jar 在不在、@ConditionalOnMissingBean 看用户是否已自定义,满足才注册 @Bean

2. @Import(AutoConfigurationImportSelector.class) 和普通 @Import(配置类.class) 区别在哪?

结论先行:前者延迟决定「导入谁」,后者写死「导入谁」。

因为:普通 @Import 导入的是确定的配置类,编译期就固定了;而 ImportSelector 是 SPI 扩展点,Spring 会调用 selectImports() 动态返回一组类名,框架因此能在运行时去 classpath 收集所有 starter 声明的候选类,做到「引入什么依赖就装配什么」。

3. 自动装配为什么用 SPI 文件而不是包扫描?

结论先行:因为候选类分散在第三方 jar,且无法被用户包路径覆盖。

因为:@ComponentScan 只能扫启动类所在包及其子包,而自动配置类散布在几十个 starter 的 jar 里、包名各异,扫不到;SPI 用「约定固定文件位置」取代「约定扫描路径」,第三方 jar 只需在自己的 META-INF/spring 下声明类名,框架统一收集,解耦了「提供能力」和「发现能力」。

4. @ConditionalOnMissingBean 为什么有时会失效、出现两个同类型 Bean?

结论先行:因为条件判断的顺序取决于配置类处理顺序,自动配置类先于用户配置类时检查不到用户的 Bean。

因为:@ConditionalOnMissingBean 只在「被处理的那一刻」看当前容器里有没有同类型 Bean,它是逐类推进的,无法预知未来。自动配置类先被处理 → 还没看到用户 Bean → 条件满足 → 注册了自己的 DataSource,随后用户 Bean 也注册,两者冲突抛 NoUniqueBeanDefinitionException。解法是 @AutoConfigureBefore/After 控顺序或用户加 @Primary

5. Spring Boot 3.x 为什么用 .imports 文件取代 spring.factories

结论先行:让文件格式与唯一职责对齐,解析更简单、更快。

因为:spring.factories 是通用 properties,要完整解析 key-value,且加载时要把所有文件的同名 key 合并;.imports 只承载「每行一个类名」的单一职责,按行读取、剥注释、trim 即可,无需解析属性结构,职责纯粹所以更快。同时按注解全限定名命名文件,也天然区分了不同 SPI 用途。


Share this post on:

Previous Post
Spring事务——传播行为与@Transactional失效的6种场景
Next Post
Spring循环依赖——三级缓存的精妙设计