深入理解 Spring 循环依赖:成因、解决方案与局限

0 阅读4分钟

引言

在使用 Spring 框架进行开发时,循环依赖是一个常见且容易让人困惑的问题。当多个 Bean 之间相互依赖形成闭环时,就会引发循环依赖,可能导致应用启动失败。本文将详细探讨 Spring 循环依赖的产生场景、Spring 处理循环依赖的机制以及循环依赖无法解决的情况。

image.png

一、Spring 循环依赖的产生场景

1. 构造器注入循环依赖

构造器注入循环依赖是指在 Bean 的构造函数中相互依赖,从而形成死循环。以下是示例代码:

public class A {
    private final B b;

    public A(B b) {
        this.b = b;
    }
}

public class B {
    private final A a;

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

在这个例子中,A 的构造函数需要 B 的实例,而 B 的构造函数又需要 A 的实例。Spring 在创建 A 时,为了完成实例化,会先尝试创建 B;而创建 B 时又需要先创建 A,这样就陷入了无限循环。

2. setter 注入循环依赖

setter 注入循环依赖是指 Bean 通过 setter 方法相互依赖。示例代码如下:

@Component
public class A {
    @Autowired
    private B b;

    public void setB(B b) {
        this.b = b;
    }
}

@Component
public class B {
    @Autowired
    private A a;

    public void setA(A a) {
        this.a = a;
    }
}

A 通过 setter 方法依赖 BB 同样通过 setter 方法依赖 A,形成了循环依赖关系。

二、Spring 处理循环依赖的机制

Spring 主要利用三级缓存来解决单例 Bean 的 setter 注入循环依赖问题,下面详细介绍这三级缓存:

  • 一级缓存(singletonObjects :也称为单例对象缓存,用于存放已经完全初始化好的单例 Bean 实例。这些 Bean 已经完成了实例化、属性注入和初始化等所有流程,可以直接使用。

image.png

  • 二级缓存(earlySingletonObjects :即早期单例对象缓存,存储的是已经实例化但还未完成属性注入和初始化的单例 Bean 实例。这些 Bean 处于中间状态,还不能完全正常使用。

image.png

  • 三级缓存(singletonFactories :单例工厂缓存,存放的是生产单例 Bean 的工厂对象。通过这些工厂对象,可以获取到早期的单例 Bean 实例。

image.png 以 A 和 B 相互依赖为例,Spring 解决循环依赖的具体流程如下:

  1. 创建 A 实例:Spring 开始创建 A,首先进行实例化操作,然后将 A 的工厂对象放入三级缓存 singletonFactories 中。此时 A 仅仅完成了实例化,还未进行属性注入和初始化。

  2. 注入 A 的依赖 B:在对 A 进行属性注入时,发现需要 B 实例,于是 Spring 开始创建 B

  3. 创建 B 实例:实例化 B 后,将 B 的工厂对象放入三级缓存 singletonFactories 中,接着对 B 进行属性注入。

  4. 注入 B 的依赖 A:在对 B 进行属性注入时,需要 A 实例。这时 Spring 从三级缓存 singletonFactories 中获取 A 的工厂对象,通过该工厂对象获取 A 的早期实例,将其放入二级缓存 earlySingletonObjects 中,并从三级缓存移除。然后将 A 的早期实例注入到 B 中。

  5. 完成 B 的初始化B 完成属性注入和初始化后,将 B 实例放入一级缓存 singletonObjects 中,并从二、三级缓存中移除。

  6. 完成 A 的初始化:将 B 实例注入到 A 中,A 完成属性注入和初始化后,将 A 实例放入一级缓存 singletonObjects 中,并从二、三级缓存中移除。

image.png

image.png

三、循环依赖无法解决的情况

1. 构造器注入循环依赖

由于构造器注入是在实例化对象时就需要依赖对象,Spring 无法在实例化阶段打破这种循环依赖。在创建对象时,必须先满足构造函数的参数要求,而循环依赖导致参数永远无法满足,因此构造器注入循环依赖无法解决。

2. 非单例 Bean 的循环依赖

Spring 只对单例 Bean 的循环依赖进行处理。对于原型(prototype)作用域的 Bean,每次请求都会创建新的实例,Spring 不会对这些 Bean 进行缓存。这就导致在处理循环依赖时,无法像单例 Bean 那样通过缓存获取早期实例,从而无法解决循环依赖问题

四、总结

Spring 循环依赖是开发中常见的问题,理解其产生的原因和 Spring 的处理机制有助于我们更好地编写代码。Spring 通过三级缓存机制巧妙地解决了单例 Bean 的 setter 注入循环依赖问题,但对于构造器注入和非单例 Bean 的循环依赖,目前还无法有效处理。在实际开发中,我们应尽量避免出现循环依赖