Appearance
Bean 生命周期与循环依赖
如果说 Spring 容器里哪条主线最容易“平时会用,但一问原理就散”,那通常就是:
- Bean 生命周期
- 循环依赖
这两件事本质上都和同一个问题有关:Spring 容器到底是怎么创建、装配和维护对象的。
这篇文章重点讲:
- Bean 生命周期到底分哪些阶段
- Spring 为什么能在创建过程中插入那么多扩展逻辑
- 循环依赖到底是什么
- Spring 为什么只能解决一部分循环依赖
- 工程上该怎么看待循环依赖问题
1. Bean 生命周期到底是什么
在 Spring 里,一个 Bean 不是简单地 new 出来就结束了。
Bean 从创建到可用,再到容器关闭时销毁,中间通常会经历一整套生命周期流程。
它主要解决的是 对象在进入 Spring 容器管理后,不只是被创建,还要被装配、初始化、增强和销毁。
2. Bean 生命周期大致分哪些阶段
如果先用最容易记的方式理解,可以分成 4 段:
- 实例化
- 依赖注入
- 初始化
- 销毁
这是最适合先建立骨架的版本。
一张流程图,会更容易把后面的各种扩展点放到正确位置:
mermaid
flowchart TD
A[实例化 Bean] --> B[依赖注入]
B --> C[Aware 回调]
C --> D[BeanPostProcessor 前置处理]
D --> E[执行初始化回调]
E --> F[执行自定义初始化方法]
F --> G[BeanPostProcessor 后置处理]
G --> H[Bean 可用]
H --> I[容器关闭]
I --> J[执行销毁回调]
J --> K[执行自定义销毁方法]3. 如果再稍微展开一点,生命周期里到底发生了什么
更完整一点的过程,通常可以看成:
- 容器先实例化对象
- 再给对象注入依赖
- 执行各种感知和前置处理
- 执行初始化逻辑
- 再执行初始化后处理
- Bean 进入可用状态
- 容器关闭时执行销毁逻辑
这里最关键的理解是:Spring 能在 Bean 创建前后插入很多扩展点,所以事务、代理、增强这些能力才有机会挂进去。
3.1 一个更直观的生命周期示例
java
@Component
public class DemoBean implements BeanNameAware, InitializingBean, DisposableBean {
@Override
public void setBeanName(String name) {
// Bean 已经被创建,并拿到了容器分配给它的名称
System.out.println("BeanNameAware: " + name);
}
@PostConstruct
public void postConstruct() {
// 依赖注入完成后,执行基于注解的初始化回调
System.out.println("@PostConstruct");
}
@Override
public void afterPropertiesSet() {
// 依赖注入完成后,执行基于接口的初始化回调
System.out.println("InitializingBean.afterPropertiesSet");
}
@PreDestroy
public void preDestroy() {
// 容器准备销毁 Bean 时,执行基于注解的清理回调
System.out.println("@PreDestroy");
}
@Override
public void destroy() {
// 容器准备销毁 Bean 时,执行基于接口的清理回调
System.out.println("DisposableBean.destroy");
}
}这个例子想表达的重点不是“把接口都背下来”,而是:同一个 Bean 在不同生命周期阶段,确实会有不同扩展点被触发。
4. 为什么 BeanPostProcessor 这么重要
BeanPostProcessor 可以看成 Bean 初始化前后可插入处理逻辑的一类扩展点。
它解决的是 容器在 Bean 创建过程中,如何统一插入增强和加工逻辑。
这件事非常关键,因为很多 Spring 能力最后都要落到这里。
例如:
- 代理包装
- 自动增强
- 一些框架级处理逻辑
都可能和这类扩展点有关。
所以如果要讲 Spring 为什么“不只是 new 对象”,BeanPostProcessor 就是很重要的一环。
4.1 一个 BeanPostProcessor 示例
java
@Component
public class TraceBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
System.out.println("before init: " + beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("after init: " + beanName);
return bean;
}
}这类扩展点很关键,因为很多框架能力本质上都是在 Bean 初始化前后“加工对象”。
5. Aware、初始化方法、销毁方法这些词到底在说什么
这些词本质上都在回答:Bean 在生命周期不同阶段,怎么拿到容器能力,怎么执行自定义初始化和销毁逻辑。
5.1 Aware
它更偏:让 Bean 感知容器中的某些能力或上下文信息。
5.2 初始化方法
它解决的是 依赖都注入完成后,Bean 自己是否还需要做一些额外准备动作。
5.3 销毁方法
它解决的是 容器关闭时,Bean 是否需要释放资源。
5.4 生命周期相关常见注解和接口
| 注解 / 接口 | 作用 |
|---|---|
@PostConstruct | 依赖注入完成后执行初始化逻辑 |
@PreDestroy | Bean 销毁前执行清理逻辑 |
BeanNameAware | 感知当前 Bean 名称 |
ApplicationContextAware | 感知容器上下文 |
InitializingBean | 初始化回调接口 |
DisposableBean | 销毁回调接口 |
6. 循环依赖到底是什么
循环依赖 可以看成 对象之间依赖关系形成了环。
最典型的情况就是:
- A 依赖 B
- B 又依赖 A
这时容器在创建对象时,就可能陷入:A 还没创建完,需要 B;B 还没创建完,又需要 A。
例如字段注入形成的循环依赖:
java
@Component
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
@Component
public class ServiceB {
@Autowired
private ServiceA serviceA;
}7. 为什么循环依赖会成为问题
因为容器创建对象时,通常要把依赖关系装配起来。
如果依赖关系形成闭环,就会出现:
- 对象还没准备好
- 另一个对象又急着要它
- 最后谁都完不成完整初始化
所以循环依赖问题的核心不是“语法错误”,而是:对象图在创建阶段形成了无法直接按顺序完成的依赖链。
8. Spring 为什么能解决一部分循环依赖
这是最容易被问到的点。
把边界摆清楚:Spring 不是能解决所有循环依赖,而是能解决一部分单例 Bean 场景下的循环依赖。
它的大致思路可以看成:
- 先创建对象的早期形态
- 提前暴露一个可引用对象
- 让另一个 Bean 先能拿到这个引用
- 再继续把后续初始化走完
也就是说,它不是“神奇地忽略了依赖环”,而是:通过提前暴露对象引用,把创建流程拆开处理。
如果把这个过程画成流程,会更容易理解为什么它只能解决一部分场景:
mermaid
flowchart TD
A[创建单例 Bean A] --> B[实例化 A 的早期对象]
B --> C[提前暴露早期引用]
C --> D[给 A 注入依赖 B]
D --> E[开始创建 Bean B]
E --> F[发现 B 依赖 A]
F --> G[从早期引用中拿到 A]
G --> H[B 完成注入与初始化]
H --> I[回到 A]
I --> J[A 完成注入与初始化]
J --> K[最终 Bean 进入单例池]8.1 为什么构造器循环依赖更难解
看下面这个例子会更直观:
java
@Component
public class ServiceA {
public ServiceA(ServiceB serviceB) {
}
}
@Component
public class ServiceB {
public ServiceB(ServiceA serviceA) {
}
}这时 A 还没实例化完成就需要 B,B 也还没实例化完成就需要 A,容器几乎没有“先暴露一个半成品引用”的空间。
9. 为什么会提到三级缓存
如果再往深一点,循环依赖通常会和“三级缓存”一起出现。
你不用一开始死记每一级细节,但要知道它想解决的是:对象还没彻底初始化完成时,容器如何有控制地提前暴露可用引用。
更稳妥的理解是:
- 一层放完整可用对象
- 一层放早期对象
- 一层放能生成早期引用的工厂
这背后的目标不是“多搞几层结构”,而是:让容器在对象未完全创建时,也能安全地协调一部分依赖关系。
10. 为什么 Spring 只能解决一部分循环依赖
因为不是所有依赖环都能通过“提前暴露引用”解决。
10.1 构造器循环依赖更麻烦
如果 A 构造器要 B,B 构造器又要 A,那对象连“早期形态”都还没出来,就已经卡住了。
所以这类问题通常更难处理。
10.2 非单例作用域场景也没那么容易处理
Spring 默认更擅长处理的是部分单例 Bean 场景。
一旦作用域和生命周期更复杂,循环依赖就更难自动协调。
所以边界要看清:Spring 解决循环依赖是有边界的,不是只要出现依赖环它都能兜住。
11. 为什么说循环依赖“能解决”不等于“应该存在”
这是工程上特别重要的一点。
Spring 能解决一部分循环依赖,不代表循环依赖就是好设计。
因为一旦依赖形成环,往往意味着:
- 模块职责边界不清
- 依赖方向不合理
- 测试和维护变难
- 重构成本更高
所以更稳妥的工程态度通常是:循环依赖应尽量避免,把它当作设计信号,而不是框架能力展示。
12. Bean 生命周期和循环依赖为什么要放在一起讲
因为这两条线其实都在回答同一个底层问题:Spring 容器在对象创建过程中,到底做了哪些事。
Bean 生命周期强调的是:一个对象是怎么一步步变成可用 Bean 的。
循环依赖强调的是:当这个过程被依赖环打断时,容器怎么想办法继续推进。
所以把这两者放在一起,理解会更连贯。
13. 工程上最常见的几个误区
13.1 误区一:Bean 生命周期就是 new 完就结束
不是。
真正的 Bean 创建还包括依赖注入、初始化、后置处理和销毁。
13.2 误区二:Spring 能处理循环依赖,所以循环依赖没问题
这是一种很危险的误解。
框架能兜住一部分,不代表设计就是合理的。
13.3 误区三:只要有三级缓存,就所有循环依赖都能解决
不是。
尤其构造器循环依赖和复杂作用域场景,通常没那么容易处理。
13.4 误区四:Bean 生命周期只是背阶段名词
真正重要的是:为什么这些阶段存在,以及 Spring 能在哪些阶段插入扩展逻辑。
14. 一句话总结
Bean 生命周期解决的是“一个对象如何在 Spring 容器里被创建、装配、初始化和销毁”,循环依赖解决的是“当对象创建顺序被依赖环打断时,容器如何在有限边界内继续推进”;两者一起看,才能真正理解 Spring 容器的底层主线。