AutoConfiguration 的生效原理
AutoConfigurationImportSelector在@SpringBootApplication下的@EnableAutoConfiguration中被导入:@Import({AutoConfigurationImportSelector.class})
自动配置生效的核心流程调用链如下:
AutoConfigurationImportSelector.selectImports -> AutoConfigurationImportSelector.getAutoConfigurationEntry -> AutoConfigurationImportSelector.getCandidateConfigurations -> ImportCandidates.load
1. 入口:AutoConfigurationImportSelector.selectImports
这是 DeferredImportSelector 接口定义的入口方法,Spring 容器启动时会调用它。
源码分析
java
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 1. 检查是否开启了自动配置(一般都是 true)
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
// 2. 【核心】获取自动配置项的实体对象
// 这步执行完,所有的类名(包括官方的和你的)都已经找齐了
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
// 3. 将结果转为 String 数组返回给容器,Spring 容器随后会把这些类加载为 Bean
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
运行时变量快照 (Example)
annotationMetadata: 代表你的启动类YudaoServerApplication的元数据。autoConfigurationEntry: 包含了一个 List,例如["...RedisAutoConfiguration", "...YudaoCacheAutoConfiguration", ...],共计 140+ 个类名。
2. 调度核心:AutoConfigurationImportSelector.getAutoConfigurationEntry
这个方法负责指挥"搜集 -> 去重 -> 排除 -> 过滤"的全过程。
源码分析
java
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
// 1. 获取是否配置了 exclude 等属性
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 2. 【核心调用】获取所有候选配置类的全限定名
// 这一步会去扫描所有 jar 包下的配置文件
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 3. 去重(防止同一个 jar 包被引了两次导致重复)
configurations = removeDuplicates(configurations);
// 4. 处理排除逻辑(比如你在 @SpringBootApplication(exclude=...) 里写的类)
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
// 5. 再次过滤(根据 onClass 等条件进行过滤,注意这里只是初步过滤)
configurations = getConfigurationClassFilter().filter(configurations);
// 6. 发送事件(通知监听器,自动配置类已经拿到了)
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
运行时变量快照 (Example)
configurations(步骤2执行后) : 这是一个原始的巨大列表,包含org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration和cn.iocoder...YudaoCacheAutoConfiguration。exclusions: 假如你没配置排除,这里是Empty Set。
3. 双轨加载:AutoConfigurationImportSelector.getCandidateConfigurations
这一步是 Spring Boot 2.7+ / 3.x 兼容性的体现,它同时加载新旧两种配置文件。
源码分析
java
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// 1. 【加载旧版】加载 spring.factories
List<String> configurations = new ArrayList<>(
SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader())
);
// 2. 【加载新版】加载 .imports 文件
// 这一步是读取 META-INF/spring/...imports
ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader())
.forEach(configurations::add); // 将新版读到的结果追加到 list 中
// 3. 断言非空(如果一个都没找到,说明 jar 包损坏或环境有问题)
Assert.notEmpty(configurations, "No auto configuration classes found...");
return configurations;
}
运行时变量快照 (Example)
configurations(步骤1后) : 包含从spring.factories读到的旧版配置类(如果是纯 Spring Boot 3.0+ 项目,这里可能是空的或很少)。configurations(步骤2后) : 追加了从.imports文件读到的新版配置类。此时你的YudaoCacheAutoConfiguration被加进来了。
4. 物理扫描:ImportCandidates.load
这是最底层的 IO 读取逻辑。
源码分析
java
public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
// 1. 确定类加载器
ClassLoader classLoaderToUse = decideClassloader(classLoader);
// 2. 【拼接路径】
// annotation.getName() = "org.springframework.boot.autoconfigure.AutoConfiguration"
// 结果 location = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"
String location = String.format("META-INF/spring/%s.imports", annotation.getName());
// 3. 【物理查找】
// 调用 ClassLoader.getResources(location)
// 去所有 jar 包里找这个路径的文件,返回一组 URL
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
List<String> importCandidates = new ArrayList<>();
// 4. 【循环读取】
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
// 打开每一个 URL 指向的文件,逐行读取里面的字符串(类名)
// 并把它们全部 add 到 importCandidates 集合里
importCandidates.addAll(readCandidateConfigurations(url));
}
return new ImportCandidates(importCandidates);
}
运行时变量快照 (具体的例子)
这里我们还原一下 ImportCandidates.load 运行时各个变量的真实值:
1. annotation:
- 值:
org.springframework.boot.autoconfigure.AutoConfiguration.class
2. location:
- 值:
"META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"
3. urls (Enumeration 对象) :
假设你的项目依赖了官方包和你的 yudao 包,这个枚举里会包含两个 URL 对象:
- URL 1 :
jar:file:/Users/repo/.../spring-boot-autoconfigure-2.7.18.jar!/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports - URL 2 :
jar:file:/Users/repo/.../yudao-spring-boot-starter-redis-1.0.jar!/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
4. readCandidateConfigurations(url) 的返回值:
- 当读取 URL 1 时 :返回一个
List,包含RedisAutoConfiguration,WebMvcAutoConfiguration等 100 多个官方类名。 - 当读取 URL 2 时 :返回一个
List,包含cn.iocoder...YudaoCacheAutoConfiguration这 1 个类名。
5. importCandidates (最终返回值):
- 这是一个合并后的大 List:
[...官方的100个..., "cn.iocoder...YudaoCacheAutoConfiguration"]。
这是一篇为您准备的技术博客,深入剖析 Spring Boot 自动配置的"失效"(即自动退让)机制。
AutoConfiguration 的失效原理
------ 为什么自定义 Bean 会让默认配置自动"闭嘴"?
在使用 Spring Boot 时,我们经常会遇到这种"魔法"现象:
- 什么都不配置,Spring Boot 默认为我们配置好了 Redis、DataSource、MVC。
- 一旦我们在项目中手动定义了一个
RedisTemplate或DataSource,Spring Boot 的默认配置就瞬间"消失"了,转而使用我们的配置。
这种约定大于配置 且用户优先的机制是如何实现的?
一、 核心机制:防御性注解
Spring Boot 能够实现"你配了我就不配,你没配我来兜底"的效果,核心在于一个关键的条件注解:@ConditionalOnMissingBean。
这个注解直译过来就是:"仅当容器中缺失(Missing)指定 Bean 时,条件才成立。"
1. 源码证据
让我们看一眼 RedisCacheConfiguration(Spring Boot 官方自动配置类)的源码:
java
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class) // <--- 【核心开关】
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, ...) {
// 创建默认的 RedisCacheManager
return RedisCacheManager.builder(redisConnectionFactory).build();
}
}
注意那行 @ConditionalOnMissingBean(CacheManager.class)。它的逻辑非常简单粗暴:
Spring 容器,请检查一下你自己,现在有没有一个类型为
CacheManager的 Bean?
- 如果没有 :好的,执行下面的
@Bean方法,创建一个默认的。- 如果有:打扰了,我不执行了。
二、 关键维度:加载时机 (Timing)
仅有注解是不够的。如果自动配置类比用户的配置类先执行 ,那容器里肯定没有 Bean,@ConditionalOnMissingBean 就会误判,导致默认配置总是生效,覆盖了用户的配置。
因此,加载顺序是"失效原理"的另一个核心支柱。
1. DeferredImportSelector (延迟导入)
我们在之前分析过,@EnableAutoConfiguration 导入的是 AutoConfigurationImportSelector。
注意这个类的接口定义:
java
public class AutoConfigurationImportSelector implements DeferredImportSelector { ... }
Deferred (延迟) 是关键词。在 Spring 的生命周期中,实现了 DeferredImportSelector 的类,会在所有的标准 @Configuration 类(也就是用户你自己写的配置)被解析和注册之后,才开始执行。
2. 完美的时序配合
Spring Boot 的启动流程经过精心设计,确保了"用户优先":
- Phase 1: 用户配置加载期
- Spring 扫描你的包结构。
- 发现你定义的
@Configuration类(例如YudaoCacheAutoConfiguration)。 - 执行其中的
@Bean方法,将你自定义的RedisCacheManager注册到 IoC 容器中。 - 此时:容器里已有 1 个 CacheManager。
- Phase 2: 自动配置加载期 (Deferred)
- 用户配置全部处理完后,Spring 开始处理
AutoConfiguration。 - 加载器读取
META-INF/.../AutoConfiguration.imports。 - 找到官方的
RedisCacheConfiguration。
- Phase 3: 条件评估期
- Spring 准备加载官方默认的
cacheManagerBean。 - 触发
@ConditionalOnMissingBean(CacheManager.class)检查。 - 检查结果:容器中已存在(Phase 1 注册的那个)。
- 最终决策:放弃加载官方 Bean。
三、 特殊情况:层级与泛型
@ConditionalOnMissingBean 还有两个需要注意的高级特性,这在自定义 Starter 时非常有用。
1. 按名称失效 (Name)
你可以指定 Bean 的名称,而不是类型。
java
// 只有当容器里没有叫 "myRedisTemplate" 的 Bean 时才生效
@ConditionalOnMissingBean(name = "myRedisTemplate")
2. 按泛型失效 (Generics)
对于 RedisTemplate<K, V> 这种带泛型的类,Spring Boot 甚至能识别泛型参数。
官方源码示例:
java
@Bean
@ConditionalOnMissingBean(name = "redisTemplate") // 只要没这个名字的
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(...) { ... }
如果不指定泛型,@ConditionalOnMissingBean(RedisTemplate.class) 会把 RedisTemplate<String, String> 和 RedisTemplate<Object, Object> 视为同一个类型。如果你想精确控制(比如只针对 <Object, Object> 类型的缺失做回退),需要在注解中详细配置。
实战如何定制自己的 AutoConfiguration 及生效原理
在微服务架构或中台建设中,我们经常需要封装公司内部的通用组件(如:统一鉴权、日志SDK、自定义缓存策略)。如果每个项目都去复制粘贴代码,维护成本极高。
最佳实践是开发一个自定义的 Starter 。我们将通过一个完整的示例,演示如何编写自己的 AutoConfiguration,并解析它到底是如何被 Spring Boot 发现并接管的。
一、 核心目标
我们要实现一个简单的功能模块:YudaoWelcomeService。
- 功能:打印一条欢迎日志。
- 配置 :可以通过
application.yml配置开关和欢迎语。 - 效果:引入 Jar 包后,无需任何 Java 配置,开箱即用。
二、 步骤详解 (The "How")
步骤 1:定义配置属性类 (Properties)
首先,定义一个 POJO 来承接 application.yml 中的配置项。这体现了 "约定大于配置" 的思想。
java
package cn.iocoder.yudao.framework.welcome.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
// 1. 指定配置文件的前缀,例如 yudao.welcome.msg = "Hello"
@ConfigurationProperties(prefix = "yudao.welcome")
public class YudaoWelcomeProperties {
/**
* 是否启用
*/
private boolean enabled = true;
/**
* 欢迎语内容
*/
private String msg = "Welcome to Yudao System!";
// 省略 Getter 和 Setter
}
步骤 2:编写业务逻辑类 (Service)
这是我们要自动注入到容器里的核心功能 Bean。
java
package cn.iocoder.yudao.framework.welcome.core;
public class YudaoWelcomeService {
private String msg;
public YudaoWelcomeService(String msg) {
this.msg = msg;
}
public void sayHello() {
System.out.println(">>> Yudao System: " + msg);
}
}
步骤 3:编写自动配置类 (AutoConfiguration)
这是最关键的装配车间。我们将在这里组装 Bean,并加上条件判断。
java
package cn.iocoder.yudao.framework.welcome.config;
import cn.iocoder.yudao.framework.welcome.core.YudaoWelcomeService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
// 1. 标记这是一个自动配置类
@AutoConfiguration
// 2. 激活属性绑定,将 YudaoWelcomeProperties 注册为 Bean
@EnableConfigurationProperties(YudaoWelcomeProperties.class)
// 3. 总开关:只有配置中 yudao.welcome.enabled = true 时才生效(默认为 true)
@ConditionalOnProperty(prefix = "yudao.welcome", name = "enabled", havingValue = "true", matchIfMissing = true)
public class YudaoWelcomeAutoConfiguration {
@Bean
public YudaoWelcomeService yudaoWelcomeService(YudaoWelcomeProperties properties) {
// 利用 Properties 中的值创建 Service
return new YudaoWelcomeService(properties.getMsg());
}
}
步骤 4:注册身份 (The Imports File)
这是让 Spring Boot 发现你的配置类的入场券。如果没有这一步,你的类只是 Jar 包里的一个普通文件,Spring 根本不会看它一眼。
在 src/main/resources 目录下创建文件:
路径 :META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
内容(写入你的全限定类名):
text
cn.iocoder.yudao.framework.welcome.config.YudaoWelcomeAutoConfiguration
三、 生效原理深度解析 (The "Why")
为什么做完上面四步,引入 Jar 包就能生效?这背后是 Spring Boot 启动时的一套严密的 SPI (Service Provider Interface) 机制。
1. 搜集阶段:全网扫描
当你的应用启动,@SpringBootApplication 包含的 @EnableAutoConfiguration 开始工作。
它会召唤 AutoConfigurationImportSelector,利用 ClassLoader 对 Classpath 下所有的 Jar 包发出指令:
"把你们家里
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里的名单都交上来!"
你的 yudao-framework.jar 响应了号召,交出了 YudaoWelcomeAutoConfiguration 这个名字。
2. 加载阶段:内存融合
Spring 将搜集到的成百上千个配置类名放入一个巨大的列表中。此时,你的配置类和官方的 RedisAutoConfiguration、WebMvcAutoConfiguration 处于同等地位,都在等待被加载。
3. 筛选阶段:条件过滤
Spring 开始遍历这个列表,尝试加载每一个配置类。此时,你的类头上的注解开始发挥作用:
@ConditionalOnProperty: Spring 检查application.yml。如果用户设置了yudao.welcome.enabled=false,你的配置类在这一步就会被"踢出局",Bean 不会被创建。@EnableConfigurationProperties: Spring 发现需要属性绑定,于是自动把YudaoWelcomeProperties注册进容器,并填充配置文件里的值。
4. 注册阶段:Bean 的诞生
如果所有条件都满足,Spring 就会执行 @Bean 方法,new YudaoWelcomeService(...),并将实例放入 IoC 容器。
四、 最佳实践 Tips
- 命名规范:
- 自动配置类建议以
AutoConfiguration结尾。 - 属性类建议以
Properties结尾。
- 模块分离:
- Starter 模块 (
yudao-spring-boot-starter-xxx):通常是一个空项目,只负责引入依赖(pom.xml)。 - Autoconfigure 模块 (
yudao-spring-boot-autoconfigure):存放真正的代码(上面的 Java 代码)。 - 这样做是为了让用户只需要引一个 starter 依赖,就能把所有东西拉下来,同时保持依赖树清晰。
- 使用
@ConditionalOnMissingBean:
- 如果你开发的通用组件允许用户覆盖,务必在你的
@Bean上加上@ConditionalOnMissingBean。 - 这样,如果用户自己在代码里手动
new了一个 Service,你的默认配置就会自动礼貌退让,避免冲突。