Spring 循环依赖是如何处理的?

1. 什么是循环依赖?

循环依赖是指两个或多个 Bean 之间相互依赖,形成一个闭环。例如:

  • A 依赖 B,B 也依赖 A:

    A -> B -> A
    
  • A 依赖 B,B 依赖 C,C 又依赖 A:

    A -> B -> C -> A
    

2. Spring 支持和不支持的循环依赖类型:

  • 支持:
    • Setter 方法注入的循环依赖。
    • 字段注入(Field Injection)的循环依赖。
  • 不支持:
    • 构造器注入(Constructor Injection)的循环依赖。
    • prototype 作用域的循环依赖(任何注入方式都不支持)。

3. Spring 解决循环依赖的核心:三级缓存

Spring 使用“三级缓存”来解决 Setter 方法注入和字段注入的循环依赖。这三级缓存是 DefaultSingletonBeanRegistry 类中的三个 Map

  • 一级缓存:singletonObjects(单例对象缓存)

    • 存放已经经历了完整生命周期的 Bean 实例,即完全初始化好的单例 Bean。
    • 从该缓存中获取的 Bean 可以直接使用。
  • 二级缓存:earlySingletonObjects(早期单例对象缓存)

    • 存放早期暴露的 Bean 实例,这些 Bean 实例已经被创建,但尚未完成属性注入和初始化。
    • 为了解决循环依赖,Spring 会在 Bean 实例化后、初始化前,将其放入二级缓存,提前暴露一个不完整的 Bean 实例。
  • 三级缓存:singletonFactories(单例工厂缓存)

    • 存放创建 Bean 实例的 ObjectFactory(一个函数式接口,用于创建对象)。
    • 当一个 Bean 被放入三级缓存时,它并不直接暴露 Bean 实例,而是暴露一个 ObjectFactory,通过 ObjectFactory.getObject() 可以获取到 Bean 实例。
    • ObjectFactory 的作用是:
      • 延迟创建: 直到真正需要 Bean 实例时才调用 getObject() 方法创建。
      • 解决 AOP 代理问题: 如果 Bean 需要被 AOP 代理,ObjectFactory 可以返回代理对象,而不是原始对象。

4. 循环依赖的解决过程(以 Setter 方法注入为例):

假设有两个类 A 和 B,它们通过 Setter 方法相互依赖:

public class A {
    private B b;

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

public class B {
    private A a;

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

Spring 解决 A 和 B 循环依赖的过程如下:

  1. 创建 A 实例:

    • Spring 尝试获取 A,发现 A 不存在,开始创建 A。
    • 通过反射实例化 A(调用 A 的无参构造方法),得到一个 A 的原始对象。
    • 将 A 的 ObjectFactory 放入三级缓存(singletonFactories.put(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)))。
    • 将一个早期的 A 实例(尚未进行属性注入)放入二级缓存(earlySingletonObjects.put(beanName, bean))。
    • 开始对A进行属性注入
  2. A 注入 B:

    • Spring 发现 A 依赖 B,尝试获取 B。
    • 发现 B 不存在,开始创建 B。
    • 流程与创建 A 类似
  3. 创建 B 实例:

    • 通过反射实例化 B,得到一个 B 的原始对象。
    • 将 B 的 ObjectFactory 放入三级缓存。
    • 将一个早期的 B 实例放入二级缓存。
    • 开始对B进行属性注入
  4. B 注入 A:

    • Spring 发现 B 依赖 A,尝试获取 A。
    • 由于 A 已经在创建过程中,Spring 会依次从一级缓存、二级缓存、三级缓存中查找 A。
    • 在二级缓存中找到早期的 A 实例(虽然 A 还没有完成初始化,但 B 可以先持有 A 的引用)。
    • 将早期的 A 实例注入到 B 中。
  5. B 完成初始化:

    • B 完成属性注入和初始化,从二级缓存和三级缓存中移除,放入一级缓存(singletonObjects)。
  6. A 继续初始化:

    • A 继续进行属性注入,从一级缓存中获取到已经初始化完成的 B 实例。
    • A 完成初始化,从二级缓存和三级缓存中移除,放入一级缓存。

循环依赖解决的关键:

  • 提前暴露: 在 Bean 实例化后、初始化前,就将其放入二级缓存,提前暴露一个不完整的 Bean 实例。
  • 延迟创建: 使用 ObjectFactory 延迟创建 Bean 实例,直到真正需要时才创建。
  • 三级缓存: 通过三级缓存,Spring 可以在 Bean 创建的不同阶段获取到 Bean 的引用,从而解决循环依赖。

5. 为什么构造器注入无法解决循环依赖?

因为构造器注入要求在创建对象时就必须传入所有依赖,如果存在循环依赖,A 的构造方法需要 B 的实例,B 的构造方法又需要 A 的实例,这会导致无限递归,无法创建任何一个对象。

6. 为什么 Prototype 作用域无法解决循环依赖?

prototype 作用域的 Bean 每次获取都会创建一个新的实例,Spring 不会对 prototype Bean 进行缓存,因此无法通过提前暴露的方式解决循环依赖。

总结:

Spring 通过“三级缓存”机制,巧妙地解决了 Setter 方法注入和字段注入的循环依赖问题。

你可能感兴趣的:(Spring,Framework,2025,Java面试系列,spring,java,后端,循环依赖)