Spring循环依赖解决方案
Spring 循环依赖的产生原因、底层解决逻辑(三级缓存)
核心前提:Spring 仅解决「单例 Bean + Setter 注入」的循环依赖,其他场景均无法解决,这是基础前提,必须先记住。
一、循环依赖的产生场景
定义:两个或多个 Bean 互相依赖对方,形成闭环(如 A 依赖 B,B 依赖 A;或 A→B→C→A),Spring 容器启动时无法正常创建 Bean,需通过特定机制解决。
1. Setter 注入(可解决)
最常见场景,通过 Setter 方法注入依赖,允许“先实例化 Bean,后注入依赖”,这是 Spring 能解决循环依赖的核心前提。
|
|
2. 构造器注入(无法解决)
通过构造器注入依赖,要求“实例化 Bean 时必须注入所有构造器依赖”,而循环依赖的 Bean 互相等待对方实例化,形成死锁,Spring 无法解决。
|
|
关键点:“构造器注入无法解决循环依赖,Setter 注入(单例)可解决”,后续结合三级缓存说明原因。
二、三级缓存解决循环依赖的原理(源码级流程)
核心逻辑:Spring 通过「三级缓存」提前暴露“未完成初始化的 Bean 实例”,让依赖方先获取到这个“半成品”Bean,避免互相等待,待双方都实例化、注入完成后,再完善 Bean 初始化。
核心前提:仅适用于「单例 Bean」(原型 Bean 每次获取都新建,无法缓存,故无法解决)、「Setter 注入」(允许先实例化后注入)。
1. 三级缓存的定义(源码核心变量)
Spring 容器中,三级缓存是三个 Map(定义在 DefaultSingletonBeanRegistry 中),作用各不相同,缺一不可:
| 缓存级别 | 缓存名称(源码变量) | 存储内容 | 核心作用 |
|---|---|---|---|
| 一级缓存(最终缓存) | singletonObjects | 完全初始化完成的单例 Bean(可直接使用) | 供外部获取已就绪的 Bean,避免重复创建,是最终的缓存容器 |
| 二级缓存(临时缓存) | earlySingletonObjects | 未完成初始化(未注入依赖、未执行初始化方法)但已实例化的单例 Bean(半成品) | 避免重复执行三级缓存中的工厂方法,提升性能,临时存储“提前暴露”的半成品 Bean |
| 三级缓存(工厂缓存) | singletonFactories | 单例 Bean 的工厂对象(ObjectFactory),用于生成“提前暴露的半成品 Bean” | 核心!解决 AOP 代理场景下的循环依赖,通过工厂方法动态生成代理对象(而非直接存储 Bean) |
2. 源码级解决流程(以 A→B、B→A 为例)
核心步骤:实例化 → 暴露工厂 → 注入依赖 → 初始化 → 放入一级缓存,循环依赖的关键是“提前暴露工厂”,打破互相等待。
-
Spring 容器启动,开始创建 Bean A(单例):
-
1.1 检查一级缓存(singletonObjects):无 A,继续;
-
1.2 检查二级缓存(earlySingletonObjects):无 A,继续;
-
1.3 检查三级缓存(singletonFactories):无 A,继续;
-
1.4 实例化 A(仅执行构造器,未注入依赖、未初始化,是半成品);
-
1.5 创建 A 的工厂对象(ObjectFactory),放入三级缓存(singletonFactories),提前暴露 A 的半成品;
-
1.6 开始为 A 注入依赖 B(此时 A 还未初始化)。
-
-
Spring 开始创建 Bean B(单例):
-
2.1 检查一级、二级、三级缓存:无 B,继续;
-
2.2 实例化 B(半成品,未注入依赖、未初始化);
-
2.3 创建 B 的工厂对象,放入三级缓存;
-
2.4 开始为 B 注入依赖 A(此时 B 还未初始化)。
-
-
B 注入依赖 A 时,触发 A 的获取流程:
-
3.1 检查一级缓存:无 A(A 未初始化完成);
-
3.2 检查二级缓存:无 A;
-
3.3 检查三级缓存:有 A 的工厂对象;
-
3.4 通过 A 的工厂对象,获取 A 的半成品(未初始化),放入二级缓存(earlySingletonObjects),并删除三级缓存中的 A 工厂;
-
3.5 将 A 的半成品注入到 B 中(此时 B 已注入依赖)。
-
-
B 完成注入后,执行初始化方法,初始化完成后,将 B 放入一级缓存(singletonObjects),删除二级、三级缓存中的 B;
-
B 创建完成,回到 A 的注入流程,将 B(已就绪)注入到 A 中;
-
A 完成注入后,执行初始化方法,初始化完成后,将 A 放入一级缓存,删除二级缓存中的 A 半成品;
-
循环依赖解决,A 和 B 均为就绪状态,可正常使用。
面试技巧:不用背源码细节,重点说清“提前暴露工厂→获取半成品→注入→初始化→放入一级缓存”的流程,强调三级缓存的协作逻辑。
三、关键问题:为什么需要三级缓存?二级缓存行不行? 🟠
核心结论:不行!必须用三级缓存,二级缓存无法解决 AOP 代理场景下的循环依赖,这是 Spring 设计三级缓存的核心原因。
1. 二级缓存的局限
如果只保留一级+二级缓存,流程会变成:Bean 实例化后,直接将半成品放入二级缓存,供依赖方获取。
但如果 Bean 需要被 AOP 代理(如 @Transactional、@Aspect 注解),问题就出现了:
-
Bean 实例化时,生成的是“原始对象”,而最终需要的是“代理对象”;
-
如果直接将原始对象放入二级缓存,依赖方注入的是原始对象,而非代理对象,后续 Bean 初始化时生成代理对象,会导致依赖注入的对象与最终的 Bean 对象不一致,出现异常。
2. 三级缓存的核心作用(解决 AOP 代理问题)
三级缓存存储的是“工厂对象(ObjectFactory)”,而非直接存储 Bean 实例,工厂对象的核心作用是:在依赖方获取 Bean 时,动态生成代理对象(如果需要)。
具体逻辑:
-
Bean 实例化后,不直接暴露半成品,而是暴露一个工厂;
-
当依赖方需要获取该 Bean 时,通过工厂方法 getObject() 生成对象:
-
如果 Bean 需要 AOP 代理:工厂方法生成代理对象,放入二级缓存;
-
如果不需要代理:工厂方法直接返回原始半成品对象,放入二级缓存;
-
-
这样,依赖方注入的就是“最终需要的对象”(代理/原始),保证了对象一致性。
3. 总结
二级缓存只能存储“固定的半成品 Bean”,无法处理 AOP 代理场景(无法动态生成代理对象);三级缓存通过工厂对象,延迟生成代理对象,既解决了循环依赖,又保证了 AOP 代理的正确性,所以必须用三级缓存。
四、无法解决循环依赖的场景 🔴
Spring 仅解决「单例 Bean + Setter 注入」的循环依赖,以下场景均无法解决,会直接抛出 BeanCurrentlyInCreationException 异常。
1. 构造器注入(最常见)
原因:构造器注入要求“实例化 Bean 时必须注入所有依赖”,循环依赖的 Bean 互相等待对方实例化,形成死锁,Spring 无法打破。
解决方案:将构造器注入改为 Setter 注入,或使用 @Lazy 注解(懒加载,延迟依赖注入)。
2. 原型 Bean(prototype 作用域)
原因:原型 Bean 每次 getBean 都会新建一个实例,Spring 不缓存原型 Bean,无法通过三级缓存提前暴露半成品,每次创建都会陷入循环。
解决方案:尽量避免原型 Bean 之间的循环依赖,或改为单例 Bean。
3. 其他场景
-
单例 Bean + 构造器注入(同场景1);
-
Bean 被 @Scope("request/session") 修饰(非单例,无法缓存);
-
手动调用 getBean() 触发的循环依赖(未走 Spring 容器的缓存流程)。
五、伪代码
核心:三级缓存的协作流程,重点体现“实例化→暴露工厂→注入→初始化”的逻辑,以 A→B、B→A 为例。
1. 三级缓存定义(伪代码)
|
|
2. 循环依赖解决流程(伪代码+文字描述,默写重点)
|
|
A 实例化 → 放工厂(三级)→ 注入 B → B 实例化 → 放工厂(三级)→ 注入 A → 从三级拿 A 工厂 → 生成 A 半成品(放二级)→ B 注入 A 后初始化 → 放一级 → A 注入 B 后初始化 → 放一级 → 循环解决。
总结 🔴
-
Spring 仅解决「单例 Bean + Setter 注入」的循环依赖,构造器注入、原型 Bean 无法解决;
-
三级缓存:singletonObjects(成品)、earlySingletonObjects(半成品)、singletonFactories(工厂);
-
核心原理:提前暴露 Bean 工厂,动态生成半成品/代理对象,打破互相等待;
-
为什么需要三级缓存?二级缓存无法处理 AOP 代理,三级缓存通过工厂延迟生成代理对象,保证对象一致性;
-
异常场景:构造器注入、原型 Bean 循环依赖,会抛出 BeanCurrentlyInCreationException。