Spring 循环依赖原理及解决方案

一、什么是循环依赖

循环依赖指的是一个实例或多个实例存在相互依赖的关系(类之间循环嵌套引用)。
举例:

@Component
public class AService {// A中注入了B@Autowiredprivate BService bService;
}@Component
public class BService {// B中也注入了A@Autowiredprivate AService aService;
}

上述例子中 AService 依赖于 BService,BService 也依赖了 AService,这就是两个对象之间互相依赖。循环依赖还包括 身依赖、多个实例之间相互依赖(A依赖于B,B依赖于C,C又依赖于A)。

在普通Java环境下正常运行上面的代码调用 AService 对象并不会出现问题,也就是说普通对象就算出现循环依赖也不会存在问题,因为可以将属性设置为null,那么为什么被 Spring 容器管理后的对象有循环依赖的情况会出现问题呢?

二、Spring 可以解决哪些循环依赖问题

在Spring 中,只有同时满足以下两点才能解决循环依赖的问题:

  1. 依赖的 Bean 必须都是单例
  2. 依赖注入的方式,必须不全是构造器注入,且 beanName字母序在前的不能是构造器注入

1.为什么必须是单例

如果两个Bean都是原型模式的话,那么创建A1需要创建一个B1,创建B1的时候要创建一个A2,创建A2又要创建一个B,创建B2又要创建一个A3,创建A3又要创建—个B3…
就又卡BUG了。因为原型模式都需要创建新的对象,不能用以前的对象。

如果是单例的话,创建A需要创建B,而创建的B需要的是之前的那个A。也是基于这点,Spring 就能操作操作了。具体做法就是:先创建A,此时的A是不完整的(没有注入B),用个map保存这个不完整的A,再创建B,B需要A,所以从那个map得到不完整"的A,此时的B就完整了,然后A就可以注入B,然后A就完整了,B也完整了,且它们是相互依赖的。

2.为什么不能全是构造器注入

在 Spring 中创建 Bean 分三步(详细见Spring Bean 的生命周期):

  1. 实例化, createBeanInstance,就是 new了个对象
  2. 属性注入, populateBean,就是 set 一些属性值
  3. 初始化, initializeBean,执行一些 aware 接口中的方法,init-Method,AOP代理等

明确了上面这三点,再结合我上面说的“不完整的”,我们来理一下。
如果全是构造器注入,比如 A(B b),那表明在new A 的时候,就需要得到 B,此时需要new B,但是 B 也是要在构造的时候注入 A,即B(A a),这时候 B 需要在一个 map 中找到不完整的 A,发现找不到。为什么找不到? 因为 A 还没 new 完呢,所以找不到完整的A,因此如果全是构造器注入的话,那么 Spring 无法处理循环依赖。而如果 A 是通过 set 注入 B 的,那么 B 在属性注入时就能注入不完整的 A 了(因为 A 虽然没有进行属性注入,但是已经实例化了),因此 B 就能完整创建 Bean,B 创建完,A 也能进行属性注入了,因此就解决了循环依赖。

在这里插入图片描述

三、Spring 如何解决循环依赖问题

1、三级缓存

  1. 一级缓存,singletonObjects,存储所有已创建完毕的单例 Bean (完整的 Bean)
  2. 二级缓存,earlySingletonObjects,存储所有仅完成实例化,但还未进行属性注入和初始化的Bean。
  3. 三级缓存,singletonFactories,存储能建立这个 Bean 的一个工厂,通过工厂能获取这个 Bean,延迟化 Bean 的生成,工厂生成的 Bean 会塞入二级缓存。

2、三个缓存如何配合解决循环依赖问题?

关键就是提前暴露未完全创建完毕的Bean。

  1. 首先,获取单例 Bean 的时候会通过 BeanName 先去一级缓存查找完整的 Bean,如果找到则直接返回,否则进行步骤2。
  2. 看对应的 Bean 是否在创建中,如果不在直接返回找不到,如果是,则会去二级缓存查找 Bean,如果找到则返回,否则进行步骤3。
  3. 三级缓存通过 BeanName 查找到对应的工厂,如果存在工厂则通过工厂创建 Bean,并且放置到二级缓存中。
  4. 如果三个缓存都没找到,则返回null。

从上面的步骤我们可以得知,如果查询发现 Bean 还未创建,到第二步就直接返回 null,不会继续查二级和三级缓存。

返回 null 之后,说明这个 Bean 还未创建,这个时候会标记这个 Bean 正在创建中,然后再调用 createBean 来创建 Bean,而实际创建是调用方法 doCreateBean。
doCreateBean 这个方法就会执行上面我们说的三步骤:实例化、属性注入、初始化。

在实例化 Bean 之后,会往三级缓存塞入一个工厂,而调用这个工厂的 getObject 方法,就能得到这个 Bean。

3、举例说明

举例:A、B 两个类循环依赖,Spring 结合三级缓存这样解决:

  1. 一开始创造 A 时候查询—级缓存(里面存成品),发现没找到则看二级缓存是否在创建中(有没有半成品)。都没有则需要创建 A 的 bean,调用的是 createBean,过程分别是实例化、属性注入、初始化。
  2. A 实例化之后往三级缓存加入一个 A 的 getObject 方法,这个就是解决循环依赖的关键。
  3. 到了属性注入,因为 A 依赖 B 因此需要创建 B 。同样的路线 B 也要 createBean 。不一样的也是解决循环依赖的一环:到了属性注入,查询二级缓存的 A 为创建中,则调用三级缓存的工厂 getObject 创建一个半成品的 A,放入到二级缓存中,并完成 B 的第二步属性注入。
  4. 后面初始化 initializeBean,完成 B 的 Bean 创建,放到—级缓存。
    回到 A 刚刚卡在属性注入,现在可以成功注入 B,然后初始化,A 也完成了 Bean 创建。(注︰成品和半成品就是没有注入所需的依赖)

为什么 A 中注入的 B 是构造器注入就不能解决?

再次结合实例解释一下:
如果 A 是构造器注入,B 是 set 注入。则说明 A 需要 B 的时刻提前了,在实例化new A(B b) 的时候就需要 B。此时 A 没有往三级缓存放 getObject,因此到了创建依赖 B 的时候,无法获取 A 的 getObject 工厂方法,只能继续 new A,造成循环依赖的死循环。

4、三级缓存有必要吗?

在上述的步骤中,三级缓存的作用就是直接返回实例化的 A,去掉三级缓存,直接从二级缓存中取出 A 也可以。所以理论上通过二级缓存就能解决循环依赖。那么三级缓存的作用是什么?

三级缓存的作用体现在:在 A 被代理的情况下,可以控制:1)当没有循环依赖时,会按照 Bean 的生命周期来创建 Bean(即在初始化后的后处理器中才会进行代理)2)当有循环依赖时,提前将代理对象返回给 B。

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

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

相关文章

Redis 的标准使用规范之数据类型使用规范

数据类型使用规范 提示:以下是本篇文章正文内容,可供参考 (1)、字符文本(STRING) 【建议】选型为简易文本类缓存 :比如普通的字符、文本、Json 结构 ,通常能起到加速读写和降低后端压力的作用。 【建议】…

数据库系统 第46节 数据库版本控制

数据库版本控制是确保数据库架构和数据模型随着时间的推移而正确演进的重要实践。它允许开发团队跟踪数据库的变更历史,回滚到以前的版本,以及在不同环境(如开发、测试和生产环境)之间同步数据库结构。以下是两种主要的数据库版本…

ActiveMQ 的网络连接及消息回流机制

1、ActiveMQ 的网络连接 activeMQ 如果要实现扩展性和高可用性的要求的话,就需要用用到网络连接模式。 NetworkConnector:主要用来配置 broker 与 broker 之间的通信连接 如上图所示,MQ 服务器1 和MQ 服务器2 通过 NewworkConnector 相连&…

Leetcode Hot 100刷题记录 -Day12(轮转数组)

轮转数组 问题描述: 给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4]解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向…

一台笔记本电脑的硬件都有哪些以及对应的功能

一台笔记本电脑的硬件通常包括多个关键组件,这些组件共同协作,确保电脑的正常运行。以下是笔记本电脑的主要硬件及其功能: 1. 中央处理器(CPU) 功能:CPU 是电脑的“大脑”,负责处理所有的计算…

CocosCreator面试真题详解

最近有位同学面试Cocos Creator,我们把面试时问道的真题列举出来,并配上参考答案。 问题1: 你们公司项目时如何做战斗系统的? 面试官你好,做战斗系统和架构的时候,我们一般把代码逻辑分成3层来设计,同时把数据独立出…

Linux业务系统将/home目录删除并将空间扩给根目录

原有目录空间分配如下: [roothisdb ~]# df -h Filesystem Size Used Avail Use% Mounted on /dev/mapper/cl-root 21G 10G 11G 50% / devtmpfs 905M 0 905M 0% /dev tmpfs 920M 177M 744M 20% /dev/shm tm…

千益畅行,共享旅游卡,有哪些优势和特点?

1、同行人数灵活: 与一般旅游卡相比,千益畅行旅游卡对同行人数的限制更宽松,单卡支持 2 至 6 人同时出行,能满足小团体、家庭等多人出行需求,为多人共同出游提供了便利和优惠。 2、服务模式多样: 1&#xf…

前端月中总结

1、领导一拍脑门想要一个内部聊天软件 --基于open IM二次开发 背景 前段时间不是接手了一个内部办公软件的项目嘛,这个项目已经写了三四年了,一代代的前端融合了不知到多少种代码风格,再加上最初搭这个项目架子的人不知道咋想的&#xff0c…

操作系统 ---- 进程的概念、组成、特征

学习路线: 一、进程的概念及组成 我们通过一个例子来说明进程的概念以及程序和进程的区别。 我们在Windows操作系统中打开任务管理器,在任务管理器当中能看到此时系统当中运行的进程有哪些,如下图所示: 此时&#…

H5漂流瓶社交系统源码

一个非常有创意的H5漂流瓶社交系统源码,带完整前端h5和后台管理系统。 环境:Nginx 1.20.1-MySQL 5.6.50-PHP-7.3 代码下载

一家电子信息企业终止,前五大客户收入占比超九成,募资合理性存疑

兴天科技终止原因如下:首先,兴天科技前五大客户收入占比约超九成,客户集中度较高且高于行业平均水平,其中近期来自第一大客户收入占比超七成,单一客户依赖程度进一步上升;其次,兴天科技除第一大…

Spring boot启动过程详解

程序设计的所有原则和方法论都是追求一件事——简单——功能简单、依赖简单、修改简单、理解简单。因为只有简单才好用,简单才好维护。因此,不应该以评论艺术品的眼光来评价程序设计是否优秀,程序设计的艺术不在于有多复杂多深沉,…

ffmpeg的安装和使用教程及案例

FFmpeg的安装与使用教程 一、FFmpeg简介 FFmpeg是一个开源的、跨平台的音视频处理工具,可以用来转换、播放、录制、流化音视频数据,以及进行多种音视频编码和解码。 二、安装FFmpeg 1. Windows系统安装 下载预编译的二进制文件:从FFmpeg…

starrocks结合同步和异步物化视图建立数据湖和数据仓库

StarRocks 是一个高性能的开源 MPP(大规模并行处理)数据库,主要用于 OLAP(联机分析处理)场景。它支持各种数据操作,包括数据仓库中的大规模查询处理。同步和异步物化视图是 StarRocks 中的数据处理特性&…

云原生学习交流

欢迎加入技术交流群&#xff0c;与阿里、腾讯、字节、华为等运维大佬面对面交流和互相学习。 请扫码备注 加群&#xff0c;<城市>-<公司/自由职业>-<昵称>&#xff0c;如“加群&#xff0c;广州-阿里-行则”&#xff1a;

LabVIEW灵活集成与调试的方法

在LabVIEW开发中&#xff0c;为了构建一个既便于调试又能灵活集成到主VI中的控制VI&#xff0c;开发者需要采用适当的编程方式和架构。常见的选择包括模块化设计、状态机架构以及事件驱动编程。这些方法有助于简化调试过程、提高系统的稳定性&#xff0c;并确保代码的重用性和可…

目标检测-YOLOv4

YOLOv4介绍 YOLOv4 是 YOLO 系列的第四个版本&#xff0c;继承了 YOLOv3 的高效性&#xff0c;并通过大量优化和改进&#xff0c;在目标检测任务中实现了更高的精度和速度。相比 YOLOv3&#xff0c;YOLOv4 在框架设计、特征提取、训练策略等方面进行了全面升级。它在保持实时检…

分布式事务Seata原理及其项目使用

0.Seata官方文档 1.Seata概念及原理 Seata是什么 Seata 是一款开源的分布式事务解决方案&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式&#xff0c;为用户打造一站式的分布式解决方案。 Seata主要由三个重要组…

SpringBoot整合分页助手

目录 1 导入依赖 2 测试使用 关于分页有些数据 默认访问首页,即默认当前页是 pageNum 1 数据有总条数, total select count(*) 页面大小/每页展示多少条数据, pageSize 10 总页数 , pageCount total / pageSize (需要注意除不尽情况) -- total共7条 select count(*) f…