主题
Bean 生命周期与扩展点全家桶
本文是 Spring 系统学习系列的 L2 核心篇。前置:容器启动流程:从 refresh() 到 BeanDefinition。学完可以配合面试题食用:Spring Bean 生命周期详解、Spring 循环依赖与三级缓存源码解析
为什么需要搞懂生命周期
写业务代码时感知不到 Bean 的生命周期,出问题时才会:@PostConstruct 里拿到的配置是 null、两个 Bean 相互依赖导致启动报错、给原型 Bean 加了 @PreDestroy 却永远不执行。这些问题的根源都在创建流水线的顺序上。上一篇看到 refresh() 最后的 finishBeanFactoryInitialization 会把所有非懒加载单例真正创建出来,这一篇把"创建"两个字拆开:对象在哪一步诞生、属性在哪一步注入、Spring 留了哪些合法的插队窗口、容器关闭时又发生什么。
生命周期主线:四步走完一生
单例 Bean 的创建集中在 AbstractAutowireCapableBeanFactory#doCreateBean,主线四步:
- 实例化(createBeanInstance):反射调用构造器,对象诞生,此时所有字段都是默认值
- 属性填充(populateBean):处理 @Autowired、@Value,依赖的其它 Bean 在这一步被递归创建并注入
- 初始化(initializeBean):先回调 Aware 系列接口,再走 BeanPostProcessor 前置、init 三连、后置
- 就绪:成品放入单例池(singletonObjects),此后 getBean 直接命中缓存
容器 close() 时对所有单例执行销毁链:@PreDestroy -> DisposableBean#destroy -> 自定义 destroy-method。注意初始化三连(@PostConstruct -> afterPropertiesSet -> init-method)和销毁三连是同构的:注解优先,接口居中,配置殿后。
mermaid
flowchart LR
A[实例化<br/>反射调构造器] --> B[属性填充<br/>@Autowired/@Value]
B --> C[Aware 回调<br/>BeanName/BeanFactory]
C --> D[BPP 前置<br/>@PostConstruct 在此窗口]
D --> E[init 三连<br/>注解/接口/init-method]
E --> F[BPP 后置<br/>AOP 代理在这生成]
F --> G[就绪<br/>进入单例池]
G --> H[容器 close<br/>销毁三连]五个扩展点各管一段
| 扩展点 | 回调时机 | 典型用途 |
|---|---|---|
| Aware 系列 | 属性填充后、初始化前 | 拿到 Bean 名、BeanFactory、ApplicationContext 的引用 |
| @PostConstruct | BPP 前置阶段(由 CommonAnnotationBeanPostProcessor 触发) | 依赖注入完成后初始化状态:预热缓存、校验配置 |
| InitializingBean#afterPropertiesSet | @PostConstruct 之后、init-method 之前 | 同上,历史接口,会把代码耦合进 Spring |
| BeanPostProcessor | 每个 Bean 初始化的前后各一次 | 加工别的 Bean:替换、包装、生成代理 |
| SmartInitializingSingleton | 所有单例预实例化完成后,只调一次 | 全局一次性初始化,前提是别的 Bean 都就绪 |
选型原则一句话:初始化自己的状态用 @PostConstruct(三个 init 手段里最不侵入);要等其他 Bean 全部就绪再动作用 SmartInitializingSingleton;要加工别的 Bean 用 BeanPostProcessor。
BeanPostProcessor 需要特别注意:它对容器里所有 Bean 生效,是容器级设施。Spring 自己大量用它干活,@Autowired 靠 AutowiredAnnotationBeanPostProcessor 解析注入,@PostConstruct 靠 CommonAnnotationBeanPostProcessor 触发,AOP 代理靠 AbstractAutoProxyCreator 在后置阶段生成。日常业务很少直接写它,但看 Spring 源码时遇到的八成" Processor"都是这一家子。
三级缓存:循环依赖的解法
A 依赖 B,B 又依赖 A,最朴素的创建顺序会死循环。Spring 的解法是三级缓存,都在 DefaultSingletonBeanRegistry 里:
- 一级 singletonObjects:成品单例,走完了全部四步
- 二级 earlySingletonObjects:提前曝光的半成品,实例化了但没走完初始化
- 三级 singletonFactories:ObjectFactory 工厂,调用后才产出早期引用
流程拿 A、B 互相依赖举例:A 实例化之后、属性填充之前,把自己的 ObjectFactory 放进三级缓存;A 填充属性时触发创建 B;B 填充属性时需要 A,依次查一、二、三级缓存,在三级缓存里找到工厂,调用工厂拿到 A 的早期引用(如果 A 需要 AOP,这里拿到的就是提前生成的代理),放进二级缓存;B 顺利走完创建进入一级缓存;A 拿到 B 的引用,把自己的初始化走完。
为什么是三级不是两级?只用两级的话,A 必须在实例化后立刻决定是否生成代理并放进二级缓存。但 Spring 的设计是代理在初始化之后(BPP 后置)生成,两级缓存会强行改变所有 Bean 的行为,包括绝大多数没有循环依赖的 Bean。三级缓存用工厂把"要不要提前生成代理"这个决策推迟到真的发生循环依赖那一刻才做,没循环依赖就完全按原流程走。代价也有:构造器注入的循环依赖(实例化阶段就需要对方)和原型 Bean 的循环依赖,三级缓存都救不了,只能改造依赖关系或改用 setter 注入。源码级细节可以看面试题 03 那篇。
单例 vs 原型:容器管到哪一步为止
单例的完整一生都由容器负责:创建、注入、初始化、缓存、关闭时销毁,销毁链一定会执行。
原型每次 getBean 都重新走一遍创建流水线,但容器把实例交出去之后就不再持有引用,也就不知道它何时死亡。所以销毁三连(@PreDestroy、DisposableBean、destroy-method)对原型全部无效,需要释放资源就自己管理。原型同样不受 SmartInitializingSingleton 照顾(它只遍历单例),也享受不到三级缓存的循环依赖解法。
动手实操:一个 Bean 打满全部扩展点
写一个类把所有扩展点都实现一遍,配上一个探针 Processor,直接观察调用顺序:
java
@Component("lifecycleDemoBean")
public class LifecycleDemoBean implements BeanNameAware, BeanFactoryAware,
InitializingBean, DisposableBean, SmartInitializingSingleton {
public LifecycleDemoBean() {
System.out.println("1. 构造器:对象诞生,字段全是默认值");
}
@Override
public void setBeanName(String name) {
System.out.println("2. BeanNameAware:我的名字是 " + name);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
System.out.println("3. BeanFactoryAware:拿到工厂引用");
}
@PostConstruct
public void annotateInit() {
System.out.println("5. @PostConstruct:注入完成,适合预热缓存");
}
@Override
public void afterPropertiesSet() {
System.out.println("6. InitializingBean#afterPropertiesSet");
}
// 配合 @Bean(initMethod = "customInit") 使用,@Component 默认不识别 initMethod
public void customInit() {
System.out.println("7. 自定义 init-method");
}
@Override
public void afterSingletonsInstantiated() {
System.out.println("所有单例就绪后:SmartInitializingSingleton,只调一次");
}
@PreDestroy
public void annotateDestroy() {
System.out.println("9. @PreDestroy:容器关闭,注解销毁先走");
}
@Override
public void destroy() {
System.out.println("10. DisposableBean#destroy");
}
}java
@Component
public class LifecycleProbeProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
if ("lifecycleDemoBean".equals(beanName)) {
System.out.println("4. BPP 前置:@PostConstruct 就发生在这个窗口");
}
return bean; // 返回值会替换原 Bean,返回 null 会出问题
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if ("lifecycleDemoBean".equals(beanName)) {
System.out.println("8. BPP 后置:AOP 代理在这里替换原对象");
}
return bean;
}
}java
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
ctx.close(); // 触发销毁三连输出顺序固定为:1 构造器 -> 2 setBeanName -> 3 setBeanFactory -> 4 BPP 前置 -> 5 @PostConstruct -> 6 afterPropertiesSet -> 7 init-method -> 8 BPP 后置 -> afterSingletonsInstantiated(此时其它单例也都好了)-> close 时 9 @PreDestroy -> 10 destroy -> 11 destroy-method。把这个 Demo 跑一遍,比背十遍面试答案记得牢。
常见误区与小结
- 在构造器里访问 @Autowired 字段——属性填充还没发生,拿到的是 null,构造器只负责让对象诞生
- 给原型 Bean 加 @PreDestroy 期望容器来调——容器不跟踪原型,销毁回调不会执行
- 在 @PostConstruct 里做重活——它阻塞的是整个单例预实例化过程,耗时操作考虑放 ApplicationRunner 或异步执行
- 把 BeanPostProcessor 当业务工具随手写——它影响容器内所有 Bean,写错波及面是全局的
- 以为三级缓存能解一切循环依赖——构造器注入和原型 Bean 的循环依赖它无能为力
小结:上一篇看容器怎么把图纸变成实例,这一篇拆开了单个实例内部每一站的顺序,BPP 后置正是 AOP 代理的生成入口。下一篇 AOP 与事务源码 就顺着这个入口往下走。
参考
- Spring 官方文档:Core Technologies - Lifecycle Callbacks 与 Dependencies
- AbstractAutowireCapableBeanFactory#doCreateBean、DefaultSingletonBeanRegistry#getSingleton 源码
- 《Spring 源码深度解析》第 5 章