Spring循环依赖解决
什么是依赖循环?
我们知道在 Spring 中依赖注入时的行为是:
- 如果当前 Bean 依赖其他 Bean,那么递归注入子 Bean
- 如果来源于配置文件,则从
Environment中读取
这就导致了如果 A→B→C→A 成环互相依赖,就会不断递归陷入死循环。就像是死锁一样,互相等待彼此。
类比死锁的解决方案
我们可以借鉴解决死锁的方法来解决依赖循环:
- 破坏互斥条件:允许所有资源被共享,但这不适用于某些不能共享的资源(如打印机)
- 破坏不剥夺条件:当进程请求新资源未得到满足时,必须释放所有已占有的资源
- 破坏请求与保持条件:采用预先静态分配,即进程在运行前一次性申请所有所需资源
- 破坏循环等待条件:采用顺序资源分配,规定进程必须按照资源的编号顺序来申请
Spring 的选择:
Spring 选择的是破坏循环等待条件(或者说打破了"资源必须完全就绪才能被使用"的互斥条件),允许循环中的 Bean 在未完全初始化结束前,先拿到一个"半成品"引用,从而结束递归等待。
Spring 在哪里、通过什么方法解决了它?
Spring 通过在 Instantiation 之后提前暴露工厂对象,在 Populate 之前,加入到三级缓存,允许其他对象通过 ObjectFactory 获取到 bean。
三级缓存机制
| 级别 | 缓存名称 | 存放内容 | 作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 完全初始化好的 Bean | 存放成熟的单例 Bean |
| 二级缓存 | earlySingletonObjects | 半成品的 Bean(提前代理) | 解决循环依赖中的 AOP 代理单例问题 |
| 三级缓存 | singletonFactories | ObjectFactory | 延迟决策是否需要代理 |
具体流程
A 实例化 → 暴露工厂到三级缓存
↓
A 填充属性,需要 B
↓
B 实例化 → 暴露工厂到三级缓存
↓
B 填充属性,需要 A → 从三级缓存获取 A 的工厂 → 创建代理/获取原始对象 → 放入二级缓存
↓
B 完成 → 放入一级缓存
↓
A 继续填充 B(从一级缓存获取)→ A 完成 → 放入一级缓存
![[../pics/BeanDependencyLoop.png]]
为什么要三级缓存而不是二级?
如果去掉二级缓存会发生什么:
-
当非 AOP代理的对象注入属性时 B从三级缓存中 getbean获取到A,打破循环,此时B已经已经成熟,放入一级缓存,回到A的属性注入流程,查找一级缓存,发现B存在,完成注入。 整体流程没有问题,也就是说在没有AOP代理时是可以的。
-
当有AOP代理时 B从三级缓存中 getbean获取到proxy012_A,把自身放入一级缓存,A又依赖C,C从三级缓存中获取proxy013_A,回到A,此时B和C拿到的是不同的A的代理对象,违反了单例原则。 当加入了二级缓存,获取到proxy_A后会把代理对象放到二级缓存,其他依赖需要时拿到的是同一个代理对象。
为什么不直接在三级缓存中存放代理对象?
- Spring 的设计初衷是 AOP 代理应当在 Bean 生命周期最后一步(初始化后)完成。但在 循环依赖 这种极端情况下,为了打破死锁,Spring 此时不得不妥协,允许在 实例化后、填充属性前 提前进行 AOP。
为什么提前暴露“空壳代理”没问题?
因为 Java 是 值传递(传递的是对象引用的地址)。
-
代理对象(Proxy)内部持有原始对象(Target)的引用。
-
虽然注入给别人时,原始对象还是“空的”,但随着生命周期继续,原始对象会被填充完好。
-
当真正调用代理对象方法时,内部委托的原始对象已经是成熟的了。
为什么要三级缓存总结:
-
三级缓存(ObjectFactory):它的作用是 “延迟决策”。它存的是一个生成代理的工厂逻辑,而不是具体的对象。只有当循环依赖发生时,才去运行这个逻辑(打破原则);否则,永远不运行(坚持原则)。Spring选的的是破坏不剥夺条件,允许循环中的Bean先拿到不成熟的Bean,结束递归。
-
二级缓存(EarlySingletonObjects):它的作用是 “保持单例”。一旦三级缓存的工厂被执行,生成的代理对象(Proxy)必须放入二级缓存。因为
createProxy每次都会产生新对象,如果有多个 Bean 依赖它,必须从二级缓存拿同一个 Proxy,保证全局唯一。
为什么构造器注入无法解决依赖循环?
在 Bean 的生命周期中 Instantiation 中如果使用的是构造器注入,会在此步骤直接注入依赖,但暴露工厂对象是在 Instantiation 结束后进行的,此时缓存中没有对象,无法获取到未成熟的Bean 。 解决方法: 使用懒加载 @Lazy,返回一个空壳,当真正需要该对象时才加载注入属性。
总结
| 问题 | 答案 |
|---|---|
| Spring 如何解决循环依赖 | 三级缓存 + 提前暴露半成品对象 |
| 为什么需要三级缓存 | 三级缓存用于"延迟决策"是否需要代理,二级缓存用于保持代理单例 |
| 构造器注入为什么不支持 | 构造器注入发生在暴露工厂之前 |
| 推荐的注入方式 | 构造器注入(避免循环依赖)+ 必要时使用 @Lazy |