Spring 可以解决哪些情况的循环依赖?

12 19~25 min

看看这几种情形(AB 循环依赖):

三分恶面渣逆袭:循环依赖的几种情形

也就是说:

  • AB 均采用构造器注入,不支持

  • AB 均采用 setter 注入,支持

  • AB 均采用属性自动注入,支持

  • A 中注入的 B 为 setter 注入,B 中注入的 A 为构造器注入,支持

  • B 中注入的 A 为 setter 注入,A 中注入的 B 为构造器注入,不支持

第四种可以,第五种不可以的原因是 Spring 在创建 Bean 时默认会根据自然排序进行创建,所以 A 会先于 B 进行创建。

简单总结下,当循环依赖的实例都采用 setter 方法注入时,Spring 支持,都采用构造器注入的时候,不支持;构造器注入和 setter 注入同时存在的时候,看天(😂)。

在Spring框架中,循环依赖的处理依赖于Bean的注入方式(构造器注入或Setter注入)以及容器的三级缓存机制。以下是两种情况的详细分析:

情况1:A使用Setter注入B,B使用构造器注入A

  1. 创建A:首先通过默认构造器实例化A,此时A的引用(早期对象)被放入三级缓存。

  2. 注入B到A:Spring尝试注入B,发现B尚未创建,开始创建B。

  3. 创建B:B的构造器需要A的实例。此时Spring从三级缓存中获取A的早期引用,成功完成B的实例化。

  4. 完成依赖注入:B初始化完成后,注入到A的Setter中,A完成初始化。

关键点
A的实例化不依赖B的完全初始化状态,仅需B的早期引用即可完成构造器注入。Setter注入的延迟特性允许A在初始化完成后才注入B,从而解决循环依赖。


情况2:A使用构造器注入B,B使用Setter注入A

  1. 创建A:A的构造器需要B的实例,触发B的创建。

  2. 创建B:通过默认构造器实例化B,并将B的早期引用放入三级缓存。

  3. 注入A到B:B的Setter需要A的实例,触发A的创建。但此时A的构造器仍需要B的实例,而B尚未完成初始化。

  4. 死锁问题:A的构造器要求B完全初始化,但B的Setter又依赖A的实例。由于A无法完成构造(等待B初始化),B也无法完成Setter注入(等待A初始化),导致循环依赖无法解决。

关键点
构造器注入要求依赖的Bean(B)在实例化时完全初始化,而B的Setter注入又需要A的实例,但A此时因依赖B而无法完成初始化,形成死锁。


根本原因

  • 构造器注入:必须在实例化阶段解析所有依赖,要求依赖的Bean完全初始化

  • Setter注入:在属性填充阶段解析依赖,允许注入正在创建中的Bean(通过三级缓存的早期引用)。

当A使用构造器注入B时,B必须完全初始化才能注入到A的构造器中。但B的Setter需要A的实例,而A此时尚未完成初始化,导致循环依赖无法解决。反之,当A使用Setter注入B时,B的构造器可以提前使用A的早期引用,打破循环依赖。

总结

  • Setter注入的延迟特性允许容器先暴露Bean的早期引用,再完成依赖注入,从而解决循环依赖。

  • 构造器注入的严格性要求依赖Bean完全初始化,导致循环依赖无法在类似场景下解决。

Spring 通过三级缓存机制来解决循环依赖:

  1. 一级缓存:存放完全初始化好的单例 Bean。

  2. 二级缓存:存放正在创建但未完全初始化的 Bean 实例。

  3. 三级缓存:存放 Bean 工厂对象,用于提前暴露 Bean。

三级缓存解决循环依赖的过程是什么样的?

  1. 实例化 Bean 时,将其早期引用放入三级缓存。

  2. 其他依赖该 Bean 的对象,可以从缓存中获取其引用。

  3. 初始化完成后,将 Bean 移入一级缓存。

假如 A、B 两个类发生循环依赖:

A 实例的初始化过程:

①、创建 A 实例,实例化的时候把 A 的对象⼯⼚放⼊三级缓存,表示 A 开始实例化了,虽然这个对象还不完整,但是先曝光出来让大家知道。

②、A 注⼊属性时,发现依赖 B,此时 B 还没有被创建出来,所以去实例化 B。

③、同样,B 注⼊属性时发现依赖 A,它就从缓存里找 A 对象。依次从⼀级到三级缓存查询 A。

发现可以从三级缓存中通过对象⼯⼚拿到 A,虽然 A 不太完善,但是存在,就把 A 放⼊⼆级缓存,同时删除三级缓存中的 A,此时,B 已经实例化并且初始化完成了,把 B 放入⼀级缓存。

④、接着 A 继续属性赋值,顺利从⼀级缓存拿到实例化且初始化完成的 B 对象,A 对象创建也完成,删除⼆级缓存中的 A,同时把 A 放⼊⼀级缓存

⑤、最后,⼀级缓存中保存着实例化、初始化都完成的 A、B 对象。


解决循环依赖

3. 使用 @Lazy 延迟初始化

通过 @Lazy 注解标记一个依赖为延迟加载,打破初始化时的死锁。

示例

@Component
public class A {
    private final B b;

    @Autowired
    public A(@Lazy B b) {  // 延迟加载 B
        this.b = b;
    }
}

@Component
public class B {
    private final A a;

    @Autowired
    public B(A a) {
        this.a = a;
    }
}