Skip to content

Bean 生命周期与循环依赖

如果说 Spring 容器里哪条主线最容易“平时会用,但一问原理就散”,那通常就是:

  1. Bean 生命周期
  2. 循环依赖

这两件事本质上都和同一个问题有关:Spring 容器到底是怎么创建、装配和维护对象的。

这篇文章重点讲:

  1. Bean 生命周期到底分哪些阶段
  2. Spring 为什么能在创建过程中插入那么多扩展逻辑
  3. 循环依赖到底是什么
  4. Spring 为什么只能解决一部分循环依赖
  5. 工程上该怎么看待循环依赖问题

1. Bean 生命周期到底是什么

在 Spring 里,一个 Bean 不是简单地 new 出来就结束了。

Bean 从创建到可用,再到容器关闭时销毁,中间通常会经历一整套生命周期流程。

它主要解决的是 对象在进入 Spring 容器管理后,不只是被创建,还要被装配、初始化、增强和销毁


2. Bean 生命周期大致分哪些阶段

如果先用最容易记的方式理解,可以分成 4 段:

  1. 实例化
  2. 依赖注入
  3. 初始化
  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. 如果再稍微展开一点,生命周期里到底发生了什么

更完整一点的过程,通常可以看成:

  1. 容器先实例化对象
  2. 再给对象注入依赖
  3. 执行各种感知和前置处理
  4. 执行初始化逻辑
  5. 再执行初始化后处理
  6. Bean 进入可用状态
  7. 容器关闭时执行销毁逻辑

这里最关键的理解是: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 能力最后都要落到这里。

例如:

  1. 代理包装
  2. 自动增强
  3. 一些框架级处理逻辑

都可能和这类扩展点有关。

所以如果要讲 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依赖注入完成后执行初始化逻辑
@PreDestroyBean 销毁前执行清理逻辑
BeanNameAware感知当前 Bean 名称
ApplicationContextAware感知容器上下文
InitializingBean初始化回调接口
DisposableBean销毁回调接口

6. 循环依赖到底是什么

循环依赖 可以看成 对象之间依赖关系形成了环

最典型的情况就是:

  1. A 依赖 B
  2. B 又依赖 A

这时容器在创建对象时,就可能陷入:A 还没创建完,需要 B;B 还没创建完,又需要 A。

例如字段注入形成的循环依赖:

java
@Component
public class ServiceA {

    @Autowired
    private ServiceB serviceB;
}

@Component
public class ServiceB {

    @Autowired
    private ServiceA serviceA;
}

7. 为什么循环依赖会成为问题

因为容器创建对象时,通常要把依赖关系装配起来。

如果依赖关系形成闭环,就会出现:

  1. 对象还没准备好
  2. 另一个对象又急着要它
  3. 最后谁都完不成完整初始化

所以循环依赖问题的核心不是“语法错误”,而是:对象图在创建阶段形成了无法直接按顺序完成的依赖链。


8. Spring 为什么能解决一部分循环依赖

这是最容易被问到的点。

把边界摆清楚:Spring 不是能解决所有循环依赖,而是能解决一部分单例 Bean 场景下的循环依赖。

它的大致思路可以看成:

  1. 先创建对象的早期形态
  2. 提前暴露一个可引用对象
  3. 让另一个 Bean 先能拿到这个引用
  4. 再继续把后续初始化走完

也就是说,它不是“神奇地忽略了依赖环”,而是:通过提前暴露对象引用,把创建流程拆开处理。

如果把这个过程画成流程,会更容易理解为什么它只能解决一部分场景:

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. 为什么会提到三级缓存

如果再往深一点,循环依赖通常会和“三级缓存”一起出现。

你不用一开始死记每一级细节,但要知道它想解决的是:对象还没彻底初始化完成时,容器如何有控制地提前暴露可用引用。

更稳妥的理解是:

  1. 一层放完整可用对象
  2. 一层放早期对象
  3. 一层放能生成早期引用的工厂

这背后的目标不是“多搞几层结构”,而是:让容器在对象未完全创建时,也能安全地协调一部分依赖关系。


10. 为什么 Spring 只能解决一部分循环依赖

因为不是所有依赖环都能通过“提前暴露引用”解决。

10.1 构造器循环依赖更麻烦

如果 A 构造器要 B,B 构造器又要 A,那对象连“早期形态”都还没出来,就已经卡住了。

所以这类问题通常更难处理。

10.2 非单例作用域场景也没那么容易处理

Spring 默认更擅长处理的是部分单例 Bean 场景。

一旦作用域和生命周期更复杂,循环依赖就更难自动协调。

所以边界要看清:Spring 解决循环依赖是有边界的,不是只要出现依赖环它都能兜住。


11. 为什么说循环依赖“能解决”不等于“应该存在”

这是工程上特别重要的一点。

Spring 能解决一部分循环依赖,不代表循环依赖就是好设计。

因为一旦依赖形成环,往往意味着:

  1. 模块职责边界不清
  2. 依赖方向不合理
  3. 测试和维护变难
  4. 重构成本更高

所以更稳妥的工程态度通常是:循环依赖应尽量避免,把它当作设计信号,而不是框架能力展示。


12. Bean 生命周期和循环依赖为什么要放在一起讲

因为这两条线其实都在回答同一个底层问题:Spring 容器在对象创建过程中,到底做了哪些事。

Bean 生命周期强调的是:一个对象是怎么一步步变成可用 Bean 的。

循环依赖强调的是:当这个过程被依赖环打断时,容器怎么想办法继续推进。

所以把这两者放在一起,理解会更连贯。


13. 工程上最常见的几个误区

13.1 误区一:Bean 生命周期就是 new 完就结束

不是。

真正的 Bean 创建还包括依赖注入、初始化、后置处理和销毁。

13.2 误区二:Spring 能处理循环依赖,所以循环依赖没问题

这是一种很危险的误解。

框架能兜住一部分,不代表设计就是合理的。

13.3 误区三:只要有三级缓存,就所有循环依赖都能解决

不是。

尤其构造器循环依赖和复杂作用域场景,通常没那么容易处理。

13.4 误区四:Bean 生命周期只是背阶段名词

真正重要的是:为什么这些阶段存在,以及 Spring 能在哪些阶段插入扩展逻辑。


14. 一句话总结

Bean 生命周期解决的是“一个对象如何在 Spring 容器里被创建、装配、初始化和销毁”,循环依赖解决的是“当对象创建顺序被依赖环打断时,容器如何在有限边界内继续推进”;两者一起看,才能真正理解 Spring 容器的底层主线。

基于 VitePress 构建的个人技术笔记。