Spring循环依赖解决

#依赖循环 #Spring #配置管理

什么是依赖循环?

我们知道在 Spring 中依赖注入时的行为是:

  1. 如果当前 Bean 依赖其他 Bean,那么递归注入子 Bean
  2. 如果来源于配置文件,则从 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]]


为什么要三级缓存而不是二级?

如果去掉二级缓存会发生什么:

  1. 当非 AOP代理的对象注入属性时 B从三级缓存中 getbean获取到A,打破循环,此时B已经已经成熟,放入一级缓存,回到A的属性注入流程,查找一级缓存,发现B存在,完成注入。 整体流程没有问题,也就是说在没有AOP代理时是可以的。

  2. 当有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

Reference

聊透Spring循环依赖 - 掘金