一、什么是循环依赖
当多个Bean互相依赖时
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
A的实例化需要B,B的实例化又需要有A,就好像先有鸡还是先有蛋一样,形成了循环的依赖关系。
二、Spring 默认使用三级缓存解决循环依赖
- 一级缓存:这是常规的 Bean 缓存,存储已创建并初始化完成的 Bean,包括不存在循环依赖的Bean,即每个 Bean 创建完成后会被放入这个缓存中。
- 二级缓存:存储已创建但未初始化的 Bean。在 Bean 的实例化过程中,Spring 会将它们存放到二级缓存中,以便在依赖注入时使用。
- 三级缓存:存储正在创建中的 Bean。Spring 会为每个 Bean 分配一个早期引用(Early Bean Reference),即一个代理对象(或者是未完成的 Bean 对象)。这样其他 Bean 在依赖它时,就能获取到这个代理对象,从而避免循环依赖的死锁。
具体的工作流程如下:
- Spring 首先创建一个 Bean 实例,并放入二级缓存(这个 Bean 还未完成所有属性的注入)。当 Spring 发现该 Bean 被其他 Bean 依赖时,它会先将这个 Bean 的 早期引用 放入三级缓存中。
- 如果一个 Bean 需要依赖另一个尚未完成的 Bean(即存在循环依赖),Spring 会从三级缓存中取出这个早期引用,而不是等待 Bean 完全初始化。
- 在 Bean 的依赖注入完成后,Spring 会将这个 Bean 放入一级缓存,并完成初始化。
通过三级缓存,Spring 实现了懒加载和延迟注入,允许依赖的 Bean 在创建过程中互相引用。
二、浅显易懂的理解方式
看了上面的解释,是不是还是云里雾里?
不过纸箱子大家都叠过吧
在叠纸箱子的过程中,有四个面,每个面都依赖它下面的面先完成折叠,这四个面就形成了一个循环的依赖关系,我们的解决步骤如下:
- 先把一个面立起来,准备进行折叠,发现这个面依赖于前面的面先折叠完成,于是依次把四个面都立起来,此时这四个面的状态都在折叠中,就相当于 Spring 的二级缓存。
- 此时四个面是循环依赖的状态,然后任意弯曲其中一个面,使其形成斜面,此时这个面虽然还没折叠完成,但它已经具备了一些可以与其他面连接的特性,它的斜面使其成为了临时的引用,此时这个被斜面的状态,就相当于 Spring 的三级缓存。
- 通过临时的斜面,解决了循环依赖的问题,将所有面的层级关系压平,折叠成功!最终四个面都处于折叠完成的状态,此时四个面的状态就相当于 Spring 的一级缓存。
现在再回头看,当 Spring 遇到循环依赖时,它先创建一个不完全的 Bean(早期引用),这就像是弯曲了一个面,虽然这个面还没有完全形成,但它已经可以被其他面引用。然后,Spring 会利用这个“斜面”来让其他 Bean 引用这个不完整的实例,从而逐步完成整个对象的初始化。
三、构造注入导致的循环依赖
构造注入要求这个类一次成型,就相当于不能弯曲斜面,无法通过三级缓存解决,这就是为什么 Spring 默认只能解决 @Autowired 注入和 setter 注入导致的循环依赖,如果出现构造注入导致的循环依赖,Spring 会抛出一个 BeanCurrentlyInCreationException 异常。
此时的解决办法是在循环依赖的一个构造方法中,加入 @Lazy 注解。
@Lazy 注解的作用是延迟初始化,即当某个 Bean 被标记为 @Lazy 时,Spring 会推迟该 Bean 的创建,使用代理类完成父Bean的实例化,直到它被实际需要时才会初始化。
同样用叠箱子举例,四个面分别是 A、B、C、D,当B被添加@Lazy注解时,叠A时,太懒了,B先不叠了,先拿个板子放在B面的位置,垫在A面下面,就当B面折叠完成了吧,实际用到B面的时候再说,就这样A面实例化完成,存入一级缓存。
此时A面下的板子是B面的代理对象,实际的B面不需要叠,C面D面也就不需要了,这就是 @Lazy 在依赖链中的传递。
当实际需要使用B面时,B去找C,C又找D,D又找A,此时A已经在一级缓存中了,不存在循环了,倒序从DCB依次构建完成,最后再将实际的B替换掉A下面的板子,就是代理的B,整条循环依赖链路就构建完成了。
这就是使用 @Lazy 解决构造注入循环依赖的逻辑。
@Component
public class A {
private final B b;
@Autowired
public A(@Lazy B b) {
this.b = b;
}
}
@Component
public class B {
private final C c;
@Autowired
public B(C c) {
this.c = c;
}
}
@Component
public class C {
private final D d;
@Autowired
public C(D d) {
this.d = d;
}
}
@Component
public class D {
private final A a;
@Autowired
public D(A a) {
this.a = a;
}
}
四、多例模式循环依赖为什么无法解决
上面的两种解决方案只适用于单例的Bean,单例模式可以临时对象来打破循环依赖,而多例 Bean 的生命周期不受 Spring 容器的控制,被声明为 @Scope(“prototype”) 的多例Bean,每次使用时都会重新创建新的对象。
当 A、B、C、D 四个Bean循环依赖,尝试获取A时,构造链路为
A => B => C => D => A1 => B1 => C1 => D1 => A2 …
所以多例模式循环依赖无法被解决。