如何通过三级缓存解决 Spring 循环依赖

news2025/2/1 7:23:28

以下内容基于 Spring6.0.4。

这个其实是一个特别高频的面试题,松哥也一直很想和大家仔细来聊一聊这个话题,网上关于这块的文章很多,但是我一直觉得要把这个问题讲清楚还有点难度,今天我来试一试,看能不能和小伙伴们把这个问题梳理清楚。

这块我是打算整几篇文章,今天,我尽量不聊源码,单纯从设计思路方面来和小伙伴们梳理循环依赖该如何解决。

1. 循环依赖

1.1 什么是循环依赖

首先,什么是循环依赖?这个其实好理解,就是两个 Bean 互相依赖,类似下面这样:

@Service
public class AService {
    @Autowired
    BService bService;
}
@Service
public class BService {
    @Autowired
    AService aService;
}

AService 和 BService 互相依赖:

这个应该很好理解。

1.2 循环依赖的类型

一般来说,循环依赖有三种不同的形态,上面 1.1 小节是其中一种。

另外两种分别是三者依赖,如下图:

这种循环依赖一般隐藏比较深,不易发觉。

还有自我依赖,如下图:

一般来说,如果我们的代码中出现了循环依赖,则说明我们的代码在设计的过程中可能存在问题,我们应该尽量避免循环依赖的发生。不过一旦发生了循环依赖,Spring 默认也帮我们处理好了,当然这并不能说明循环依赖这种代码就没问题。实际上在目前最新版的 Spring 中,循环依赖是要额外开启的,如果不额外配置,发生了循环依赖就直接报错了。

另外,Spring 并不能处理所有的循环依赖,后面松哥会和大家进行分析。

2. 循环依赖解决思路

2.1 解决思路

那么对于循环依赖该如何解决呢?其实很简单,中加加入一个缓存就可以了,小伙伴们来看下面这张图:

我们在这里引入了一个缓存池。

当我们需要创建 AService 的实例的时候,会首先通过 Java 反射创建出来一个原始的 AService,这个原始 AService 可以简单理解为刚刚 new 出来(实际是刚刚通过反射创建出来)还没设置任何属性的 AService,此时,我们把这个 AService 先存入到一个缓存池中。

接下来我们就需要给 AService 的属性设置值了,同时还要处理 AService 的依赖,这时我们发现 AService 依赖 BService,那么就去创建 BService 对象,结果创建 BService 的时候,发现 BService 依赖 AService,那么此时就先从缓存池中取出来 AService 先用着,然后继续 BService 创建的后续流程,直到 BService 创建完成后,将之赋值给 AService,此时 AService 和 BService 就都创建完成了。

可能有小伙伴会说,BService 从缓存池中拿到的 AService 是一个半成品,并不是真正的最终的 AService,但是小伙伴们要知道,咱们 Java 是引用传递(也可以认为是值传递,只不过这个值是内存地址),BService 当时拿到的是 AService 的引用,说白了就是一块内存地址而已,根据这个地址找到的就是 AService,所以,后续如果 AService 创建完成后,BService 所拿到的 AService 就是完整的 AService 了。

那么上面提到的这个缓存池,在 Spring 容器中有一个专门的名字,就叫做 earlySingletonObjects,这是 Spring 三级缓存中的二级缓存,这里保存的是刚刚通过反射创建出来的 Bean,这些 Bean 还没有经历过完整生命周期,Bean 的属性可能都还没有设置,Bean 需要的依赖都还没有注入进来。另外两级缓存分别是:

  • singletonObjects:这是一级缓存,一级缓存中保存的是所有经历了完整生命周期的 Bean,即一个 Bean 从创建、到属性赋值、到各种处理器的执行等等,都经历过了,就存到 singletonObjects 中,当我们需要获取一个 Bean 的时候,首先会去一级缓存中查找,当一级缓存中没有的时候,才会考虑去二级缓存。
  • singletonFactories:这是三级缓存。在一级缓存和二级缓存中,缓存的 key 是 beanName,缓存的 value 则是一个 Bean 对象,但是在三级缓存中,缓存的 value 是一个 Lambda 表达式,通过这个 Lambda 表达式可以创建出来目标对象的一个代理对象。

有的小伙伴可能会觉得奇怪,按照上文的介绍,一级缓存和二级缓存就足以解决循环依赖了,为什么还冒出来一个三级缓存?那就得考虑 AOP 的情况了!

2.2 存在 AOP 怎么办

上面松哥给大家介绍的是普通的 Bean 创建,那确实没有问题。但是 Spring 中还有一个非常重要的能力,那就是 AOP。

说到这里,我得先和小伙伴么说一说 Spring 中 AOP 的创建流程。

正常来说是我们首先通过反射获取到一个 Bean 的实例,然后就是给这个 Bean 填充属性,属性填充完毕之后,接下来就是执行各种 BeanPostProcessor 了(不了解 BeanPostProcessor 的小伙伴可以看松哥之前发的文章 BeanFactoryPostProcessor 和 BeanPostProcessor 有什么区别?),如果这个 Bean 中有需要代理的方法,那么系统就会自动配置对应的后置处理器,松哥举一个简单例子,假设我有如下一个 Service:

@Service
public class UserService {

    @Async
    public void hello() {
        System.out.println("hello>>>"+Thread.currentThread().getName());
    }
}

那么系统就会自动提供一个名为 AsyncAnnotationBeanPostProcessor 的处理器,在这个处理器中,系统会生成一个代理的 UserService 对象,并用这个对象代替原本的 UserService。

那么小伙伴们要搞清楚的是,原本的 UserService 和新生成的代理的 UserService 是两个不同的对象,占两块不同的内存地址!!!

我们再来回顾下面这张图:

如果 AService 最终是要生成一个代理对象的话,那么 AService 存到缓存池的其实还是原本的 AService,因为此时还没到处理 AOP 那一步(要先给各个属性赋值,然后才是 AOP 处理),这就导致 BService 从缓存池里拿到的 AService 是原本的 AService,等到 BService 创建完毕之后,AService 的属性赋值才完成,接下来在 AService 后续的创建流程中,AService 会变成了一个代理对象了,不是缓存池里的 AService 了,最终就导致 BService 所依赖的 AService 和最终创建出来的 AService 不是同一个。

为了解决这个问题,Spring 引入了三级缓存 singletonFactories。

singletonFactories 的工作机制是这样的(假设 AService 最终是一个代理对象):

当我们创建一个 AService 的时候,通过反射刚把原始的 AService 创建出来之后,先去判断当前一级缓存中是否存在当前 Bean,如果不存在,则:

  1. 首先向三级缓存中添加一条记录,记录的 key 就是当前 Bean 的 beanName,value 则是一个 Lambda 表达式 ObjectFactory,通过执行这个 Lambda 可以给当前 AService 生成代理对象。
  2. 然后如果二级缓存中存在当前 AService Bean,则移除掉。

现在继续去给 AService 各个属性赋值,结果发现 AService 需要 BService,然后就去创建 BService,创建 BService 的时候,发现 BService 又需要用到 AService,于是就先去一级缓存中查找是否有 AService,如果有,就使用,如果没有,则去二级缓存中查找是否有 AService,如果有,就使用,如果没有,则去三级缓存中找出来那个 ObjectFactory,然后执行这里的 getObject 方法,这个方法在执行的过程中,会去判断是否需要生成一个代理对象,如果需要就生成代理对象返回,如果不需要生成代理对象,则将原始对象返回即可。最后,把拿到手的对象存入到二级缓存中以备下次使用,同时删除掉三级缓存中对应的数据。这样 AService 所依赖的 BService 就创建好了。

接下来继续去完善 AService,去执行各种后置的处理器,此时,有的后置处理器想给 AService 生成代理对象,发现 AService 已经是代理对象了,就不用生成了,直接用已有的代理对象去代替 AService 即可。

至此,AService 和 BService 都搞定。

本质上,singletonFactories 是把 AOP 的过程提前了。

3. 小结

总的来说,Spring 解决循环依赖把握住两个关键点:

  • 提前暴露:刚刚创建好的对象还没有进行任何赋值的时候,将之暴露出来放到缓存中,供其他 Bean 提前引用(二级缓存)。
  • 提前 AOP:A 依赖 B 的时候,去检查是否发生了循环依赖(检查的方式就是将正在创建的 A 标记出来,然后 B 需要 A,B 去创建 A 的时候,发现 A 正在创建,就说明发生了循环依赖),如果发生了循环依赖,就提前进行 AOP 处理,处理完成后再使用(三级缓存)。

原本 AOP 这个过程是属性赋完值之后,再由各种后置处理器去处理 AOP 的(AbstractAutoProxyCreator),但是如果发生了循环依赖,就先 AOP,然后属性赋值,最后等到后置处理器执行的时候,就不再做 AOP 的处理了。

不过需要注意,三级缓存并不能解决所有的循环依赖,这个松哥后面继续整文章和大家细聊。

好啦,今天这篇文章需要一定的 Spring 源码基础方能理解哦,后面松哥会再整一篇文章从源码的角度和小伙伴们验证本文的每一个细节。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/762752.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一张证,三年月薪翻三倍!

18年9月,我获取了PMP(项目管理)认证,19年6月获取了PgMP(项目集群管理)认证。考证过程并不是很难,月薪却从1万突破3万,也找到了自己喜欢和擅长的工作领域,获益无穷。 什么…

Navicat 用户权限功能 | 预防 MySQL 删库风险

近期,我们后台收到一位用户的问询,有关于误删库的解决办法。对于企业来说,这可能是一个大事故!但幸运的是,该用户在不久之前看了我们的 Navicat 自动备份功能文章,并且实施了数据库备份操作,所以…

如何下载SRA存放在AWS的原始数据

通常,我们都是利用prefetch从NCBI上获取数据,然后用fasterp-dump/fastq-dump 转成fastq。但遗憾的SRA的数据是原数据的有损压缩,比如说我19年参与发表的文章里单细胞数据上传的是3个文件,但是当时的faster-dump/fastq-dump只能拆出…

MongoDB源码安装

文章目录 MongoDB源码安装:注:下载:解压:创建数据目录:创建软链接:创建变量脚本:执行脚本:启动mongodb:检查:连接mongodb: MongoDB源码安装: 注&…

Flutter系列文章-Flutter基础

Flutter是Google推出的一种新的移动应用开发框架,允许开发者使用一套代码库同时开发Android和iOS应用。它的设计理念、框架结构、以及对Widget的使用,都让开发者能更有效率地创建高质量的应用。 一、Flutter设计理念 Flutter的设计理念是“一切皆为Wid…

安装hive数据仓库

部署hive数据库 环境准备 需要安装部署完成的Hadoop的环境如果不会搭建的可以参考: 安装mysql 卸载Centos7自带的mariadb rpm -qa|grep mariadbrpm -e mariadb-libs-5.5.64-1.el7.x86_64 --nodepsrpm -qa|grep mariadb mariadb-libs-5.5.64-1.el7.x86_64是使用…

ToT: 利用大语言模型进行有意识的问题解决(上)

ToT 摘要介绍利用大语言模型进行有意识的问题解决1. 思维分解2. 思维产生 G(p,s,k)3. 状态评估V(p,S)4. 搜索算法 实验相关工作讨论 原文: 摘要 语言模型正在迅速成为一般问题解决的部署,但在推理过程中仍然局限于 标记级别(token-level&…

uniapp左右滑动切换月份

左右滑动触发事件 给组件绑定事件,主要利用组件的触摸开始和触摸结束事件来实现: <view @touchstart="touchStart" @touchend="touchEnd"> 2,声明初始化点击位置变量startX data() {return {list:[],pageNum:1,pageSize:10,//初始化点击位置…

手撕Spring06

概述 该章节通过各种Context解决上下文问题&#xff0c;使用模版方法的设计模式&#xff0c;并增加了bean实例化之前、beanc初始化前后的扩展点整体设计 知识点补充 类图 context context包下主要是传递上下文、调用core.io、beans等包下的实际功能完成&#xff0c;配置文件…

12.11 FS4412开发环境搭建

目录 开发边硬件资源介绍 地址映射表 硬件控制原理 load/store 地址映射表4个G包括 开发边硬件资源介绍 地址映射表 硬件控制原理 1.数据运算指令&#xff08;CPU内部&#xff09; 2.跳转指令&#xff08;CPU内部&#xff09; 3.load/store&#xff08;通过读写对硬件…

测试的基本概念(测试系列2)

目录 前言&#xff1a; 1.什么是需求 1.1需求的定义 1.2为什么有软件需求 1.3测试人眼里的需求 2测试用例 2.1什么是测试用例 2.2为什么要有测试用例 3.软件错误&#xff08;BUG&#xff09; 3.1什么是bug 4.软件的生命周期 5.开发模型 5.1瀑布模型 5.2螺旋模型 …

【PHP源码】手术麻醉管理系统介绍

手术麻醉管理系统是什么&#xff1f; 手术麻醉信息管理系统是数字化手段应用于手术过程中的重要组成部分&#xff0c;用数字形式获取并存储手术相关信息&#xff0c;既便捷又高效。既然是管理系统&#xff0c;那就是一整套流程&#xff0c;管理患者手术、麻醉的申请、审批、安…

c#语法问题记录

1.using using 语句定义一个范围&#xff0c;在此范围的末尾将释放对象。using 指令为命名空间创建别名&#xff0c;或导入在其他命名空间中定义的类型。 2. internal sealed partial class internal&#xff1a; 关键字是类型和类型成员的访问修饰符。sealed &#xff1a;应…

IndexDB完整使用指南

IndexDB完整使用指南 数据存储是大多数 Web 应用程序的重要组成部分&#xff0c;从跟踪用户数据到应用程序数据。随着更快、更强大的 Web 应用程序的快速开发&#xff0c;需要高效的客户端存储来帮助开发。 多年来&#xff0c;Web 上的客户端存储已经发生了很大的变化&#x…

极智AI | torch与torchvision版本对应关系速查

欢迎关注我的公众号 [极智视界]&#xff0c;获取我的更多经验分享 大家好&#xff0c;我是极智视界&#xff0c;本文来介绍一下 torch与torchvision版本的对应关系。 邀您加入我的知识星球「极智视界」&#xff0c;星球内有超多好玩的项目实战源码下载&#xff0c;链接&#x…

2023年湖北安全员ABC考试题型有哪些?考多少分及格呢?

2023年湖北安全员ABC考试题型有哪些&#xff1f;考多少分及格呢&#xff1f; 安全员分为交通厅、水利厅和建设厅三个部门颁发&#xff0c;每个部门发的安全员证书也是不一样的。那么今天启程别跟大家讲的是建设厅安全员ABC证书报考的一些事宜&#xff08;启程别是谁&#xff1f…

稀疏光流法跟中移动物体、监督学习聚类、K均值聚类、加载深度神经网络模型、深度神经网络模型的使用

目录 1、稀疏光流法跟中移动物体 2、监督学习聚类 3、K均值聚类 4、加载深度神经网络模型 5、深度神经网络模型的使用 1、稀疏光流法跟中移动物体 //稀疏光流法跟中移动物体 vector<Scalar> color_lut;//颜色查找表 void draw_lines(Mat &image, vector<Point…

程序员如何90天成功转行黑客(网络安全)?

有人说&#xff1a;”黑客到底比程序员高在哪&#xff0c;为什么很多人开始转行了“ 其实黑客都是程序员&#xff0c;但是并不是所有的程序员都是黑客. 从企业和社会需求来看&#xff0c;现在真不缺程序猿 &#xff0c;反而大量的缺安全工程师 &#xff0c;同样8000块月薪&am…

【Spring框架】spring更简单的读取和存储对象

目录 前置工作更加简单的存储Bean对象类注解存储Bean命名问题Java项目标准分层五大类注解之间的关系方法注解Bean重命名Bean的几种方式 前置工作 在spring-config.xml添加如下配置&#xff1a; 配置bean的扫描根路径&#xff1a;只有当前目录下的类才会扫描是否添加了注解&…

【Android Framework系列】第7章 WMS原理

1 前言 前面【Android Framework系列】第5章 AMS启动流程和【Android Framework系列】第6章 AMS原理之Launcer启动流程我们分析了AMS启动以及Launcher启动的整体流程&#xff0c;那Launcher(Activity启动)后&#xff0c;UI是如何渲染到屏幕并且展示出来的呢&#xff1f;我们这…