导语: 你是否曾在 Spring 项目中遭遇过 BeanCurrentlyInCreationException 的暴击?是否在面试中被连环追问“Spring 如何解决循环依赖”?这背后隐藏着 Spring IoC 容器最精妙的设计之一——三级缓存机制。今天,我们就撕开它的神秘面纱,从源码流程到设计哲学,彻底搞懂这个高频面试题,并教你写出更优雅的代码!
一、循环依赖:Spring 开发者的“经典噩梦”
想象一下:
BeanA需要注入BeanBBeanB又需要注入BeanA- 两者相互等待,陷入死锁... 🤯
这就是循环依赖(Circular Dependency)。Spring 默认通过三级缓存(Three-level Cache) 这个精妙的机制,为单例Bean的Setter/Field注入场景提供了优雅的解决方案。但构造器注入和原型Bean,它就无能为力了(后面细说)。
核心问题:如何在 Bean 未完全初始化完成前,提前暴露它的引用?
二、三级缓存:Spring 的破局利器
Spring 容器内部维护了三个核心 Map,它们构成了解决循环依赖的基石:
// 一级缓存(成品仓库):存放完全初始化好的、立即可用的 Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存(半成品展厅):存放提前暴露的 Bean(已实例化,但属性未填充/未初始化)。用于避免重复创建早期引用。
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存(Bean 工厂车间):存放 ObjectFactory,用于生产 Bean 的早期引用(原始对象或代理对象)。这是解决循环依赖的关键入口。
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
关键点理解:
- 一级缓存 (
singletonObjects):最终的、完全可用的 Bean 归宿。 - 二级缓存 (
earlySingletonObjects):性能优化层。一旦某个 Bean 的早期引用被从三级缓存中创建出来并放到二级缓存,后续依赖查找就直接从这里拿,避免重复执行ObjectFactory.getObject()(尤其是涉及代理时)。 - 三级缓存 (
singletonFactories):核心中的核心! 它存放的不是 Bean 本身,而是一个能生产 Bean 早期引用的工厂 (ObjectFactory)。这个工厂至关重要,因为它能处理 AOP 代理等需要包装原始对象的情况。工厂的存在让 Spring 有能力在需要时动态生成正确的引用(原始对象或代理对象)。
三、源码级流程拆解:以 A→B→A 为例
让我们跟随 Spring 容器的脚步,看三级缓存如何协同工作,破解 A 和 B 的相爱相杀:
-
创建 Bean A (开始征途)
- 实例化
A: 调用A的构造函数,在堆内存中创建出一个原始对象(此时A的属性b还是null)。 - 暴露工厂 (关键一步!): 将这个原始对象包装进一个
ObjectFactory,并将这个工厂放入三级缓存 (singletonFactories)。代码体现:addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); // 核心!getEarlyBeanReference()是灵魂方法!它会检查该 Bean 是否需要被包装(如应用BeanPostProcessor, 特别是SmartInstantiationAwareBeanPostProcessor用于 AOP 代理)。如果需要代理,此时就会创建代理对象;否则返回原始对象。这确保了后续注入的引用是最终形态(可能是代理)!
- 实例化
-
填充
A的属性 (发现依赖B)- 容器尝试为
A的属性b注入值。 - 发现需要
BeanB,但一级缓存 (singletonObjects) 中没有。
- 容器尝试为
-
创建 Bean B (连锁反应)
- 实例化
B: 调用B的构造函数,创建B的原始对象。 - 暴露工厂: 同样,将
B的ObjectFactory放入三级缓存 (singletonFactories)。
- 实例化
-
填充
B的属性 (关键转折 - 需要A!)- 容器尝试为
B的属性a注入值。 - 查找
BeanA:- 一级缓存 (
singletonObjects):无(A还没完全初始化好)。 - 二级缓存 (
earlySingletonObjects):无(首次查找)。 - 三级缓存 (
singletonFactories):找到A的工厂!
- 一级缓存 (
- 执行
ObjectFactory.getObject()(魔法发生!):- 调用之前放入的
ObjectFactory(即() -> getEarlyBeanReference(...))。 getEarlyBeanReference()执行:如果A需要代理(比如有@Async,@Transactional或自定义 AOP),此时创建并返回A的代理对象;否则返回A的原始对象。- 将得到的这个早期引用(可能是代理对象)放入二级缓存 (
earlySingletonObjects),并从三级缓存中移除A的工厂。
- 调用之前放入的
- 注入: 将这个
A的早期引用注入到B的属性a中。至此,B持有了A的引用(即使A还没完全初始化好)!
- 容器尝试为
-
完成
B的初始化 (B 率先毕业)- 继续执行
B的初始化逻辑(如@PostConstruct方法)。 - 将完全初始化好的
B放入一级缓存 (singletonObjects)。 - 清理: 从二级缓存 (
earlySingletonObjects) 和三级缓存 (singletonFactories) 中移除B的相关记录(如果存在)。
- 继续执行
-
完成
A的初始化 (A 紧随其后)- 回到
A的属性填充流程,现在可以顺利拿到一级缓存中完全初始化好的B,注入到A的属性b。 - 继续执行
A的初始化逻辑(如@PostConstruct)。 - 将完全初始化好的
A放入一级缓存 (singletonObjects)。 - 清理: 从二级缓存 (
earlySingletonObjects) 中移除A的早期引用(如果有)。
- 回到
🎉 循环依赖成功解决!A 和 B 都完成了初始化并可用。
四、深度解析:设计精妙之处与关键考量
-
为什么是三级?两级不行吗?
先说结论:Spring 设计三级缓存的核心是为了解决 AOP 代理与循环依赖的时序冲突。无 AOP 时,两级缓存足以处理循环依赖。
各级缓存必要性与 AOP 的关系:
-
三级缓存 (
singletonFactories- 工厂缓存):- 有 AOP:绝对必需。 它存储的
ObjectFactory动态生成并返回代理对象作为早期引用,确保依赖方在循环中拿到的是最终形态(代理),解决了代理创建在初始化之后与依赖注入在初始化之前的矛盾。 - 无 AOP:非必需(功能上可被二级缓存替代)。 其工厂虽能返回原始半成品对象,但二级缓存直接存储该对象也能达到相同目的。
- 有 AOP:绝对必需。 它存储的
-
二级缓存 (
earlySingletonObjects- 早期引用缓存):- 有 AOP:必需(性能核心)。 缓存首次生成的代理对象(早期引用),避免同一 Bean 被多次依赖时重复执行昂贵的代理创建逻辑 (
ObjectFactory.getObject())。 - 无 AOP:必需(性能优化)。 缓存原始半成品对象,避免同一 Bean 被多次依赖时重复查找或构造。
- 有 AOP:必需(性能核心)。 缓存首次生成的代理对象(早期引用),避免同一 Bean 被多次依赖时重复执行昂贵的代理创建逻辑 (
-
一级缓存 (
singletonObjects- 成品缓存):- 有无 AOP 均必需。 存储最终初始化完成的、可用的单例 Bean 对象。
-
-
AOP 代理的优雅处理:
getEarlyBeanReference()- 这是三级缓存机制能正确处理 AOP 的核心。在暴露早期引用的关键时刻(放入三级缓存时),Spring 通过
ObjectFactory预留了后路。 - 当真正需要注入早期引用时(调用
getObject()),getEarlyBeanReference()方法会应用那些需要在早期暴露阶段处理的BeanPostProcessor(主要是AbstractAutoProxyCreator),确保最终注入的是代理对象(如果需要代理)。如果等到完全初始化后再代理,循环依赖就无法解决了,因为注入的将是一个原始对象。
- 这是三级缓存机制能正确处理 AOP 的核心。在暴露早期引用的关键时刻(放入三级缓存时),Spring 通过
-
作用域限制:单例的专利
- 仅单例 (Singleton) Bean 适用: Spring 容器只对单例 Bean 维护了这套三级缓存结构。原型 (Prototype) Bean 每次请求都会创建一个新实例,容器无法管理其生命周期和依赖关系,遇到循环依赖直接抛
BeanCurrentlyInCreationException:if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); }
- 仅单例 (Singleton) Bean 适用: Spring 容器只对单例 Bean 维护了这套三级缓存结构。原型 (Prototype) Bean 每次请求都会创建一个新实例,容器无法管理其生命周期和依赖关系,遇到循环依赖直接抛
-
构造器注入:三级缓存的“死穴”
- 为什么无法解决?
@Component public class A { private final B b; // final 字段,必须在构造器赋值 public A(B b) { this.b = b; } // 构造器注入:创建 A 实例时,必须立即有 B! } @Component public class B { private final A a; // final 字段,必须在构造器赋值 public B(A a) { this.a = a; } // 构造器注入:创建 B 实例时,必须立即有 A! } - 根源: 构造器注入要求依赖在对象实例化完成的那一刻就必须就绪。此时
A的构造函数执行需要B,而B的构造函数执行又需要A。双方都卡在实例化这一步,没有任何一个 Bean 有机会被实例化并提前暴露其引用(即放入三级缓存)。三级缓存机制在属性注入阶段才介入,此时为时已晚。
- 为什么无法解决?
五、实战:解决方案对比与最佳实践
| 方案 | 适用场景 | 是否推荐 | 说明与注意事项 |
|---|---|---|---|
| Setter/Field 注入 | 属性循环依赖 (单例) | ✅ 推荐 | Spring 三级缓存天然支持。最常见、最安全的解决方式。 |
| 构造器注入 | 无循环依赖场景 | ⚠️ 慎用 | 无法解决自身循环依赖。 优先保证无循环,否则强推构造器注入会带来问题。 |
@Lazy 延迟加载 | 解决特定循环问题 (如 Controller/Service) | ✅ 可选 | 注解在依赖项上 (@Autowired private @Lazy B b;)。使用时才触发初始化,打破循环。简单有效,但可能掩盖设计问题。 |
| 代码重构 | 根治循环依赖 | ✅ 强烈推荐 | 最根本的解决方案! 思考: - 提取公共逻辑到新的 Service? - 是否过度耦合? - 使用事件 ( ApplicationEvent) 解耦? |
ApplicationContext.getBean() | 手动获取 Bean | ❌ 避免 | 破坏 IoC 控制反转,增加耦合度,使测试困难。万不得已的 hack 手段。 |
📌 开发者最佳实践:
- 优先使用 Setter/Field 注入: 避免构造器注入带来的无法解决的循环依赖问题。
- 拥抱重构:将消除循环依赖作为首要目标!
- 循环依赖往往是设计缺陷(高耦合、职责不清)的信号。
- 积极思考: 能否将相互依赖的部分抽离到一个新的、独立的 Bean 中?能否使用观察者模式(事件监听)替代直接调用?
@Lazy作为权宜之计: 在紧急修复或重构成本过高时,@Lazy是一个有效的临时解决方案。但要清醒认识到它只是延迟了问题,并非根治。- 理解原理,面试不慌: 深刻理解三级缓存流程(尤其
getEarlyBeanReference的作用)和构造器注入的限制,足以应对大部分深度面试提问。
六、总结:Spring 的智慧与开发者的责任
Spring 的三级缓存机制 (singletonFactories + earlySingletonObjects + singletonObjects) 是其 IoC 容器设计中极其精妙的一环。它通过:
- 提前暴露引用: 在 Bean 仅实例化后、未初始化前,就通过
ObjectFactory将其(可能是代理对象)暴露出去。 - 代理感知: 核心方法
getEarlyBeanReference()确保在依赖注入时提供的是最终形态的对象。 - 缓存协作: 三级分工明确,兼顾功能与性能。
它优雅地解决了单例 Bean Setter/Field 注入的循环依赖难题,展现了框架设计的强大。
然而,技术再巧妙,也非万能药。 构造器注入循环和原型 Bean 循环仍是禁区。更重要的是,过度依赖框架解决循环,往往掩盖了代码设计的不足。 作为追求卓越的开发者,我们应当:
- 理解原理: 知晓三级缓存如何运作,知其然也知其所以然。
- 善用工具: 在必要时正确使用 Setter/Field 注入或
@Lazy。 - 追求卓越: 将重构代码、消除循环依赖视为提升系统可维护性和设计质量的关键步骤! 写出低耦合、高内聚的代码,才是治本之道。
你在项目中遇到过哪种棘手的循环依赖?是用什么方案解决的?或者对三级缓存还有哪些疑问?欢迎在评论区分享讨论!