【Spring】循环依赖

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

1. 什么是循环依赖

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

如下面代码所示:

java 复制代码
@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 在错误的阶段被使用,从而引发各种难以排查的问题。

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

java 复制代码
@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方法等初始化操作的正确执行顺序。

相关推荐
米花米唐4 分钟前
Spring AI 2.0 会话记忆开发实践
后端
漫随流水18 分钟前
Java——springboot web案例
java·开发语言·spring boot
从此以后自律26 分钟前
Java Object 类常用方法全讲解
java·开发语言
念何架构之路1 小时前
Docker 容器状态机 学习
java·学习·docker
你有我备注吗1 小时前
SQL学习之查询
java·sql·学习
从零开始的代码生活_1 小时前
C++ stack、queue 与 priority_queue:容器适配器原理与实战
开发语言·c++·后端·学习·算法
爱勇宝1 小时前
《道德经》第6章:别把团队的创造力用到枯竭
前端·后端
无双_Joney1 小时前
[四期 - 4] NestJS专栏 - 征召一下大家的意见,非常想知道大家都想看看Nest的哪些方面
前端·后端·nestjs
彭亚川Allen1 小时前
停更的这一年,All In AI、陪媳妇生娃、还考了个研
前端·后端·产品经理
techdashen1 小时前
Go 1.25 新增 `reflect.TypeAssert`:更直接、更高效地从 `reflect.Value` 取出类型值
开发语言·后端·golang