【Spring】循环依赖

发布于:2025-03-19 ⋅ 阅读:(11) ⋅ 点赞:(0)

        在使用 Spring 框架进行开发的过程中,我们可能会遇到一种棘手的问题 —— 循环依赖。循环依赖不仅会影响程序的正常运行,还可能导致系统性能下降,甚至出现死锁等严重问题。那么,Spring 是如何巧妙地解决这一难题的呢?今天,就让我们一起深入探究 Spring 解决循环依赖的奥秘。

1. 什么是循环依赖

        循环依赖(Circular Dependency)是指两个或多个模块、对象之间相互依赖,形成一个封闭的依赖环。简而言之,模块A依赖于模块B,而模块B又依赖于模块A,这会导致依赖链的循环,无法确定加载或初始化的顺序。

        如下面代码所示:

@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private A a;
}

//或者自己依赖自己
@Service
public class A {
    @Autowired
    private A a;
}

2. 如何解决循环依赖

        要想解决循环依赖,关键就是提前暴露未完全创建完毕的 Bean。Spring 解决循环依赖主要依赖于其强大的 Bean 工厂和三级缓存机制。在深入了解这一机制之前,我们先来看看 Spring 创建 Bean 的基本流程。

2.1 Spring 创建 Bean 的基本流程

1)实例化 Bean:Spring 容器根据 Bean 的定义,使用反射机制创建一个 Bean 的实例。​

2)填充 Bean 的属性:在 Bean 实例化之后,Spring 会根据 Bean 的定义,为其填充依赖的属性。​

3)初始化 Bean:在属性填充完成后,Spring 会调用 Bean 的初始化方法(如果有定义),对 Bean 进行进一步的初始化操作。

2.2 三级缓存机制

Spring 通过三级缓存来解决循环依赖问题,这三级缓存分别是:​

1)singletonObjects:一级缓存,用于存储已经创建并初始化完成的单例 Bean。当 Spring 从这个缓存中获取到 Bean 时,说明该 Bean 已经可以直接使用。​

2)earlySingletonObjects:二级缓存,用于存储已经实例化但尚未完成属性填充和初始化的单例 Bean。这个缓存中的 Bean 处于一个 “半成品” 状态。​

3)singletonFactories:三级缓存,用于存储 Bean 的工厂对象。这些工厂对象可以用来创建 Bean 的早期引用。

2.3 解决循环依赖的具体过程

假设我们有两个相互依赖的 Bean,Bean A 和 Bean B。当 Spring 创建 Bean A 时,它会按照以下步骤进行:​

1)实例化 Bean A:Spring 首先在一级缓存singletonObjects中查找 Bean A,如果没有找到,则创建一个 Bean A 的实例。​

2)将 Bean A 的早期引用放入三级缓存:在 Bean A 实例化之后,Spring 会将一个能够获取 Bean A 早期引用的工厂对象放入三级缓存singletonFactories中。这个早期引用是一个还没有完成属性填充和初始化的 Bean A。​

3)填充 Bean A 的属性:此时,Spring 发现 Bean A 依赖于 Bean B,于是开始创建 Bean B。​

4)实例化 Bean B:Spring 在一级缓存singletonObjects中查找 Bean B,没有找到后创建 Bean B 的实例。​

5)将 Bean B 的早期引用放入三级缓存:类似于 Bean A,Spring 将一个能够获取 Bean B 早期引用的工厂对象放入三级缓存singletonFactories中。​

6)填充 Bean B 的属性:在填充 Bean B 的属性时,Spring 发现 Bean B 依赖于 Bean A。这时,Spring 会从三级缓存singletonFactories中获取 Bean A 的早期引用,并将其注入到 Bean B 中。​

7)完成 Bean B 的初始化:Bean B 完成属性填充和初始化后,将其放入一级缓存singletonObjects中,并从三级缓存singletonFactories中移除对应的工厂对象。​

8)完成 Bean A 的初始化:回到 Bean A 的属性填充过程,由于 Bean B 已经创建并初始化完成,Spring 将 Bean B 从一级缓存singletonObjects中取出并注入到 Bean A 中。然后,Bean A 完成初始化,被放入一级缓存singletonObjects中,并从三级缓存singletonFactories中移除对应的工厂对象。​

通过这样的方式,Spring 巧妙地利用三级缓存机制,打破了循环依赖的僵局,成功创建了相互依赖的 Bean。

3. 为什么 Spring 循环依赖需要三级缓存

       当 Bean A 完成实例化后,将其存入earlySingletonObjects二级缓存中。随即进入 Bean A 的属性注入阶段,此时发现 Bean A 依赖于 Bean B ,于是 Spring 容器开始创建 Bean B。在创建 Bean B 的过程中,当执行到填充 Bean B 属性且其依赖 Bean A 时,由于 Bean A 已在earlySingletonObjects中,Bean B 可从该二级缓存获取到 Bean A 的早期引用,进而完成 Bean B 的创建流程。待 Bean B 创建完成并完成相关初始化操作后,Spring 容器再回到 Bean A 的属性注入环节,利用已创建好的 Bean B 完成 Bean A 剩余的属性注入及初始化操作,从而使 Bean A 得以完整创建。

        很明显,如果仅仅只是为了破解循环依赖,二级缓存够了,压根就不必要三级。所以为什么要搞个三级缓存,且里面存的是创建 Bean 的工厂呢

3.1 二级缓存的局限性

        假如只有二级缓存,会出现什么问题呢?假设仅有earlySingletonObjects,在创建 Bean A 时,实例化后将其放入二级缓存,接着填充属性发现依赖 Bean B,开始创建 Bean B。当 Bean B 填充属性需要 Bean A 时,从二级缓存获取到的 Bean A 是未完成初始化的。此时若直接使用这个未初始化完全的 Bean A,在某些场景下会引发错误。​

        比如,若 Bean A 在初始化过程中有一些后置处理器的操作,这些操作可能会改变 Bean A 的状态或添加额外功能。在二级缓存中获取到的未初始化 Bean A 并没有经过这些后置处理器处理。如果 Bean B 直接使用了这个不完整的 Bean A,后续可能会导致程序运行异常,因为 Bean A 的预期功能可能并未完全实现。

3.2 三级缓存的关键作用

3.2.1 支持 AOP 代理

        在 Spring 中,AOP 是非常重要的功能。当 Bean 被代理时,情况就变得复杂起来。假设 Bean A 需要被代理,在仅有二级缓存的情况下,Bean A 实例化后放入二级缓存,在初始化之前,它还不是代理对象。如果此时 Bean B 从二级缓存获取 Bean A,得到的就是未代理的对象,这显然不符合预期,因为 Bean A 的业务逻辑可能需要通过代理来增强。​

        而三级缓存的存在解决了这个问题。在 Bean A 实例化后,将其工厂对象放入三级缓存。当 Bean B 需要 Bean A 时,从三级缓存获取工厂对象来创建 Bean A 的早期引用。如果 Bean A 需要被代理,工厂对象会在合适的时机创建出代理对象,这样即使 Bean A 还未完成初始化,Bean B 获取到的也是经过代理增强的 Bean A,保证了 AOP 功能的正常运作。

3.2.2 保证 Bean 的正确创建顺序

        三级缓存使得 Spring 能够更好地控制 Bean 的创建顺序。在创建复杂依赖关系的 Bean 时,通过工厂对象,Spring 可以在合适的时机创建和提供 Bean 的早期引用。例如,对于一些有复杂初始化逻辑的 Bean,先将其工厂对象放入三级缓存,在其他 Bean 需要它时,根据工厂对象创建的早期引用可以保证在合适的阶段被使用,同时不会干扰到自身后续的初始化流程。如果只有二级缓存,无法精准地控制创建顺序,可能导致部分 Bean 在错误的阶段被使用,从而引发各种难以排查的问题。

        下面通过简单的代码示例进一步说明:

@Component
public class BeanA {
    private BeanB beanB;
    @Autowired
    public BeanA(BeanB beanB) {
        this.beanB = beanB;
    }
    // 假设这里有一些初始化方法
    @PostConstruct
    public void init() {
        System.out.println("BeanA初始化");
    }
}

@Component
public class BeanB {
    private BeanA beanA;
    @Autowired
    public BeanB(BeanA beanA) {
        this.beanA = beanA;
    }
    // 假设这里有一些初始化方法
    @PostConstruct
    public void init() {
        System.out.println("BeanB初始化");
    }
}

        如果只有二级缓存,当 Bean B 获取 Bean A 时,可能得到的是未执行@PostConstruct方法的 Bean A。而有了三级缓存,Spring 可以确保 Bean A 在合适的阶段被创建和使用,保证了@PostConstruct方法等初始化操作的正确执行顺序。