Spring Bean 生命周期 & 循环依赖:90% 开发者只知结论不懂原理

Spring Bean 生命周期 & 循环依赖:90% 开发者只知结论不懂原理

"Spring 能解决 setter 循环依赖,解决不了构造器循环依赖。"

这句话,几乎所有 Java 后端都能背出来。

但问一句:为什么?底层是怎么做到的?三级缓存到底存了什么?

绝大多数人就开始支支吾吾了。

这篇文章,我们不背结论,一起把 Spring Bean 生命周期 + 循环依赖的底层机制​ 拆清楚。读完你会明白:

Spring 不是"神奇地"解决了循环依赖,而是"精心设计"了一套流程。


一、先跳出细节:Bean 生命周期全景图(简化版)

为了讲清楚循环依赖,必须先有"地图"。

一个 Bean 从定义到可用,核心经历以下几个阶段:

csharp 复制代码
实例化(new)
   ↓
填充属性(populate)
   ↓
初始化(init)
   ↓
就绪(getBean)
   ↓
销毁(destroy)

对应到 Spring 源码中,最核心的方法是:

bash 复制代码
AbstractAutowireCapableBeanFactory#doCreateBean

里面三件大事,务必记住:

  1. createBeanInstance:创建实例(反射 new)
  2. populateBean:依赖注入(@Autowired / setter)
  3. initializeBean:执行 aware、init-method、AOP 代理

👉 循环依赖的问题,就发生在「实例化之后、属性注入期间」


二、什么是循环依赖?为什么是个问题?

1️⃣ 什么是循环依赖

less 复制代码
@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private A a;
}

创建 A → 需要 B → 创建 B → 又需要 A → 无限套娃。

2️⃣ 问题的本质

对象还没创建完,就被别人引用了。

  • 如果等 A 完全创建好再给 B,B 拿不到
  • 如果先给 B 一个"半成品 A",又怕出问题

Spring 的解法很巧妙:

👉 提前暴露一个"尚未初始化完成的 Bean 引用"


三、三级缓存:Spring 解决循环依赖的核心

这是面试必考点,也是理解原理的关键。

1️⃣ 三级缓存分别是什么?

缓存名称 作用
singletonObjects 一级缓存:存放完全初始化好的 Bean
earlySingletonObjects 二级缓存:存放早期暴露的 Bean(未初始化完)
singletonFactories 三级缓存:存放 ObjectFactory(Bean 的工厂)

2️⃣ 它们的关系(重点)

  • 一级缓存:最终对外暴露的 Bean
  • 二级缓存:防止重复创建早期引用
  • 三级缓存真正解决循环依赖的关键

注意:三级缓存里存的不是 Bean 本身,而是一个 工厂(ObjectFactory)


四、源码级流程:Spring 是如何"破局"的?

我们用 A → B → A​ 这个经典循环依赖,走一遍流程。

Step 1:创建 A

scss 复制代码
getBean(A)
  • singletonObjects 中没有 A
  • 标记 A 正在创建中
  • 调用 createBeanInstance(A)实例化 A(此时 a 是半成品)

Step 2:将 A 的 ObjectFactory 放入三级缓存

scss 复制代码
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean))

这一步非常关键:

  • A 虽然没初始化完
  • 但 Spring 已经允许别人拿到它的早期引用

Step 3:填充 A 的属性(需要 B)

scss 复制代码
populateBean(A)
→ getBean(B)

Step 4:创建 B

  • singletonObjects 中没有 B
  • 实例化 B
  • 同样把 B 的 ObjectFactory 放入三级缓存

Step 5:填充 B 的属性(需要 A)

scss 复制代码
populateBean(B)
→ getBean(A)

这时候重点来了 👇


Step 6:B 再次 getBean(A) 时发生了什么?

Spring 发现:

  • A 正在创建中 ✅
  • 三级缓存中存在 A 的 ObjectFactory ✅

于是:

  1. 调用 ObjectFactory.getObject()
  2. 拿到 A 的 早期引用
  3. 放入 二级缓存(earlySingletonObjects)
  4. 删除三级缓存中的工厂

👉 B 成功拿到了"半成品 A"


Step 7:B 初始化完成

  • B 初始化完成
  • 放入 一级缓存 singletonObjects

Step 8:回到 A 的创建流程

  • A 继续填充属性(此时 B 已就绪)
  • A 初始化完成
  • 放入一级缓存
  • 清理二、三级缓存

循环依赖解决完成


五、为什么 setter / field 注入可以,构造器注入不行?

这是很多人"背结论"的地方,现在我们从原理上解释。

setter / field 注入为什么可以?

因为流程是这样的:

css 复制代码
实例化 A(new A)✅
→ 暴露早期引用 ✅
→ 再注入 B ✅

实例化 和 依赖注入 是分开的

Spring 有机会在"注入前"先把 A 曝光出去。


构造器注入为什么不行?

kotlin 复制代码
@Service
public class A {
    private final B b;
    public A(B b) { this.b = b; }
}

构造器注入的流程是:

css 复制代码
创建 A
→ 必须先有 B
→ 才能 new A

问题来了:

  • A 还没实例化完
  • 根本来不及放进三级缓存
  • B 再来要 A → 找不到 → 抛异常

📌 结论一句话版

构造器注入把"实例化"和"依赖注入"绑死了,而 Spring 的循环依赖机制依赖"先实例化、后暴露"。


六、那 AOP 呢?代理对象会不会出问题?

这是一个进阶但非常重要的问题。

如果 A 需要被 AOP 代理:

  • 早期暴露的是 原始对象
  • 但最终放入容器的是 代理对象

Spring 怎么保证一致性?

答案就在这一句:

scss 复制代码
getEarlyBeanReference()
  • 如果 Bean 需要被代理
  • 三级缓存中的 ObjectFactory
  • 会提前生成代理对象

这样:

  • B 拿到的就是 代理后的 A
  • 最终容器中也是代理后的 A
  • ✅ 完全一致

七、面试标准答案(可直接背)

Spring 通过三级缓存解决 singleton 作用域下的 setter / field 循环依赖。

在 Bean 实例化完成后、属性注入前,Spring 会将一个 ObjectFactory 放入三级缓存,从而在发生循环依赖时,能让对方拿到当前 Bean 的早期引用。

构造器循环依赖无法解决,因为构造器调用发生在实例化阶段,此时 Bean 尚未放入三级缓存,无法满足提前暴露的条件。


八、实际工作中的三点建议

  1. 不要依赖循环依赖

    • 循环依赖往往是设计坏味道
    • 优先考虑:拆分职责、引入中间层、使用 @Lazy
  2. 构造器注入优先

    • 虽然不支持循环依赖
    • 但能更早暴露设计问题
    • 更符合"不可变对象"思想
  3. 理解 > 背诵

    • 面试不是为了复述结论
    • 而是展示你对 Spring 容器运作机制的掌控力

九、总结一句话

Spring 不是"黑魔法",它只是在 Bean 生命周期的关键节点,提前暴露了一个工厂,让半成品 Bean 有了被引用的机会。

当你能把这句话讲清楚,你也就超越了那 90% 只知结论的开发者。

相关推荐
神经蛙199615 小时前
🌍 别再硬编码中文了!Python Web 项目国际化(i18n)完全指南
后端·python
二月龙16 小时前
Spring 事务失效的 8 种场景,很多老手依然频繁踩雷
后端
掘金酱16 小时前
「TRAE Work 实战帮」征文启动!你沉淀的经验,值得被看见!
前端·人工智能·后端
长大198816 小时前
MyBatis 常见性能陷阱:N+1 查询、一级缓存踩坑解决方案
后端
用户18615580086016 小时前
MinIO Java 对接试用:从连接、上传到下载的完整示例
后端
爱勇宝16 小时前
DeepSeek V4-Flash 更新:代码与 Agent 能力全面增强
前端·后端·deepseek
极客悟道16 小时前
SDKMAN vs jEnv vs JetTUI,JDK 版本管理到底选哪个
后端
长大198816 小时前
Java8 新特性到底要不要吃透?工作中高频使用的 5 个功能总结
后端