Spring的循环依赖问题

文章目录

  • 1.什么是循环依赖
  • 2.代码演示
  • 3.分析问题
  • 4.问题解决
  • 5.Spring循环依赖
  • 6. 疑问点
    • 6.1 为什么需要三级缓存
    • 6.2 没有三级缓存能解决吗?
    • 6.3 三级缓存分别什么作用

1.什么是循环依赖

image-20231108002206734

上图是循环依赖的三种情况,虽然方式有点不一样,但是循环依赖的本质是一样的,就你的完整创建要依赖与我,我的完整创建也依赖于你。相互依赖从而没法完整创建造成失败。

2.代码演示

public class CircularTest {public static void main(String[] args) {// 出现了循环依赖的情况,死循环-OOMnew CircularServiceA();}
}class CircularServiceA {// A 中依赖了 Bprivate CircularServiceB circularServiceB = new CircularServiceB();
}class CircularServiceB {// B 中依赖了 Aprivate CircularServiceA circularServiceA = new CircularServiceA();
}

执行后出现了 StackOverflowError 错误:

image-20231108002757556

上面的就是最基本的循环依赖的场景,你需要我,我需要你,然后就报错了。而且上面的这种设计情况我们是没有办法解决的。那么针对这种场景我们应该要怎么设计呢?这个是关键!

3.分析问题

首先我们要明确一点就是如果这个对象 A 还没创建成功,在创建的过程中要依赖另一个对象 B,而另一个对象 B 也是在创建中要依赖对象 A,这种肯定是无解的。

这时我们就要转换思路,我们先把 A 创建出来,但是还没有完成初始化操作,也就是这是一个半成品的对象,然后在赋值的时候先把 A 暴露出来,然后创建B,让 B 创建完成后找到暴露的 A 完成整体的实例化,这时再把 B 交给 A,完成 A 的后续操作,从而揭开了循环依赖的密码。

image-20231108012150662

4.问题解决

明白了上面的本质后,我们可以自己来尝试解决下。先来把上面的案例改为 set/get 来依赖关联,然后我们再通过把对象实例化和成员变量赋值拆解开来处理。从而解决循环依赖的问题。

public class CircularTest {public static void main(String[] args) throws Exception {// 需要把构造方法和属性赋值作为一个整体,需要提供一个获取实例对象的方法System.out.println(getBean(CircularServiceA.class).getCircularServiceB()); // com.zhulang.circular.CircularServiceB@74a14482System.out.println(getBean(CircularServiceB.class)); // com.zhulang.circular.CircularServiceB@74a14482}// 存储半成品的容器,解决半成品的关键点private static final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();/*** 根据类型获取对应的实例对象* 1.完成构造* 2.完成成员变量的赋值** @param className* @param <T>* @return* @throws Exception*/@SuppressWarnings("unchecked")public static <T> T getBean(Class<T> className) throws Exception {// 1.获取类对象对应的名称String beanName = className.getSimpleName().toLowerCase();// 2.根据名称去 singletonObjects 中查看是否有半成品的对象if (singletonObjects.containsKey(beanName)) {return (T) singletonObjects.get(beanName);}// 3.singletonObjects 没有半成品的对象,那么就反射实例化对象T t = className.newInstance();// 4.把这个半成品对象存储在 singletonObjects 中singletonObjects.put(beanName, t);// 5.获取所有的成员变量Field[] declaredFields = className.getDeclaredFields();// 6.遍历成员变量,依次赋值for (Field field : declaredFields) {// 6.1 进行爆破,针对 private 修饰的对象field.setAccessible(true);// 6.2 获取成员变量 对应的类对象Class<?> fieldType = field.getType();// 6.3 给成员变量赋值 如果 singletonObjects 中有半成品就获取,否则创建对象field.set(t, getBean(fieldType));}return t;}
}class CircularServiceA {// A 中依赖了 Bprivate CircularServiceB circularServiceB;public CircularServiceB getCircularServiceB() {return circularServiceB;}public void setCircularServiceB(CircularServiceB circularServiceB) {this.circularServiceB = circularServiceB;}
}class CircularServiceB {// B 中依赖了 Aprivate CircularServiceA circularServiceA;public CircularServiceA getCircularServiceA() {return circularServiceA;}public void setCircularServiceA(CircularServiceA circularServiceA) {this.circularServiceA = circularServiceA;}
}

在上面的方法中的核心是 getBean 方法,A 创建后填充属性时依赖 B,那么就去创建 B,在创建 B 开始填充时发现依赖于 A,但此时 A 这个半成品对象已经存放在缓存到 singletonObjects 中了,所以 B 可以正常创建,在通过递归把 A 也创建完整了。

最后总结下该案例解决的本质:

image-20231108013756569

5.Spring循环依赖

刚刚上面的案例中的对象的生命周期的核心就两个:

  1. 创建对象
  2. 属性填充

然后我们再来看看 Spring 中是如何解决循环依赖问题的呢?Spring 创建 Bean 的生命周期中涉及到的方法就很多了。下面是简单列举了对应的方法。

image-20231108014924282

基于前面案例的了解,我们知道肯定需要在调用构造方法方法创建完成后再暴露对象,在 Spring 中提供了三级缓存来处理这个事情,对应的处理节点如下图:

image-20231108015452529

  • 一级缓存:存储的是 成品Bean 对象 ,存储的所有的单例对象,其实可以说和循环依赖没有关系。

  • 二级缓存:存储的是 半成品对象,是解决循环依赖的关键,如果不去考虑 AOP 代理增加的情况,只有二级缓存的情况下也是可以解决循环依赖的,也就是不需要三级缓存。

  • 三级缓存:三级缓存存在的意义是解决 AOP 增强对象的原因,存储的是一个 Lambda 表达式(内部类)–> ObjectFactory。

对应到源码中具体处理循环依赖的流程如下:

image-20231110084404766

上面就是在Spring的生命周期方法中和循环依赖出现相关的流程了。那么源码中的具体处理是怎么样的呢?我们继续往下面看。

首先在调用构造方法的后会放入到三级缓存中

image.png

下面就是放入三级缓存的逻辑

	protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {Assert.notNull(singletonFactory, "Singleton factory must not be null");// 使用singletonObjects进行加锁,保证线程安全synchronized (this.singletonObjects) {// 如果单例对象的高速缓存【beam名称-bean实例】没有beanName的对象if (!this.singletonObjects.containsKey(beanName)) {// 将beanName,singletonFactory放到单例工厂的缓存【bean名称 - ObjectFactory】this.singletonFactories.put(beanName, singletonFactory);// 从早期单例对象的高速缓存【bean名称-bean实例】 移除beanName的相关缓存对象this.earlySingletonObjects.remove(beanName);// 将beanName添加已注册的单例集中this.registeredSingletons.add(beanName);}}}

然后在填充属性的时候会存入二级缓存中

earlySingletonObjects.put(beanName,bean);
registeredSingletons.add(beanName);

最后把创建的对象保存在了一级缓存中

	protected void addSingleton(String beanName, Object singletonObject) {synchronized (this.singletonObjects) {// 将映射关系添加到单例对象的高速缓存中this.singletonObjects.put(beanName, singletonObject);// 移除beanName在单例工厂缓存中的数据this.singletonFactories.remove(beanName);// 移除beanName在早期单例对象的高速缓存的数据this.earlySingletonObjects.remove(beanName);// 将beanName添加到已注册的单例集中this.registeredSingletons.add(beanName);}}

6. 疑问点

6.1 为什么需要三级缓存

三级缓存主要处理的是 AOP 的代理对象,存储的是一个 ObjectFactory。

三级缓存考虑的是带你对象,而二级缓存考虑的是性能-从三级缓存的工厂里创建出对象,再扔到二级缓存(这样就不用每次都要从工厂里拿)。

6.2 没有三级缓存能解决吗?

没有三级缓存是可以解决循环依赖问题的。

6.3 三级缓存分别什么作用

一级缓存:正式对象

二级缓存:半成品对象

三级缓存:工厂

在 Spring 框架中,singletonObjects、earlySingletonObjects 和 singletonFactories 是三个不同的数据结构,用于管理单例 Bean 的创建和缓存。

  • singletonObjects:该数据结构是一个哈希表,以 Bean 名称为键,存储已经完全初始化的单例 Bean 实例。当我们通过ApplicationContext.getBean() 方法请求获取一个单例 Bean 时,Spring 首先会从 singletonObjects 中查找是否存在该 Bean的实例,如果存在,则直接返回;如果不存在,则创建一个新的实例,并将其添加到 singletonObjects 中。

  • earlySingletonObjects:该数据结构也是一个哈希表,以 Bean 名称为键,存储正在创建过程中但尚未完全初始化的单例 Bean 实例。当 Spring 创建一个单例 Bean 时,它会先将其实例化并放入 earlySingletonObjects 中。在 Bean 的创建过程中,如果其他 Bean 有对该 Bean 的循环引用,就会出现循环依赖的情况,此时 Spring 会从 earlySingletonObjects 中获取到该 Bean 的早期实例,以解决循环依赖的问题。待 Bean 创建完成后,Spring 会将其从 earlySingletonObjects 移除,并放入 singletonObjects 中。

  • singletonFactories:该数据结构是一个哈希表,以 Bean 名称为键,存储用于创建单例 Bean 实例的工厂对象。这是真正打破循环依赖的 Map,缓存的是 ObjectFactory,也就是 Lambda 表达式,在每个 Bean 的生成过程中,经过实例化得到一个原始对象后,都会提前基于原始对象暴露一个 Lambda 表达式,并保存在三级缓存中。这个 Lambda 表达式可能用到,也可能用不到,如果当前 Bean 没有出现循环依赖,那么这个 Lambda 表达式没用,当前 bean 按照自己的生命周期正常执行,执行完后直接把当前 bean 放入 singletonObjects 中。如果当前 bean 在依赖注入时发现出现了循环依赖(当前正在创建的 bean 被其它 bean 依赖了),则从三级缓存中拿到 Lambda 表达式,并执行 Lambda 表达式得到一个对象,把得到的对象放入二级缓存。如果当前 bean 需要 AOP,那么执行 Lambda 表达式得到的是对应的代理对象,如果无需 AOP,则直接得到一个原始对象。

综上所述,singletonObjects 用于缓存已完全初始化的单例 Bean 实例,earlySingletonObjects 用于缓存正在创建中的单例 Bean 实例,singletonFactories 则是用于缓存用于创建单例 Bean 实例的 Factory 对象。这三个数据结构共同协作,确保了单例 Bean 的正确创建和管理。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Yolov8模型训练报错:torch.cuda.OutOfMemoryError

最近在使用自己的数据训练Yolov8模型的时候遇到了很多错误&#xff0c;下面将逐一解答。 问题报错 在训练过程中红字报错&#xff1a;torch.cuda.OutOfMemoryError: CUDA out of memory. 后面还会跟着一大段报错&#xff1a; Tried to allocate XXX MiB (GPU 0; XXX GiB to…

【云原生】使用nginx反向代理后台多服务器

背景 随着业务发展&#xff0c; 用户访问量激增&#xff0c;单台服务器已经无法满足现有的访问压力&#xff0c;研究后需要将后台服务从原来的单台升级为多台服务器&#xff0c;那么原来的访问方式无法满足&#xff0c;所以引入nginx来代理多台服务器&#xff0c;统一请求入口…

OLED透明屏的应用场景有哪些

OLED透明屏在其他领域的应用包括&#xff1a; 商业展示&#xff1a;在商业展示中&#xff0c;OLED透明屏可以作为展示窗口&#xff0c;展示产品信息、广告宣传和品牌形象。通过将透明屏幕安装在展柜、货架或商业窗口中&#xff0c;可以吸引顾客的注意力并提供引人注目的展示效…

不用开会员就能在线编辑、管理及分享各类地理空间数据!

「四维轻云」作为一款地理空间数据云管理平台&#xff0c;具有三维模型、正射影像、激光点云、数字高程模型、人工模型和矢量数据等地理空间数据的在线管理、浏览及分享等功能&#xff0c;致力于为用户提供更加方便、快捷的地理空间数据解决方案。 一、发布、管理超大空间数据…

人大金仓三大兼容:SQL Server迁移无忧

SQL Server在数据库领域一直占据着重要地位。作为一款成熟稳定的关系型数据库管理系统&#xff0c;SQL Server在国内有着广泛的用户群体&#xff0c;医疗、海关、政务等行业的核心业务系统多采用SQL Server数据库。随着政策与市场的双重驱动&#xff0c;信息技术应用创新产业的…

Node.js中的文件系统(file system)模块

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

强大好用的shell:shell的工作原理是什么

Shell的工作原理可以简要概括为以下几个步骤&#xff1a; 1.命令行输入&#xff1a;用户在命令行界面输入命令。 2.命令解析&#xff1a;Shell接收用户的输入&#xff0c;并对命令进行解析。这个过程包括解析命令名、参数、选项等&#xff0c;将其转换成计算机可以理解的形式。…

jsonlite库编写代码示例

r # 导入jsonlite库 library(jsonlite) # 设置主机和端口 proxy_host <- proxy_port <- # 使用httr库创建一个对象 proxy <- create_proxy(proxy_host, proxy_port) # 使用httr库的GET方法下载网页内容 url <- "" response <- GET(url, proxy pr…

将 Figma 轻松转换为 Sketch 的免费方法

最近浏览网站的时候&#xff0c;发现很多人不知道Figma是怎么转Sketch的。众所周知&#xff0c;Figma支持Sketch文件的导入&#xff0c;但不支持Sketch的导出&#xff0c;那么Figma是如何转Sketch的呢&#xff1f;不用担心&#xff0c;建议使用神器即时设计。它是一个可以实现在…

《嵌入式虚拟化技术与应用》:深入浅出阐述嵌入式虚拟机原理,实现“小而能”嵌入式虚拟机!

目录 关于博主前言专家推荐本书适合谁&#xff1f;内容简介书本目录权威作者团队其他 关于博主 &#x1f680;Python爬虫项目实战系列文章&#xff01;&#xff01; ⭐⭐欢迎订阅⭐⭐ 【Python爬虫项目实战一】获取Chatgpt3.5免费接口文末付代码&#xff08;过Authorization认…

高能数造电池3D打印智能制造小试线,开启全固态电池数字化新时代

在科技创新的浪潮中&#xff0c;电池制造领域又迎来了一次突破性的进展。近日&#xff0c;高能数造(西安)技术有限公司重磅推出了其最新电池数字制造装备——全固态电池3D打印智能制造小试线 &#xff0c;这一创新性的技术开启了全固态电池的数字化智造新时代&#xff0c;为全固…

如何存储队列位置信息

实际运行中的系统&#xff0c;难免会遇到重新消费某条消息、跳过一段时间内的消息等情况。这些异常情况的处理&#xff0c;都和Offset有关。本节主要分析Offset的存储位置&#xff0c;以及如何根据需要调整Offset的值。 首先来明确一下Offset的含义&#xff0c;RocketMQ中&…

Linux每日智囊

每日分享三个Linux命令&#xff0c;悄悄培养读者的Linux技能。 info 作用 查看程序、库和系统文档的详细信息。 info命令和man命令都用于查看命令和程序的帮助信息&#xff0c;区别如下&#xff1a; man命令&#xff1a;是最常用的命令之一&#xff0c;用于查看Linux系统上…

Apipost-Helper:IDEA中的类postman工具

今天给大家推荐一款IDEA插件&#xff1a;Apipost-Helper-2.0&#xff0c;写完代码IDEA内一键生成API文档&#xff0c;无需安装、打开任何其他软件&#xff1b;写完代码IDEA内一键调试&#xff0c;无需安装、打开任何其他软件&#xff1b;生成API目录树&#xff0c;双击即可快速…

如何以管理员的身份运行Powershell

大全&#xff01;珍藏 方式一&#xff1a;在Cortana搜索栏中打开带管理员权限的PowerShell Windows 10的任务栏自带了搜索。或者开始菜单选搜索只需在搜索框中输入powershell。 在出来的搜索结果中右击Windows PowerShell&#xff0c;然后选择以管理员方式运行。 随后会弹出UA…

React进阶之路(二)-- 组件通信、组件进阶

文章目录 组件通信组件通信的意义父传子实现props说明子传父实现兄弟组件通信跨组件通信Context通信案例 React组件进阶children属性props校验组件生命周期 组件通信 组件通信的意义 组件是独立且封闭的单元&#xff0c;默认情况下组件只能使用自己的数据&#xff08;state&a…

Go cobra简介

当你需要为你的 Go 项目创建一个强大的命令行工具时&#xff0c;你可能会遇到许多挑战&#xff0c;比如如何定义命令、标志和参数&#xff0c;如何生成详细的帮助文档&#xff0c;如何支持子命令等等。为了解决这些问题&#xff0c;github.com/spf13/cobra 就可以派上用场。 g…

SpringCloud——服务网关——GateWay

1.GateWay是什么&#xff1f; gateway也叫服务网关&#xff0c;SpringCloud GateWay使用的是Webflux中的reactor-netty响应式编程组件&#xff0c;底层使用了Netty通讯框架。 gateway的功能有反向代理、鉴权、流量控制、熔断、日志监控...... 2.为什么不使用Zuul&#xff1f…

智慧社区大屏:连接社区生活的数字桥梁

随着科技的不断发展&#xff0c;智慧社区已经不再只是未来的概念&#xff0c;它已经在我们的眼前悄然崭露头角。智慧社区是一种基于数字技术的社区管理和生活方式&#xff0c;旨在提高社区的安全性、便利性和生活质量。而在这个数字化的社区中&#xff0c;智慧社区大屏起到了连…

MYSQL函数,一篇文章看完!

做程序员的谁会离得开数据库呢&#xff1f;今天就来分享一下我整理的MySQL的常用函数&#xff0c;基本上囊括了平时要用的函数&#xff0c;它们已经陪我走过了不少年头了&#xff0c;风里来雨里去&#xff0c;缝缝补补又几年&#xff0c;希望能帮到你们&#xff01; 如果数据库…