

一、引子:一个时序上说不通的注解#
Spring Boot 的自动配置,标准解释是「约定优于配置」:你不写配置,它给你一套合理的默认值;你写了,它就让开。
让开的机制是 @ConditionalOnMissingBean:
@Bean
@ConditionalOnMissingBean // 如果容器里没有 DataSource,我才创建
public DataSource dataSource() {
return new HikariDataSource(...);
}java用起来毫无障碍。直到有人问我:
@ConditionalOnMissingBean是在判断「容器里有没有这个 Bean」。但如果自动配置先于我的
@Configuration被处理,那判断的时候我的 Bean 还没注册,条件会认为「没有」,于是自动配置创建了自己的 DataSource——然后我的也创建了,容器里就有两个。反过来,如果自动配置在我之后处理,那它怎么保证一定在之后?我可没写任何顺序声明。
我当时的回答是「Spring 内部会保证顺序」。这等于什么都没说。
而且这个问题还有一个更尖锐的版本:
就算顺序对了,
@ConditionalOnMissingBean判断的到底是「Bean 已经被创建出来了」还是「Bean 的定义已经登记了」?如果是前者,那意味着判断时要先把我的 DataSource 实例化——可我的 DataSource 可能依赖别的还没准备好的东西。
这两个问题指向同一个我从没认真想过的东西:Spring 容器的启动不是一个连续的过程,它有明确分开的阶段,而条件注解的正确性完全依赖于「在哪个阶段执行」。
把这层想清楚之后,「魔法」这个词就消失了。剩下的只是一套相当直白的时序安排。
这篇文章拆三句话:
| 说法 | 问题出在哪 |
|---|---|
| 自动配置是「约定优于配置」的魔法 | 它是条件化的定义注册,靠时序保证正确 |
| IoC 就是把对象交给容器管理 | 容器有两个截然不同的阶段,混淆它们就理解不了循环依赖 |
| starter 就是把依赖打个包 | 它是一套 SPI 机制,且刚刚经历过一次不兼容迁移 |
二、自动配置:时序决定一切#
2.1 先把 @SpringBootApplication 拆开#
这个注解是三个注解的组合:
@SpringBootConfiguration // 本质就是 @Configuration
@EnableAutoConfiguration // 开启自动配置
@ComponentScan // 扫描当前包及子包
public @interface SpringBootApplication { ... }java真正做事的是 @EnableAutoConfiguration,而它自己也只做一件事:
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration { ... }java@Import 一个 ImportSelector,意思是「把这个类返回的那些类名,当作配置类导入进来」。
到这里为止都很直白。关键在于 AutoConfigurationImportSelector 实现的不是 ImportSelector,而是 DeferredImportSelector。
这个 Deferred 就是引子里那个问题的全部答案。
2.2 DeferredImportSelector:延迟到最后#
Spring 在解析配置类时,用的是 ConfigurationClassParser。它的处理流程大致是:
1. 解析主配置类(你的 Application 类)
2. 处理它的 @ComponentScan → 扫出你写的所有 @Component / @Configuration
3. 递归解析这些配置类,处理它们的 @Bean、@Import ...
4. ─────── 以上全部完成之后 ───────
5. 处理所有 DeferredImportSelector → 这才轮到自动配置text第 4 步那条线是整个机制的核心。普通的 ImportSelector 在遇到时立即处理,DeferredImportSelector 被攒起来,等所有常规配置类都解析完了才统一处理。
于是顺序被强制成了:
你写的配置永远先被处理,自动配置永远最后。
这不是巧合,也不是「Spring 内部会保证」的模糊说法,而是 ConfigurationClassParser.parse() 方法里明确的两段式结构:先 processConfigurationClass() 循环,再 this.deferredImportSelectorHandler.process()。
引子里的第一个问题就此解决。
2.3 两个阶段:定义 vs 实例#
引子里的第二个问题更微妙:条件判断的是「Bean 存在」还是「Bean 定义存在」?
要回答它,必须先把 Spring 容器启动的两个阶段分清楚。这是理解 Spring 最重要的一件事,可惜大部分教程把它们混在「IoC 容器启动」一句话里。
这个阶段不创建任何业务对象,只是在容器里登记「有哪些 Bean,它们叫什么名字,是什么类型,怎么创建,依赖谁」。
登记的产物是 BeanDefinition——一个描述对象的元数据结构,可以理解成「造物说明书」。
// BeanDefinition 里存的东西(简化)
class BeanDefinition {
String beanClassName; // 类名(字符串,不是 Class)
String scope; // singleton / prototype
boolean lazyInit;
String[] dependsOn;
ConstructorArgumentValues constructorArgs;
MutablePropertyValues propertyValues;
String factoryBeanName; // @Bean 方法所在的配置类
String factoryMethodName; // @Bean 方法名
// ...
}java执行者是 ConfigurationClassPostProcessor,它是一个 BeanDefinitionRegistryPostProcessor。上一节讲的配置类解析、@Import 处理、自动配置导入,全部发生在这个阶段。
所有 BeanDefinition 登记完毕后,容器才开始按说明书造对象:
finishBeanFactoryInitialization()
→ 遍历所有非懒加载的单例 BeanDefinition
→ getBean(name)
→ createBeanInstance() 构造对象
→ populateBean() 注入依赖
→ initializeBean() 执行 Aware、BeanPostProcessor、@PostConstruct、InitializingBeantext@Autowired 的注入发生在 populateBean(),AOP 代理的织入发生在 initializeBean() 里的 BeanPostProcessor。
现在可以回答第二个问题了:
两个阶段分开的意义远不止于此:
- AOP 能工作,是因为代理的织入发生在阶段二,而阶段一登记的只是原始类的定义;
@Value和@ConfigurationProperties能读到配置,是因为BeanFactoryPostProcessor(如PropertySourcesPlaceholderConfigurer)在两个阶段之间修改了定义;- 循环依赖能被解决(下一节),完全依赖于阶段二里的一个特殊设计。
分不清这两个阶段,Spring 的大部分行为都会显得像魔法。
2.4 条件注解为什么不会因为类不存在而崩溃#
还有一个细节值得单独说,因为它反直觉。
@ConditionalOnClass 是这样用的:
@Configuration
@ConditionalOnClass(RedisOperations.class) // 类路径里有 Redis 才生效
public class RedisAutoConfiguration { ... }java问题是:如果项目里根本没引入 Redis,RedisOperations 这个类不存在,那 JVM 在读取这个注解的时候不就 NoClassDefFoundError 了吗?注解的值是 Class<?> 类型,读它必然要加载类。
答案是:Spring 根本没用反射读这个注解。
它用的是 ASM 字节码解析(SimpleMetadataReaderFactory),直接从 .class 文件里把注解的元数据当字符串读出来:
反射读取: getAnnotation(ConditionalOnClass.class).value()
→ 返回 Class<?>[],必须加载 RedisOperations → 类不存在就崩
ASM 读取: 直接解析 class 文件的常量池
→ 拿到字符串 "org.springframework.data.redis.core.RedisOperations"
→ 不加载任何类text拿到字符串之后,用 ClassUtils.isPresent(className, classLoader) 判断,内部是 Class.forName 包在 try-catch 里。加载失败就返回 false,条件不成立,这个自动配置类从头到尾都不会被加载。
2.5 顺便说说启动慢#
spring-boot-autoconfigure 里有 100 多个自动配置类。启动时每一个都要走一遍条件判断,其中 @ConditionalOnClass 要做类加载尝试。这是 Spring Boot 启动耗时的主要来源之一。
Spring Boot 为此做了几层优化,可以当作性能设计的例子看:
第一层,spring-autoconfigure-metadata.properties。 构建时把每个自动配置类的 @ConditionalOnClass 条件预先提取到一个 properties 文件里。启动时先读这个文件做一遍批量粗筛,直接排除掉大部分不可能生效的配置类,避免逐个加载它们的字节码。
第二层,条件判断的分组与短路。 OnClassCondition 会把候选列表对半拆分给两个线程并行判断(这是 Spring Boot 2.x 引入的),因为类加载尝试是这里最慢的一步。
第三层,最彻底的那个:AOT。 Spring Boot 3 的 AOT 处理把「条件判断」这件事整个挪到了构建期。构建时就确定哪些自动配置生效,直接生成对应的 Bean 注册代码,运行时不再有任何条件判断。这也是 GraalVM 原生镜像能工作的前提——原生镜像里没有动态类加载,条件判断根本没法在运行时做。
2.6 小结#
「自动配置是魔法」这句话里,被隐去的是一套很朴素的时序安排:
DeferredImportSelector保证自动配置在用户配置之后处理;- 条件判断发生在 BeanDefinition 注册阶段,查的是定义不是实例;
- ASM 读取注解元数据,让引用不存在的类也不会崩;
@AutoConfigureBefore/After解决自动配置彼此之间的顺序。
四条合起来,@ConditionalOnMissingBean 的正确性就是可推导的,不需要当成约定去记。
三、循环依赖:三级缓存的第三级到底为什么存在#
3.1 标准答案,以及它没回答的部分#
循环依赖是 Spring 面试题里的经典。标准答案是「三级缓存」:
// DefaultSingletonBeanRegistry 里的三个 Map
Map<String, Object> singletonObjects; // 一级:完整的单例
Map<String, Object> earlySingletonObjects; // 二级:早期暴露的半成品
Map<String, ObjectFactory<?>> singletonFactories; // 三级:生产半成品的工厂java流程也能背:A 依赖 B,B 依赖 A。创建 A 时先把 A 的 ObjectFactory 放进三级缓存,然后去创建 B;B 需要 A,从三级缓存拿到工厂、调用它得到半成品 A、升级到二级缓存;B 创建完成,A 继续注入,完成。
这个描述是对的。但它回答不了一个问题:
两级缓存不就够了吗?
创建 A 之后直接把半成品 A 放进二级缓存,B 来了直接取。为什么要多一层工厂?
我以前的答案是「为了延迟创建」。但这里没有什么可延迟的——半成品 A 的实例已经通过构造器创建出来了,放进 Map 和放进工厂的成本一模一样。
真正的答案和 AOP 有关。
3.2 如果 A 需要被代理#
假设 A 上有 @Transactional,那么容器最终要交付的不是 A 本身,而是A 的代理对象。
代理是在哪里生成的?在 initializeBean() 里,由 AbstractAutoProxyCreator 这个 BeanPostProcessor 在初始化之后完成。也就是说,正常流程下代理对象在 Bean 生命周期的末尾才出现。
现在把循环依赖叠加上去:
创建 A → 构造出原始的 A(还没到 initializeBean,代理还不存在)
→ 注入依赖,发现需要 B
→ 创建 B
→ B 需要 A,此时只能拿到「原始的 A」
→ B 注入了原始 A,B 创建完成
→ A 继续走完 initializeBean → 生成代理 A'
→ 容器里注册的是 A'text问题出现了:容器里的 A 是代理 A’,但 B 持有的是原始 A。
B 调用 A 的方法时不会走事务增强。这是一个静默的错误——不报异常,只是事务莫名其妙没生效。
所以需要一个机制:当 A 被提前暴露给别人时,如果 A 需要代理,那就提前把代理造出来给它。
3.3 第三级缓存的真正职责#
三级缓存里放的 ObjectFactory,它的实现是这样的:
// AbstractAutowireCapableBeanFactory.doCreateBean 里
addSingletonFactory(beanName, () -> getEarlyBeanReference(mbd, beanName, bean));
// getEarlyBeanReference 会遍历 SmartInstantiationAwareBeanPostProcessor
// AbstractAutoProxyCreator 实现了它:如果这个 Bean 需要代理,现在就生成代理返回java关键在于:这个工厂被调用时,才决定要不要生成代理。
于是:
- 没有循环依赖:工厂从头到尾没被调用过,代理照常在
initializeBean阶段生成,一切按正常流程; - 有循环依赖:B 来取 A,工厂被调用,此时提前生成代理 A’ 并放进二级缓存。A 后续走到
initializeBean时,AbstractAutoProxyCreator发现这个 Bean 已经提前代理过了(内部有个earlyProxyReferences记录),就跳过重复代理,直接返回之前那个 A’。
最终 B 持有的 A’ 和容器里的 A’ 是同一个对象。
现在可以回答「为什么不能两级」了:
3.4 Spring 解决不了的循环依赖#
三级缓存不是万能的。有几种情况它无能为力,而这几种情况的边界恰好能反过来验证上面的理解。
| 类型 | 能否解决 | 原因 |
|---|---|---|
| 字段注入 / setter 注入的单例 | 能 | 对象已构造,可以提前暴露引用 |
| 构造器注入 | 不能 | 对象还没构造出来,没有东西可以暴露 |
| prototype 作用域 | 不能 | 不进单例缓存,每次都要新建,必然无限递归 |
@Async 标注的 Bean | 通常不能 | 它的代理由 AsyncAnnotationBeanPostProcessor 在初始化后创建,不参与提前代理 |
构造器注入那一行是最能说明问题的:三级缓存的前提是「对象实例已经存在,只是还没填充完」。构造器注入意味着 A 的构造函数需要 B,B 的构造函数需要 A——两个对象谁都没法先被 new 出来,缓存里根本没有可放的东西。
这也是为什么 Spring 官方推荐构造器注入:它让循环依赖在启动时就暴露成错误,而不是被悄悄化解掉。
3.5 顺便:Bean 生命周期的完整顺序#
既然讲到实例化阶段,把完整顺序列一遍。这个顺序在排查「为什么我的初始化逻辑没执行 / 执行早了」时非常有用:
1. 实例化 createBeanInstance() 调用构造器
2. 属性填充 populateBean() @Autowired / @Value 注入
3. Aware 回调 BeanNameAware → BeanClassLoaderAware → BeanFactoryAware
4. 前置处理 BeanPostProcessor.postProcessBeforeInitialization()
└─ @PostConstruct 在这里执行(由 CommonAnnotationBeanPostProcessor 触发)
5. 初始化 InitializingBean.afterPropertiesSet()
然后 @Bean(initMethod = "...")
6. 后置处理 BeanPostProcessor.postProcessAfterInitialization()
└─ AOP 代理在这里生成
7. ── 使用中 ──
8. 销毁前 @PreDestroy
9. 销毁 DisposableBean.destroy()
然后 @Bean(destroyMethod = "...")text有两点值得注意:
@PostConstruct 在第 4 步,早于 AOP 代理生成(第 6 步)。 所以在 @PostConstruct 里调用自己的 @Transactional 方法,事务不会生效——那时代理还不存在,this 就是原始对象。
ApplicationRunner / CommandLineRunner 在所有 Bean 都完成之后才执行。 需要「容器完全就绪后做一件事」,用它们,而不是往某个 Bean 的 @PostConstruct 里塞。
四、starter:一次正在进行中的迁移#
4.1 starter 里其实没有代码#
spring-boot-starter-web 这个依赖,很多人以为它包含了 Web 相关的实现。打开看一眼:
<!-- spring-boot-starter-web 的完整内容,几乎就是这些 -->
<dependencies>
<dependency>spring-boot-starter</dependency>
<dependency>spring-boot-starter-json</dependency>
<dependency>spring-boot-starter-tomcat</dependency>
<dependency>spring-web</dependency>
<dependency>spring-webmvc</dependency>
</dependencies>xml没有一行 Java 代码。 它就是一个 POM,作用是把一组版本兼容的依赖凑在一起。
真正的自动配置代码在 spring-boot-autoconfigure 这个 jar 里,而它是 spring-boot-starter 的传递依赖——也就是说,只要你用了任何一个 starter,那 100 多个自动配置类就全都在类路径上了。
所以 starter 的职责其实是两件事,而且都不是「提供功能」:
- 版本管理:保证凑在一起的依赖版本互相兼容;
- 触发条件:把某些类带进类路径,从而让对应的
@ConditionalOnClass成立。
第 2 点是理解 starter 的钥匙。加入 spring-boot-starter-data-redis,它带来了 RedisOperations 这个类,于是 RedisAutoConfiguration 上的 @ConditionalOnClass(RedisOperations.class) 成立,Redis 的自动配置就生效了。
4.2 自动配置是怎么被发现的#
自动配置类分散在各个 jar 里,Spring 怎么知道有哪些?
这需要一套 SPI(Service Provider Interface)机制:约定一个位置,让每个 jar 把自己提供的东西登记进去,框架启动时统一扫描。
Spring Boot 用过两代方案,而这次迁移是理解这套机制的好例子。
Spring Boot 1.0 到 2.7 使用 META-INF/spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfigurationproperties格式是「接口全限定名 = 实现类列表」,一个文件可以登记多种扩展点,不止自动配置。
它的问题:
- 一个文件混装多种类型,
EnableAutoConfiguration、ApplicationListener、EnvironmentPostProcessor全挤在一起,解析时要全部读进来再按 key 过滤; - 行尾反斜杠续行,几百个类名连成一行,改动时 diff 极其难看,合并冲突频繁;
- properties 格式对类名里的特殊字符不友好,内部类的
$需要注意转义; - 无法被构建工具有效校验,写错一个类名要到运行时才发现。
Spring Boot 2.7 引入、3.0 起成为唯一方案:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importstext内容就是纯粹的类名列表,一行一个:
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfigurationtext改进点:
- 文件名即用途,一个文件只登记一种东西,不需要按 key 过滤;
- 一行一个类名,diff 干净,合并冲突大幅减少;
- 支持
#注释,可以说明为什么某个配置被临时注释掉; - 配合新的
@AutoConfiguration注解(它组合了@Configuration(proxyBeanMethods = false)和排序注解),语义更明确。
Spring Boot 3.0 完全移除了对 spring.factories 中 EnableAutoConfiguration 键的支持。 这意味着任何自定义 starter 如果只有旧文件,在 Boot 3 下会静默失效——不报错,自动配置就是不生效。
4.3 写一个 starter 需要哪些部分#
把前面所有东西串起来,一个自定义 starter 的完整结构:
my-spring-boot-starter/
├── pom.xml ← 只管依赖,不放代码
└── my-spring-boot-autoconfigure/ ← 实际的自动配置
├── MyAutoConfiguration.java
├── MyProperties.java
└── src/main/resources/META-INF/
├── spring/
│ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
└── spring-configuration-metadata.json ← IDE 配置提示(可选)text自动配置类本身:
@AutoConfiguration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 让用户能覆盖
public MyService myService(MyProperties props) {
return new MyService(props.getEndpoint(), props.getTimeout());
}
}java四个要点,每一个都对应前面讲过的机制:
@AutoConfiguration替代旧的@Configuration,它内置了proxyBeanMethods = false(少一层 CGLIB 代理,启动更快)和排序支持;@ConditionalOnClass让这个配置只在相关依赖存在时生效——这就是 starter 「开关」职责的实现;@ConditionalOnMissingBean让用户的定义优先,靠的是第二节讲的DeferredImportSelector时序;@EnableConfigurationProperties把application.yml里的配置绑定成对象。
4.4 为什么这套机制值得单独理解#
自动配置 + SPI 这套组合,解决的是一个很普遍的问题:
框架怎么在「不知道用户会用什么」的前提下,提供开箱即用的默认行为,同时不妨碍用户自定义?
Spring Boot 的答案有三个要素,每一个都可以单独迁移到别的地方:
| 要素 | 作用 | 类比 |
|---|---|---|
| SPI 登记文件 | 让框架发现分散在各处的扩展 | Java 的 ServiceLoader、Node 的 package.json exports |
| 条件化生效 | 根据环境决定哪些扩展启用 | 编译期的条件编译、特性开关 |
| 用户优先的时序 | 保证自定义能覆盖默认 | CSS 的层叠、配置文件的优先级链 |
三个要素缺一不可:只有 SPI 没有条件,就得手动开关每个扩展;有条件但时序错了,用户就覆盖不掉默认值。
「约定优于配置」是这套机制产生的效果,不是它的实现方式。 把效果当成实现方式来记忆,就只能停留在「魔法」的层面。
五、方法论:怎么读一个框架#
5.1 先找「什么时候执行」,再找「执行了什么」#
这次拆解最大的收获是这条。
引子里那个问题之所以卡住我,是因为我一直在看 @ConditionalOnMissingBean 做了什么(查容器里有没有 Bean),而问题的答案在于它什么时候做(在所有用户配置解析完之后、任何实例化开始之前)。
框架代码和业务代码最大的区别就在这里:业务代码的执行顺序基本就是你写的顺序,而框架代码的执行顺序是框架决定的,通常和你写的位置毫无关系。
所以读框架时,先建立时间轴:
Spring Boot 启动的关键时刻:
1. SpringApplication.run()
2. 准备 Environment(读配置文件、命令行参数)
3. 创建 ApplicationContext
4. ─ BeanDefinitionRegistryPostProcessor ─ 配置类解析、自动配置导入
5. ─ BeanFactoryPostProcessor ─ 修改 BeanDefinition(如占位符替换)
6. 注册 BeanPostProcessor
7. ─ finishBeanFactoryInitialization ─ 实例化所有非懒加载单例
8. ApplicationRunner / CommandLineRunnertext有了这条轴,很多问题变成了查表:
- 「为什么我在
@PostConstruct里拿不到某个 Bean」→ 第 7 步中间,依赖的 Bean 可能还没造; - 「为什么我改了
BeanDefinition没生效」→ 你的代码在第 7 步之后跑,太晚了; - 「为什么
@Value没被解析」→ 占位符替换在第 5 步,你的对象不是容器管理的。
5.2 区分「机制」和「策略」#
框架里的东西可以分成两类,混淆它们会导致学错重点:
机制是骨架,很少变化,理解它一劳永逸:
- BeanDefinition 与实例的两阶段划分;
BeanPostProcessor的扩展点模型;DeferredImportSelector的延迟处理。
策略是具体决策,会随版本变化:
- 自动配置文件从
spring.factories换成AutoConfiguration.imports; - 循环依赖从默认允许改成默认禁止;
- 从运行时条件判断走向 AOT 构建期判断。
学机制,查策略。 机制理解了,策略变化只是查一下文档的事。反过来,只记住了策略(比如背下了 spring.factories 的写法),升个大版本就全废了。
判断标准很简单:这个东西如果变了,会不会影响我对系统的整体理解? 会,那是机制;不会,只是要改几行配置,那是策略。
5.3 用「如果没有它会怎样」来验证理解#
三级缓存那节用的就是这个方法。「为什么需要第三级」这个问题,等价于「如果只有两级会怎样」——推下去发现代理对象会不一致,第三级的必要性就出来了。
这个方法对框架里那些「看起来多余」的设计特别有效:
| 设计 | 如果没有它 |
|---|---|
| 第三级缓存 | 循环依赖 + AOP 时,注入的是未代理对象 |
DeferredImportSelector | 自动配置可能先于用户配置执行,@ConditionalOnMissingBean 失效 |
| ASM 读注解元数据 | 类路径缺少依赖时,读注解直接 NoClassDefFoundError |
spring-autoconfigure-metadata | 启动时要逐个加载 100 多个类做条件判断 |
如果回答不上「没有它会怎样」,说明还没理解它解决的问题——这时候记住的只是它的存在,不是它的作用。
5.4 读源码从扩展点入手#
Spring 的源码量很大,从 SpringApplication.run() 一行行读下去会迷失。
更有效的入口是扩展点,因为扩展点是框架主动暴露给你的,它们的位置和时机都有明确定义:
| 扩展点 | 时机 | 典型用途 |
|---|---|---|
ApplicationContextInitializer | 上下文创建后、刷新前 | 注册额外的属性源 |
BeanDefinitionRegistryPostProcessor | 定义注册阶段 | 动态注册 Bean(MyBatis 的 Mapper 扫描) |
BeanFactoryPostProcessor | 定义注册完成后 | 批量修改 BeanDefinition |
BeanPostProcessor | 每个 Bean 初始化前后 | AOP、@Autowired 注入 |
ApplicationListener | 各个生命周期事件 | 启动完成后的初始化 |
顺着扩展点读,能同时看到「框架在哪个位置留了口子」和「框架自己怎么用这个口子」。 Spring 的很多核心功能(AOP、注解注入、配置绑定)都是用自己的扩展点实现的,读它们比读框架内部逻辑更有收获。
5.5 最后#
Spring Boot 被称为「魔法」,是因为它做了大量你没写的事。但拆开之后,这些事情的机制其实相当朴素:
- 自动配置 = 条件化的 BeanDefinition 注册 + 严格的时序保证;
- 循环依赖 = 在实例化阶段提前暴露引用,并用一个 lazy 决策点兼容 AOP;
- starter = 一组依赖 + 一个 SPI 登记文件。
没有一处需要「记住它就是这样」。每一处都能从「它要解决什么问题」推出来。
回到引子那个问题:@ConditionalOnMissingBean 为什么能算准?
因为它压根不需要「算」。时序已经保证了,轮到它执行的时候,用户的所有定义都已经登记完毕了。 它只是查一下表而已。
参考#
- Spring Framework Reference, The IoC Container — 尤其是
BeanFactory生命周期与BeanPostProcessor部分. - Spring Boot Reference, Creating Your Own Auto-configuration 与 Condition Annotations.
- Spring Boot 2.7 Release Notes, New
AutoConfiguration.importsfile. - Spring Boot 3.0 Migration Guide, Auto-configuration registration 与 AOT processing.
- Johnson, Rod. Expert One-on-One J2EE Design and Development. Wrox, 2002.(IoC 思想的最初论述)
- Fowler, Martin. Inversion of Control Containers and the Dependency Injection Pattern. 2004.
本站相关: