Spring中Bean的循环依赖问题(什么是循环依赖、Spring是如何解决循环依赖问题的、第三级缓存详解、Spring框架无法解决的循环依赖问题、第二级缓存的核心作用)

15 篇文章 0 订阅
11 篇文章 0 订阅


阅读本文前建议先阅读我的另一篇博文: Spring中Bean的生命周期

1. 什么是循环依赖

在 Spring 框架中,循环依赖问题指的是两个或多个 Bean 在创建时互相引用对方,形成了一个闭环,导致无法完成依赖注入的情况


循环依赖示例一

在这里插入图片描述

循环依赖示例二

在这里插入图片描述


循环依赖会导致死循环的问题,我们以循环依赖示例一为例,分析一下形成死循环的过程

在这里插入图片描述

值得注意的是,如果代码中出现循环依赖问题,说明我们的代码结构有问题,需要重新考虑依赖关系和组件设计

2. Spring是如何解决循环依赖问题的

Spring 框架已经为我们解决了大部分的循环依赖问题,那 Spring 是如何解决循环依赖问题的呢

2.1 三级缓存

Spring 框架中有一个名为 DefaultSingletonBeanRegistry 的类

在这里插入图片描述

DefaultSingletonBeanRegistry 类里面有三个集合(三个 Map),我们也称这三个集合为三级缓存,Spring 就是利用三级缓存解决了循环依赖的问题

缓存名称源码名称作用
一级缓存singletonObjects单例池,缓存已经走完完整的生命周期的 Bean 对象
二级缓存earlySingletonObjects缓存早期的 Bean 对象(生命周期还没走完)
三级缓存singletonFactories缓存的是 0bjectFactory,表示对象工厂,用于创建某个对象

三级缓存的每一级缓存都只存放单例对象,如果某一个 Bean 是多例的,获取 Bean 的时候才会创建实例,创建出来的对象也不会放到三级缓存中

2.2 仅仅使用第一级缓存能不能解决循环依赖的问题

我们先来看一下,如果仅仅使用第一级缓存,能不能解决循环依赖的问题

我们回顾一下循环依赖导致的死循环问题

在这里插入图片描述

因为一个 Bean 完整地走完了生命周期之后,才能存放到单例池里面,但是在上图中没有任何一个 Bean 走完了生命周期,也就是说没有任何一个对象可以存放到单例池里面,所以仅仅使用一级缓存,解决不了循环依赖的问题

2.3 第一级缓存和第二级缓存一起使用能不能解决循环依赖的问题

既然仅仅使用一级缓存,不能解决循环依赖的问题,那一级缓存和二级缓存一起使用能不能解决循环依赖的问题呢

在这里插入图片描述

A 对象创建成功之后,A 对象也会存放到单例池中,同时清除掉二级缓存中的半成品对象

在这里插入图片描述

通过一起使用一级缓存和二级缓存,已经解决了简单的循环依赖问题

2.4 第一级缓存和第二级缓存能完全解决循环依赖的问题吗

初步来看,我们通过一起使用一级缓存和二级缓存已经解决循环依赖的问题了,但事实真的如此吗

如果 A 是一个代理对象,只用一级缓存和二级缓存能不能完成呢,按照上述逻辑,存入单例池中的并不是 A 的代理对象

在这里插入图片描述

所以说,第一级缓存和第二级缓存只能解决一般对象的循环依赖问题,如果某个对象被增强了,也就是说该对象是一个代理对象的话,就需要借助三级缓存了


接下来我们看一下三级缓存解决循环依赖的过程

在这里插入图片描述

对象工厂可以产生代理对象(对象需要增强)或普通对象

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3. 补充:第三级缓存详解

3.1 第三级缓存里面存的是什么

函数接口(Functional Interface):只包含一个抽象方法的接口

第三级缓存(singletonFactories)里面缓存的是函数接口,利用 lambda 表达式进行传递,传递的是 Bean 的实例还有 Bean 的名字

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
       isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    if (logger.isTraceEnabled()) {
       logger.trace("Eagerly caching bean '" + beanName +
             "' to allow for resolving potential circular references");
    }
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

以上代码摘自 AbstractAutowireCapableBeanFactory 类的 doCreateBean 方法


第三级缓存不会立即调用,而且调用三级缓存之后生成的对象会存放在二级缓存当中(有可能是实例对象,也有可能是代理对象,取决于对象需不需要增强)

为什么要放到二级缓存中呢,主要是为了避免出现重复创建代理对象的问题

来看以下例子,如果 A 和 B 相互依赖,同时 A 和 C 也相互依赖,也就是说,A 被依赖了两次,就有可能会出现创建两次代理对象的情况,所以为了避免出现重复创建的问题,引入了二级缓存

在这里插入图片描述

3.2 为什么第三级缓存不会立即调用

如果第三级缓存立即调用的话,对于所有需要进行 AOP 增强的 Bean 来说,不管是否出现了循环依赖问题,都会在实例化后创建代理对象

在这里插入图片描述

但我们知道,对于一个正常的 Bean (也就是没有出现循环依赖问题的 Bean)来说,是在初始化的过程中创建动态代理对象的

Spring 官方还是希望正常的 Bean 遵循正常的 Bean 的生命周期,在初始化的过程中创建动态代理

对于出现了循环依赖问题的 Bean 来说,创建动态代理对象的过程被提前到了属性注入这一步,是为了解决循环依赖问题做出的让步


这么一想,是不是感觉 Spring 官方写的代码环环相扣、思路清晰

4. Spring框架无法解决的循环依赖问题(人工解决)

上面说,Spring 框架使用三级缓存帮我们解决了大部分的循环依赖问题,为什么说是大部分呢,因为有些循环依赖的情况 Spring 框架解决不了,需要我们手动解决

比较典型的就是构造器注入产生的循环依赖问题

在这里插入图片描述

我们先回顾一下 Bean 的生命周期

在这里插入图片描述

Bean 生命周期的第一步就是调用构造函数,两个 Bean 都采用了构造器注入的方式

如果没有解决构造器注入的问题,项目启动的时候就会报以下错误

Is there an unresolvable circular reference?


Spring 的三级缓存只能解决初始化过程中产生的循环依赖问题,但不能解决构造函数产生的循环依赖问题,那我们该如何解决构造器注入导致的循环依赖问题呢

我们可以添加一个注解来解决构造器注入导致的循环依赖问题

在这里插入图片描述

@Lazy 注解表示需要使用对象 B 的时候才会去创建对象 B,而不是在实例化的时候就把对象 B 注入进来

但是 A 确实只有一个构造函数,而且 A 的构造函数确实要使用到 B,那为什么添加 @lazy 注解就可以解决构造器注入的循环依赖问题了呢

以上述例子来说,使用了 @Lazy 注解后,会为 B 创建一个临时的 CGLIB 动态代理对象,接着返回这个临时对象给 A,当 A 真正需要用到 B 的时候,才会去创建 B 的示例

5. 第二级缓存的核心作用

我们先来回顾使用完整的三级缓存的解决循环依赖的流程图

在这里插入图片描述

其实不需要借助二级缓存,只利用一级缓存和三级缓存就能解决存在代理对象的循环依赖问题,那二级缓存有什么用呢

在对象为单例的情况下,如果需要某个对象的代理对象(在上图中是 B 需要 A 的代理对象),先通过 ObjectFactory 对象创建代理对象,接着将代理对象存放到二级缓存中,直接从二级缓存中取就可以了。而且在将 B 注入给 A 的时候,使用 的 A 也是从二级缓存中取出来的对象

如果多次调用 ObjectFactory 来生产对象,可能会产生多例的情况,处理起来更加复杂,所以 ObjectFactory 生产出来的对象,统一放到二级缓存中,使用的时候直接拿出来就可以了,无需再次生成对象(具体可参考本文的 补充:第三级缓存 部分)


以下是二级缓存的核心作用:

5.1 提前暴露对象引用

当 Spring 创建一个单例 Bean 时,它首先会实例化这个 Bean,然后填充其属性。在填充属性之前,Spring 容器会通过一个 ObjectFactory 接口的实现将创建的 Bean 实例的早期引用(即尚未完全初始化的 Bean 实例)放入三级缓存(singletonFactories

如果其他 Bean 在此时需要依赖这个正在创建的 Bean,Spring 容器可以从三级缓存中获取这个早期引用,并将其移动到二级缓存中。这样,即使原始 Bean 还没有完全初始化,它的引用也可以被其他 Bean 使用,从而解决了循环依赖问题

5.2 保证对象的唯一性

由于 Spring 框架保证每个单例 Bean 在容器中只有一个实例,二级缓存确保了在解决循环依赖的过程中不会创建重复的实例。一旦 Bean 的早期引用被放入二级缓存,后续对同一个 Bean 的请求都会直接从二级缓存中获取这个引用,而不是再次创建新的实例

5.3 避免重复创建代理对象

在没有二级缓存的情况下,Spring 容器可能会在解决循环依赖时多次创建同一个 Bean 的代理对象,这不仅效率低下,而且违反了单例的原则。二级缓存通过存储早期引用,确保了即使在循环依赖的情况下,Bean 也只会被创建一次

会创建重复的实例。一旦 Bean 的早期引用被放入二级缓存,后续对同一个 Bean 的请求都会直接从二级缓存中获取这个引用,而不是再次创建新的实例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

聂 可 以

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值