Chapter 02
原理(上):容器启动、Bean 生命周期与扩展点
上一章钉下了词汇,并埋了一根线——注入有"时机差异"。这一章把那根线拉直:看清每个 bean 走的固定生命周期,以及 @Autowired 注入到底发生在哪一步、由谁完成。这是全教程的核心。
本章你将建立的 schema
refresh()把"收配方"和"造实例"分成两段,两类后置处理器各自插在哪一段- Bean 生命周期三步:实例化 → 属性填充 → 初始化,每一步开放了什么扩展点
@Autowired不是容器内置魔法,而是一个BeanPostProcessor在属性填充阶段干的活- BeanFactoryPostProcessor 改配方、BeanPostProcessor 改实例——区分这两者是第一道分水岭
- 循环依赖的三级缓存为什么是"三级",以及第三级为谁而设
2.1refresh():容器启动的总指挥
调用 new AnnotationConfigApplicationContext(AppConfig.class) 时,真正干活的是一个叫 refresh() 的模板方法(定义在 AbstractApplicationContext)。它是一串固定顺序的步骤,整个容器的启动都在这串步骤里发生。先看全景,再逐步拆。
这套固定顺序解决什么:如果造 bean 的过程没有明确的阶段划分,"想在所有 bean 造好前改一批配方""想在每个 bean 初始化后包一层代理"这类需求就无处安放。refresh() 把启动切成有序阶段,每个阶段暴露一个确定的插入点——扩展点之所以能"准时"生效,靠的就是这套顺序。
| 没有阶段划分时的痛点 | refresh() 的回应 | 代价 |
|---|---|---|
想在造实例前统一改配置(占位符 ${})却找不到时机 | 第 4 步 BFPP 专门处理"实例化前改配方" | 启动多一遍对全部 BeanDefinition 的遍历 |
| 想给每个 bean 初始化后加增强(代理)却没有挂载点 | 第 7 步造每个 bean 时回调已注册的 BPP | 每个 bean 创建都要过一遍 BPP 链 |
| 配置错误要等到运行时第一次用才暴露 | 第 7 步急切造单例,错误启动期就炸 | 启动变慢、内存上来得早 |
带来的代价:refresh() 默认单线程顺序执行,bean 越多启动越慢;急切实例化把"运行期才发现的错误"提前到启动期(对线上服务是好事,对本地反复重启是负担)。Spring 6.2 起允许部分 bean @Bean(bootstrap = BACKGROUND) 后台并行初始化,正是为缓解大应用启动慢——但这打破了"容器单线程顺序建 bean"的旧心智模型,是近年的一个变化点。
2.2Bean 生命周期三步:@Autowired 其实是一个 BPP
每个 bean 都走同一条流水线:实例化(造原始对象)→ 属性填充(注入依赖)→ 初始化(回调 + 增强)。
图 2.1 第 7 步"造每个 bean",放大看就是 doCreateBean 里的三步流水线。这三步是本教程的脊梁——把它刻进脑子,后面所有"魔法"都能在这条线上找到自己的位置。
@Autowired 在第 ② 步注入、AOP 代理在第 ③d 步生成。所谓"魔法",本质是 BPP 挂在固定节点上的回调。逐步解读(机制,不是 API):
- ① 实例化:
createBeanInstance调构造器造出一个"原始对象"——此时字段还是空的(构造器注入的依赖此刻已就绪,因为它们是构造器参数)。 - ② 属性填充:
populateBean把依赖设进字段。@Autowired的解析就发生在这里,执行者是一个名叫AutowiredAnnotationBeanPostProcessor的后置处理器。注意这句话的份量:@Autowired不是容器写死的内置逻辑,而是一个可插拔的 BPP——理论上你能自己写一个等价的处理器。 - ③ 初始化:先回调各种
Aware接口(把容器自身能力交给 bean),再跑 BPP 的前置回调,然后是初始化方法(@PostConstruct/InitializingBean.afterPropertiesSet),最后跑 BPP 的后置回调。AOP 代理就在最后这一步(postProcessAfterInitialization)生成——这是 03 章的入口。
既然 AOP 代理在第 ③d 步(初始化之后)才生成,那么在第 ③c 步的 @PostConstruct 方法里,this 是被代理过的对象,还是未代理的原始对象?这对"在 @PostConstruct 里调用本类的 @Transactional 方法"意味着什么?
停 10 秒,再展开
是未代理的原始对象。代理在 @PostConstruct 之后才生成,所以初始化方法里的 this 永远是裸对象。后果:在 @PostConstruct 里调用本类带 @Transactional 的方法,既是自调用、对象又还没被代理,事务必然不生效。这是双重保险的失效——03 章会把自调用讲透。
把图 2.2 记成一句话:"实例化造壳、填充塞依赖、初始化做增强;@Autowired 在填充、AOP 在初始化末尾,二者都是 BPP。" 面试被问到任何"X 注解怎么生效",都先回到这条线上定位它在哪一步、由哪个处理器做。
2.3两类后置处理器:改配方的 BFPP vs 改实例的 BPP
BeanFactoryPostProcessor 在实例化前改"配方"(BeanDefinition);BeanPostProcessor 在创建中改"实例"(bean 对象)。
这是新手最常混淆的一对,名字只差三个字母,工作对象和时机却完全不同。把它们的时机摆到一条线上看:
// BFPP:拿到的是整个 BeanFactory,可遍历、改写 BeanDefinition(配方)
public interface BeanFactoryPostProcessor {
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory);
}
// BPP:拿到的是一个个已实例化的 bean,可包装、替换它(实例)
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName);
Object postProcessAfterInitialization(Object bean, String beanName); // 返回值会替换原 bean —— AOP 在此返回代理
}
看 postProcessAfterInitialization 的返回值——它会替换掉容器里的那个 bean。03 章的 AOP 自动代理创建器正是在这里返回一个代理对象,把原始 bean 偷偷换成代理。这一个返回值,就是"AOP 是怎么接进来的"的全部秘密。
| 维度 | BeanFactoryPostProcessor | BeanPostProcessor |
|---|---|---|
| 作用对象 | BeanDefinition(配方 / 元数据) | bean 实例(对象) |
| 时机 | 实例化之前(refresh 第 4 步) | 每个 bean 创建中(第 7 步) |
| 能做什么 | 改 scope、改属性值、加/删 BeanDefinition | 注入、包装成代理、改字段 |
| 典型实现 | PropertySourcesPlaceholderConfigurer、ConfigurationClassPostProcessor | AutowiredAnnotationBeanPostProcessor、AOP 自动代理创建器 |
带来的代价:BPP 对每一个 bean 的创建都会被回调,链路上挂太多 BPP 会拖慢启动;而且实现 BPP 的 bean 必须比普通 bean 更早创建(容器要先有处理器才能处理别人),这导致一个隐蔽陷阱——一个实现了 BPP 的 bean,因为被实例化得太早(负责注入的那个 BPP 还没轮到它),它自己身上的 @Autowired 会被跳过。这个陷阱 03 章会再点。
2.4循环依赖与三级缓存:第三级为谁而设
三级缓存让"还没造完的单例"能被提前暴露引用,从而解开 setter/字段注入的循环依赖。
回到 01 章埋的线:A 依赖 B、B 依赖 A。用字段注入,容器怎么不卡死?答案是它在每个 bean"造了壳、还没填完"时,就把一个引用提前挂出去。这套机制有三层缓存(都在 DefaultSingletonBeanRegistry 里):
为什么是"三级"而不是两级
一个自然的疑问:要解循环依赖,似乎"一个存成品、一个存半成品"两级就够了。第三级(工厂)为什么必须存在?答案只有一个词:AOP 代理。
| 方案 | 能解普通循环依赖吗 | 遇到 AOP 代理时 |
|---|---|---|
| 两级(成品 + 半成品对象) | 能 | 提前暴露的是原始对象,但 B 最终拿到的应是 A 的代理——两者不一致,B 持有了错的引用 |
| 三级(成品 + 半成品 + 工厂) | 能 | 工厂被调用时才决定"要不要包代理",保证早期暴露的引用和最终引用是同一个 |
关键在于:第三级存的是一个"工厂"而不是一个现成对象。只有当循环真的发生、B 真的来取 A 时,工厂才被调用,那一刻才决定"A 需不需要被 AOP 代理"。如果不需要代理,早期引用就是原始对象;如果需要,早期引用就是提前生成的代理。这样无论是否有代理,B 拿到的引用都和容器里最终的 A 始终是同一个。没有 AOP,两级确实够用——第三级是为"AOP 早期代理的正确性"专门加的。
为什么构造器注入无解:三级缓存能起作用,前提是"对象已经实例化(壳造好了),只是还没填完属性"。而构造器注入的依赖是构造器参数——对象在拿到依赖之前根本造不出壳,无法被提前暴露到任何缓存。所以 A、B 互为构造器依赖时,谁都到不了"能被暴露"的状态,启动期直接抛 BeanCurrentlyInCreationException。这不是配置开关能关掉的,是机制的硬限制。
2.5跨概念综合:跟一个 bean 走完全程
把本章四节串起来,用 01 章的 OrderService(字段注入 PaymentGateway,且假设 PaymentGateway 又依赖 OrderService 形成循环,OrderService 还被一个日志切面增强)走一遍:
- refresh 第 4 步:BFPP 解析
@Configuration、把占位符配置就位(2.1 + 2.3)。 - refresh 第 7 步开始急切造单例,轮到
OrderService。 - 实例化:构造器造出
OrderService原始壳;随即把它的"工厂"放进三级缓存(2.2 + 2.4)。 - 属性填充:要注入
PaymentGateway,于是去造它;PaymentGateway填充时反过来要OrderService,从三级缓存取出工厂、产出OrderService的早期引用(因为有切面,这个早期引用是代理),升级到二级缓存(2.4)。 PaymentGateway拿到这个代理引用、造完进一级缓存;回到OrderService继续填充完成。- 初始化:跑
@PostConstruct,最后 BPP 后置回调确认/生成代理,OrderService成品(代理)进一级缓存(2.2)。
整条链上没有一处"魔法"——每一步都是某个固定阶段里、某个后置处理器或缓存动作的结果。这就是把 Spring 当状态机看的样子。
§本章 self-check
先合上教程,把答案写下来。第 4 题是跨机制综合题,是本章的重点。
- refresh() 里 BFPP 和 BPP 分别在第几步登场?"注册 BPP"和"运行 BPP"是同一步吗?
- Bean 生命周期三步分别叫什么?
@Autowired注入和 AOP 代理各发生在哪一步? - 用一句话区分 BeanFactoryPostProcessor 和 BeanPostProcessor 的工作对象。
- (综合)已知 AOP 代理在初始化末尾生成、三级缓存第三级存的是工厂——把这两点连起来,解释为什么"有循环依赖且被 AOP 增强"的 bean 必须靠三级(而非两级)缓存才能拿到一致的引用。
答案(先做完再展开)
- BFPP 在第 4 步运行;BPP 在第 5 步只"注册"(登记+排序,不运行),到第 7 步造每个 bean 时才"运行"。注册和运行不是同一步。
- 实例化 → 属性填充 → 初始化。
@Autowired在属性填充(populateBean),AOP 代理在初始化最后一步(postProcessAfterInitialization)。 - BFPP 改的是 BeanDefinition(配方、实例化前);BPP 改的是 bean 实例(对象、创建中)。
- 被增强的 bean,其最终引用是代理而非原始对象。若循环时提前暴露的是原始对象(两级方案),依赖方会持有和容器最终版本不一致的引用。三级缓存把"暴露什么"延迟到工厂被调用那一刻,那时才决定是否生成代理,从而保证提前暴露的引用与最终引用是同一个代理对象。
预判一个"注入了 null"的现场
有人写了一个实现 BeanPostProcessor 的类,同时在它里面 @Autowired 了一个 DataSource,运行时发现这个 DataSource 是 null。结合 2.3 节"BPP 必须更早创建"的代价,解释为什么。
提示(卡住再展开)
负责处理 @Autowired 的也是一个 BPP。要让所有 BPP 能工作,容器必须先把 BPP 们都创建出来——于是这个自定义 BPP 被实例化得非常早,早到"负责注入的那个 BPP"还没准备好对它生效,它的 @Autowired 就被跳过了。修法:用构造器注入,或在用到时通过容器懒查。