【图文详解】Spring是如何解决循环依赖的?

Spring是如何解决循环依赖的呢?

很多小伙伴在面试时都被问到过这个问题,刷到过这个题的同学马上就能回答出来:“利用三级缓存”。面试官接着追问:“哪三级缓存呢?用两级行不行呢?” 这时候如果没有深入研究过Spring源码的话,估计就要根据自己的理解开始胡诌了。为了帮大家彻底理清这个问题,吊打面试官,我特意出这篇文章来帮你梳理一下Spring的解决思路。

解决循环依赖的前提是创建的Bean是都单例的,本章的所有Bean默认都采用单例模式。

我假定屏幕前的你已经熟练使用Spring并且知道IOC的原理了。

Spring 的 BeanFactory 在创建 Bean 时会经历三个重要步骤

  1. 实例化bean (createBeanInstance)
  2. 给bean的属性赋值 (populateBean)
  3. 初始化bean (initializeBean)

每个Bean在被创建时都需要按部就班走完这三步后,才能诞生一个可用的Bean。

先看一个没有循环依赖的例子

假如一个Bean(AService)持有另一个Bean(BService) 的引用,两个Bean是怎么个创建步骤呢?

AService与BService的类定义如下

@Service
public class AService {@AutowiredBService bService;public void seeA() {bService.eat();}}
@Service
public class BService {public void eat() {System.out.println("B======eat");}}

AService持有一个BService的引用,如果在创建AService这个Bean时,BService还没创建的话,那么BService也会被创建。流程如下图

可以看到这两个Bean的创建流程很明了。

Bean创建完成后会放到一个单例池中,这个单例池就是三级缓存中的“一级缓存”。

Spring用一个Map来充当这个单例池,key是beanName,value为Bean对象。

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

构建一个循环依赖(不考虑AOP代理逻辑)

假如BService中又引用了AService,两个Bean各自依赖对方,形成了循环依赖,如果还沿用上面的流程来创建Bean的话,就会陷入到一个无止境的循环中。

现在AService和BService的代码如下

@Service
public class AService {@AutowiredBService bService;public void seeA() {bService.eat();}public void eat() {System.out.println("A======eat");}}
@Service
public class BService {@AutowiredAService aService;public void seeB() {aService.eat();}public void eat() {System.out.println("B======eat");}}

怎么打破这个循环呢?

Spring是这样想的:创建一个Bean需要经过三个步骤,在第一步实例化Bean这个操作完成后,这个Bean对象在内存中就已经存在了,就能拿到该对象的引用了,但是这个对象还是个半成品,之后的第二、三步都是在丰富这个对象,第三步走完后这个对象才是个成品对象。所以第一步走完后,就可以找个地方将该Bean的引用存放起来,因为还是个半成品,所以不能放到“一级缓存”中, Spring就为它创建了一个新地方,这个地方就是“二级缓存”,Spring用一个Map来表示二级缓存,key是beanName,value为Bean半成品对象。

private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

拿上面的例子来说,AService在被实例化后,Spring就将AService的引用放到二级缓存中,然后第二步填充属性时触发了BService的Bean创建流程,BService在第二步填充属性时发现又依赖了AService,于是尝试获取AService的Bean实例,一级缓存中没有,二级缓存中有,于是就将二级缓存中的“AService引用”拿过来填充属性值。这样循环就被打破了。

来画图说明一下

我用一个2×2的表格,来代表一级和二级缓存

两个Bean的创建流程如下图,我将两级缓存的内容放在时序图的左侧,你可以清楚的看到,两级缓存的内容是在什么时候发生变化的。

可以看到,如果只是简简单单的两个Bean相互依赖的话,引入一个存放半成品Bean的二级缓存就可以打破这个循环依赖了。

构建一个循环依赖(考虑AOP代理逻辑)

但是如果产生循环依赖的Bean同时又被AOP代理,需要产生代理对象的话,两级缓存就显得有点不够用了。

默认情况下,Spring将AOP产生代理对象的逻辑放在了“初始化Bean”这一步里,并且是放在了执行init方法之后,通过一段简略代码来看一下初始化Bean的流程

protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {// 如果Bean实现了Aware接口,需要执行对应Aware的逻辑invokeAwareMethods(beanName, bean);Object wrappedBean = bean;// 执行Bean初始化前需要执行的前置操作wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, // 执行init方法,初始化beaninvokeInitMethods(beanName, wrappedBean, mbd);// 执行Bean初始化后需要执行的后置操作,代理对象就是在这一步生成wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, return wrappedBean;
}

所以,Bean创建三步曲中,第一步实例化的Bean可能不是最终的Bean,如果这个Bean有代理逻辑的话,在第三步初始化Bean后,就产生了一个代理Bean,最终放入单例池中的也是这个代理Bean。

如何非要还用两级代理的话,能实现吗?

我觉得也能实现,但是要将AOP的代理逻辑往前提,提到第一步中,在实例化Bean之后,马上就执行代理逻辑,并且放入二级缓存里的也是代理后的对象。这样的话当循环依赖产生时,从二级缓存中也能直接获取到最终的代理对象。

但是这个做法貌似违背了Spring的设计初衷,Spring是将AOP的代理逻辑放在偏后的位置的,前面也说了,是放在执行了Bean对象的init方法后的。也就是说Spring的设计是:先将一个完整的Bean对象创建好,然后再看需不需要进行AOP代理。

那么Spring最终是怎么做的呢?Spring没有修改原来的设计,而是又加了一级缓存,也就是“三级缓存”,这第三级缓存就是用来处理AOP代理逻辑的。Spring允许当某个Bean产生循环依赖时提前执行其AOP代理逻辑,但是当没有循环依赖产生时,仍然是按部就班的先创建好Bean后,再尝试进行AOP代理。

Spring用一个Map来表示三级缓存,key是beanName,value为ObjectFactory对象。

private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

这个ObjectFactory是何方神圣呢?看代码

它是一个接口,并且只有一个方法 getObject() ,这个接口上有一个@FunctionalInterface注解,这个注解用于指示一个接口是一个函数式接口。函数式接口是指仅包含一个抽象方法的接口,这样的接口可以用作 Lambda 表达式或方法引用的目标。而Spring也是采用了Lambda表达式的方式来使用这个接口。

来看两段源码

Spring往三级缓存中加对象的方法是这样定义的

调用 addSingletonFactory 这个方法的方式是这样的

可以看到调用处是用了一个Lambda表达式来表示 ObjectFactory 这个接口的具体实现。而getEarlyBeanReference这个方法则是代表了 ObjectFactory 接口中的 getObject() 方法。三级缓存就是调用这个方法来拿到一个半成品Bean的。

继续我们的代码和流程演示

这里我为上面的AService和BService加了一个AOP代理类

@Aspect
@Component
@Slf4j
public class MyAdvice {@Pointcut("execution(* com.sundries.circular_dependency.*Service.*(..))")public void pointCut(){}@Around("pointCut()")public Object aroundDeal(ProceedingJoinPoint joinPoint) throws Throwable {String methodName = joinPoint.getSignature().getName();StopWatch stopWatch = new StopWatch();stopWatch.start();Object result = joinPoint.proceed();stopWatch.stop();long totalTimeMillis = stopWatch.getTotalTimeMillis();log.info("{} 方法执行耗时: {}ms", methodName, totalTimeMillis);return result;}}

这个类里定义的pointCut就是指定了要切AService合BService两个类的所有方法,代理逻辑很简单,就是加了一个方法执行耗时的打印。

下面来看看使用三级缓存时,两个Bean的创建流程图。注意!这个流程已经是Spring的真实逻辑了。

Spring就是这样设计了一个三级缓存来解决了Bean的循环依赖问题。解决这个问题的核心思想就是提前将一个半成品的Bean引用给暴露出来,提供给其他Bean来作为属性值。

文末了,如果这里再问你一下:Spring是如何解决循环依赖的呢? 相信你已经能随口说出其中设计原理了👍🏻

下面是我根据Spring源码梳理这个流程时的过程图,感兴趣的话可以放大看下

不存在AOP代理时的情况

存在AOP代理时的情况

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

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

相关文章

Vs2022+QT+Opencv 一些需要注意的地方

要在vs2022创建QT项目&#xff0c;先要安装一个插件Qt Visual Studio Tools&#xff0c;根据个人经验选择LEGACY Qt Visual Studio Tools好一些&#xff0c;看以下内容之前建议先在vs2022中配置好opencv&#xff0c;配置方式建议以属性表的形式保存在硬盘上。 设置QT路径 打开v…

清华计算几何-算法LowBound和ConvexHull(凸包)-GrahamScan

算法复杂度最低界限LowBound 算法求解复杂度是否存在一个最低界限&#xff0c;有时候想尽一切办法优化一个算法&#xff0c;去优化其复杂度&#xff0c;比如 清华计算几何-ConvexHull(凸包)-求极点InTriangle/ToLeft Test-CSDN博客 清华计算几何-ConvexHull(凸包)-求极边_计…

DeFi革命:揭秘去中心化金融的核心技术与实操指南

目录 DeFi&#xff08;去中心化金融&#xff09;综述 基本特点 第一&#xff0c;DeFi 是无许可的金融 第二&#xff0c;DeFi 是无门槛的金融 第三&#xff0c;DeFi 是无人驾驶的金融 典型商业模式 闪电贷 MakerDAO 面临的挑战 DeFi技术要点 椭圆曲线签名 EIP-712:…

模拟依赖关系和 AI 是Vue.js测试的下一个前沿领域

Vue.js 是一个流行的 JavaScript 框架&#xff0c;因此&#xff0c;确保其组件按预期工作至关重要&#xff1a;有效&#xff0c;更重要的是&#xff0c;可靠。模拟依赖项是最有效的测试方法之一&#xff0c;我们将在本文中发现。 模拟依赖项的必要性 模拟依赖项是一种对测试施加…

个人定制化形象生成,FaceChain最新模型部署

FaceChain是阿里巴巴达摩院推出的一个开源的人物写真和个人数字形象的AI生成框架。 FaceChain利用了Stable Diffusion模型的文生图功能&#xff0c;并结合人像风格化LoRA模型训练及人脸相关感知理解模型&#xff0c;将输入的图片进行训练后推理输出生成个人写真图像。 FaceCh…

Live555源码阅读笔记:哈希表的实现(C++)

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…

算法日记day 20(中序后序遍历序列构造二叉树|最大、合并、搜索二叉树)

一、中序后序序列构造二叉树 题目&#xff1a; 给定两个整数数组 inorder 和 postorder &#xff0c;其中 inorder 是二叉树的中序遍历&#xff0c; postorder 是同一棵树的后序遍历&#xff0c;请你构造并返回这颗 二叉树 。 示例 1: 输入&#xff1a;inorder [9,3,15,20,…

使用SpringEvent解决WebUploader大文件上传解耦问题

目录 前言 一、SpringEvent涉及的相关组件 1、 事件&#xff08;Event&#xff09; 2、事件监听器 3、事件发布器 二、WebUploader大文件处理的相关事件分析 1、事件发布的时机 2、事件发布的代码 三、事件监听器及实际的业务处理 1、文件上传处理枚举 2、文件上传监…

Python+selenium web自动化测试知识点合集2

选择元素 对于百度搜索页面&#xff0c;如果我们想自动化输入“selenium”&#xff0c;怎么做呢&#xff1f; 这就是在网页中&#xff0c;操控界面元素。 web界面自动化&#xff0c;要操控元素&#xff0c;首先需要 选择 界面元素 &#xff0c;或者说 定位 界面元素 就是 先…

C++客户端Qt开发——界面优化(QSS)

1.QSS 如果通过QSS设置的样式和通过C代码设置的样式冲突&#xff0c;则QSS优先级更高 ①基本语法 选择器{属性名&#xff1a;属性值; } 例如&#xff1a; QPushButton {color: red; } 1>指定控件设置样式 #include "widget.h" #include "ui_widget.h&qu…

qt--做一个拷贝文件器

一、项目要求 使用线程完善文件拷贝器的操作 主窗口不能假死主窗口进度条必须能动改写文件大小的单位&#xff08;自适应&#xff09; 1TB1024GB 1GB1024MB 1MB1024KB 1KB1024字节 二、所需技术 1.QFileDialog 文件对话框 QFileDialog也继承了QDialog类&#xff0c;直接使用静态…

Redis缓存数据库进阶——Redis与分布式锁(6)

分布式锁简介 1. 什么是分布式锁 分布式锁是一种在分布式系统环境下&#xff0c;通过多个节点对共享资源进行访问控制的一种同步机制。它的主要目的是防止多个节点同时操作同一份数据&#xff0c;从而避免数据的不一致性。 线程锁&#xff1a; 也被称为互斥锁&#xff08;Mu…

Robot Operating System——内部审查(Introspection)Service

大纲 introspection_service检验Parameter值和类型修改内部审查&#xff08;Introspection&#xff09;功能的状态完整代码 introspection_client完整代码 测试参考资料 在ROS 2&#xff08;Robot Operating System 2&#xff09;中&#xff0c;内部审查&#xff08;Introspect…

【中项】系统集成项目管理工程师-第7章 软硬件系统集成-7.3软件集成

前言&#xff1a;系统集成项目管理工程师专业&#xff0c;现分享一些教材知识点。觉得文章还不错的喜欢点赞收藏的同时帮忙点点关注。 软考同样是国家人社部和工信部组织的国家级考试&#xff0c;全称为“全国计算机与软件专业技术资格&#xff08;水平&#xff09;考试”&…

python 裁剪图片

情况&#xff1a; 有时候看视频&#xff0c;看到一个漂亮的妹子&#xff0c;按下 Alt PrintScreen 进行截图之后&#xff0c;会把整个屏幕都截图。 需要适当剪裁一下。 每次打开 PS &#xff0c; 也太慢了。 所以写个代码&#xff0c; 快速处理。 效果对比&#xff1a; 原始…

【2025留学】德国留学真的很难毕业吗?为什么大家不来德国留学?

大家好&#xff01;我是德国Viviane&#xff0c;一句话讲自己的背景&#xff1a;本科211&#xff0c;硕士在德国读的电子信息工程。 之前网上一句热梗&#xff1a;“德国留学三年将是你人生五年中最难忘的七年。”确实&#xff0c;德国大学的宽进严出机制&#xff0c;延毕、休…

OOP知识整合----集合

目录 一、定义 1、集合: ( 不限制长度&#xff0c;存多少是多少) 2、集合框架: 二、List集合中常用的方法 1、Boolean add(Object o) 2、void add(int index,Object o) 3、Boolean remove(Object o) 4、Object remove(int index) 5、int size() 6、Boolean conta…

Code Effective学习笔记--第8章防御式编程

这一章聚焦如何通过断言和Java的异常处理机制这些防御式编程的方法来提高程序的健壮性和安全性&#xff0c;这是防御式编程技术的方面。但是健壮性和安全性到了一定的程度其实是矛盾的&#xff0c;健壮性意味着对于任何的输入&#xff0c;程序都不会终止而且都能给出返回&#…

Tftp服务器环境搭建

1、什么是Tftp TFTP&#xff08;Trivial File Transfer Protocol&#xff0c;简单文件传输协议&#xff09;是一种基于UDP&#xff08;User Datagram Protocol&#xff09;的文件传输协议&#xff0c;它被设计为一个非常简单的文件传输机制&#xff0c;特别适用于那些对复杂性有…

make2exe:自动集成测试

模板Makefile&#xff0c;生成多个C/C模块的集成测试程序。