Chapter 02

原理(上):容器启动、Bean 生命周期与扩展点

上一章钉下了词汇,并埋了一根线——注入有"时机差异"。这一章把那根线拉直:看清每个 bean 走的固定生命周期,以及 @Autowired 注入到底发生在哪一步、由谁完成。这是全教程的核心。

本章你将建立的 schema

  • refresh() 把"收配方"和"造实例"分成两段,两类后置处理器各自插在哪一段
  • Bean 生命周期三步:实例化 → 属性填充 → 初始化,每一步开放了什么扩展点
  • @Autowired 不是容器内置魔法,而是一个 BeanPostProcessor 在属性填充阶段干的活
  • BeanFactoryPostProcessor 改配方、BeanPostProcessor 改实例——区分这两者是第一道分水岭
  • 循环依赖的三级缓存为什么是"三级",以及第三级为谁而设

2.1refresh():容器启动的总指挥

调用 new AnnotationConfigApplicationContext(AppConfig.class) 时,真正干活的是一个叫 refresh() 的模板方法(定义在 AbstractApplicationContext)。它是一串固定顺序的步骤,整个容器的启动都在这串步骤里发生。先看全景,再逐步拆。

1 · prepareRefresh 准备:设状态、校验必填属性 2 · obtainFreshBeanFactory 载入所有 BeanDefinition(配方就位,无实例) 3 · prepareBeanFactory 配置内核:类加载器、SpEL、内置 Aware 4 · invokeBeanFactoryPostProcessors BFPP 运行:改写配方、解析 @Configuration 5 · registerBeanPostProcessors 仅注册 BPP(登记排序,尚未运行) 6 · 初始化事件 / 消息源 / 监听器 onRefresh(Boot 在此启嵌入式容器) 7 · finishBeanFactoryInitialization 急切造所有单例 → doCreateBean,BPP 在此运行 8 · finishRefresh 发布 ContextRefreshedEvent,容器就绪
图 2.1refresh() 的固定步骤。注意:朱红三步——第 4 步 BFPP 改配方、第 5 步只登记 BPP 不运行、第 7 步才真正造 bean 并让 BPP 上场。"改配方"和"改实例"被一道实例化分界线(第 4 与第 7 步之间)隔开。

这套固定顺序解决什么:如果造 bean 的过程没有明确的阶段划分,"想在所有 bean 造好前改一批配方""想在每个 bean 初始化后包一层代理"这类需求就无处安放。refresh() 把启动切成有序阶段,每个阶段暴露一个确定的插入点——扩展点之所以能"准时"生效,靠的就是这套顺序。

表 2.1 · 没有固定阶段会怎样(痛点 → 设计回应)
没有阶段划分时的痛点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 里的三步流水线。这三步是本教程的脊梁——把它刻进脑子,后面所有"魔法"都能在这条线上找到自己的位置。

① 实例化 createBeanInstance · 构造器造原始对象 ② 属性填充 populateBean · 注入依赖 @Autowired 注入在这一步 AutowiredAnnotationBeanPostProcessor ③ 初始化 initializeBean a. Aware 回调(BeanNameAware 等) b. BPP · postProcessBeforeInitialization c. init 回调(@PostConstruct / afterPropertiesSet) d. BPP · postProcessAfterInitialization → AOP 代理在此生成(03 章) 成品 bean 放入单例缓存,对外提供
图 2.2Bean 生命周期三步——本教程的脊梁。注意:两处朱红都是 BeanPostProcessor 的工作——@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 对象)。

这是新手最常混淆的一对,名字只差三个字母,工作对象和时机却完全不同。把它们的时机摆到一条线上看:

启动时间 → 实例化分界线 BeanFactoryPostProcessor 改配方 · 作用于 BeanDefinition 实例化之前 · 全部配方就位 例:占位符替换、@Configuration 解析 BeanPostProcessor 改实例 · 作用于 bean 对象 每个 bean 创建中 · 实例已存在 例:@Autowired 注入、AOP 代理
图 2.3两类处理器以"实例化分界线"分隔。注意:分界线左边还没有任何 bean 实例,BFPP 只能碰元数据;右边实例已存在,BPP 才能动对象本身。记混的根源就是忽略了这条线。
两类处理器的接口长相Java
// 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 是怎么接进来的"的全部秘密。

表 2.2 · BFPP vs BPP
维度BeanFactoryPostProcessorBeanPostProcessor
作用对象BeanDefinition(配方 / 元数据)bean 实例(对象)
时机实例化之前(refresh 第 4 步)每个 bean 创建中(第 7 步)
能做什么改 scope、改属性值、加/删 BeanDefinition注入、包装成代理、改字段
典型实现PropertySourcesPlaceholderConfigurer、ConfigurationClassPostProcessorAutowiredAnnotationBeanPostProcessor、AOP 自动代理创建器

带来的代价:BPP 对每一个 bean 的创建都会被回调,链路上挂太多 BPP 会拖慢启动;而且实现 BPP 的 bean 必须比普通 bean 更早创建(容器要先有处理器才能处理别人),这导致一个隐蔽陷阱——一个实现了 BPP 的 bean,因为被实例化得太早(负责注入的那个 BPP 还没轮到它),它自己身上的 @Autowired 会被跳过。这个陷阱 03 章会再点。

2.4循环依赖与三级缓存:第三级为谁而设

三级缓存让"还没造完的单例"能被提前暴露引用,从而解开 setter/字段注入的循环依赖。

回到 01 章埋的线:A 依赖 B、B 依赖 A。用字段注入,容器怎么不卡死?答案是它在每个 bean"造了壳、还没填完"时,就把一个引用提前挂出去。这套机制有三层缓存(都在 DefaultSingletonBeanRegistry 里):

① singletonObjects(一级) 成品 bean · 完全可用 ② earlySingletonObjects(二级) 早期引用 · 半成品(含早期代理) ③ singletonFactories(三级) 工厂 · 调用后产出早期引用 取用即升级 造完即升级 Bean A Bean B 需要 B 需要 A 字段 / setter 注入:可解 构造器注入:无解
图 2.4三级缓存与引用升级方向(三级 → 二级 → 一级)。注意:第三级存的不是对象,是工厂——只有循环真的发生、有人来取时才调用它产出早期引用。这个"延迟"正是第三级存在的唯一理由。

为什么是"三级"而不是两级

一个自然的疑问:要解循环依赖,似乎"一个存成品、一个存半成品"两级就够了。第三级(工厂)为什么必须存在?答案只有一个词:AOP 代理。

表 2.3 · 两级缓存 vs 三级缓存(备选方案)
方案能解普通循环依赖吗遇到 AOP 代理时
两级(成品 + 半成品对象)能提前暴露的是原始对象,但 B 最终拿到的应是 A 的代理——两者不一致,B 持有了错的引用
三级(成品 + 半成品 + 工厂)能工厂被调用时才决定"要不要包代理",保证早期暴露的引用和最终引用是同一个

关键在于:第三级存的是一个"工厂"而不是一个现成对象。只有当循环真的发生、B 真的来取 A 时,工厂才被调用,那一刻才决定"A 需不需要被 AOP 代理"。如果不需要代理,早期引用就是原始对象;如果需要,早期引用就是提前生成的代理。这样无论是否有代理,B 拿到的引用都和容器里最终的 A 始终是同一个。没有 AOP,两级确实够用——第三级是为"AOP 早期代理的正确性"专门加的。

为什么构造器注入无解:三级缓存能起作用,前提是"对象已经实例化(壳造好了),只是还没填完属性"。而构造器注入的依赖是构造器参数——对象在拿到依赖之前根本造不出壳,无法被提前暴露到任何缓存。所以 A、B 互为构造器依赖时,谁都到不了"能被暴露"的状态,启动期直接抛 BeanCurrentlyInCreationException。这不是配置开关能关掉的,是机制的硬限制。

2.5跨概念综合:跟一个 bean 走完全程

把本章四节串起来,用 01 章的 OrderService(字段注入 PaymentGateway,且假设 PaymentGateway 又依赖 OrderService 形成循环,OrderService 还被一个日志切面增强)走一遍:

  1. refresh 第 4 步:BFPP 解析 @Configuration、把占位符配置就位(2.1 + 2.3)。
  2. refresh 第 7 步开始急切造单例,轮到 OrderService。
  3. 实例化:构造器造出 OrderService 原始壳;随即把它的"工厂"放进三级缓存(2.2 + 2.4)。
  4. 属性填充:要注入 PaymentGateway,于是去造它;PaymentGateway 填充时反过来要 OrderService,从三级缓存取出工厂、产出 OrderService 的早期引用(因为有切面,这个早期引用是代理),升级到二级缓存(2.4)。
  5. PaymentGateway 拿到这个代理引用、造完进一级缓存;回到 OrderService 继续填充完成。
  6. 初始化:跑 @PostConstruct,最后 BPP 后置回调确认/生成代理,OrderService 成品(代理)进一级缓存(2.2)。

整条链上没有一处"魔法"——每一步都是某个固定阶段里、某个后置处理器或缓存动作的结果。这就是把 Spring 当状态机看的样子。

§本章 self-check

先合上教程,把答案写下来。第 4 题是跨机制综合题,是本章的重点。

  1. refresh() 里 BFPP 和 BPP 分别在第几步登场?"注册 BPP"和"运行 BPP"是同一步吗?
  2. Bean 生命周期三步分别叫什么?@Autowired 注入和 AOP 代理各发生在哪一步?
  3. 用一句话区分 BeanFactoryPostProcessor 和 BeanPostProcessor 的工作对象。
  4. (综合)已知 AOP 代理在初始化末尾生成、三级缓存第三级存的是工厂——把这两点连起来,解释为什么"有循环依赖且被 AOP 增强"的 bean 必须靠三级(而非两级)缓存才能拿到一致的引用。
答案(先做完再展开)
  1. BFPP 在第 4 步运行;BPP 在第 5 步只"注册"(登记+排序,不运行),到第 7 步造每个 bean 时才"运行"。注册和运行不是同一步。
  2. 实例化 → 属性填充 → 初始化。@Autowired 在属性填充(populateBean),AOP 代理在初始化最后一步(postProcessAfterInitialization)。
  3. BFPP 改的是 BeanDefinition(配方、实例化前);BPP 改的是 bean 实例(对象、创建中)。
  4. 被增强的 bean,其最终引用是代理而非原始对象。若循环时提前暴露的是原始对象(两级方案),依赖方会持有和容器最终版本不一致的引用。三级缓存把"暴露什么"延迟到工厂被调用那一刻,那时才决定是否生成代理,从而保证提前暴露的引用与最终引用是同一个代理对象。
进阶挑战 · 刚好够不着

预判一个"注入了 null"的现场

有人写了一个实现 BeanPostProcessor 的类,同时在它里面 @Autowired 了一个 DataSource,运行时发现这个 DataSource 是 null。结合 2.3 节"BPP 必须更早创建"的代价,解释为什么。

提示(卡住再展开)

负责处理 @Autowired 的也是一个 BPP。要让所有 BPP 能工作,容器必须先把 BPP 们都创建出来——于是这个自定义 BPP 被实例化得非常早,早到"负责注入的那个 BPP"还没准备好对它生效,它的 @Autowired 就被跳过了。修法:用构造器注入,或在用到时通过容器懒查。