【图文详解】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…

基于深度学习的文本自监督学习

基于深度学习的文本自监督学习&#xff08;Self-Supervised Learning, SSL&#xff09;是一种利用未标注文本数据通过预任务进行训练&#xff0c;以学习有用的文本表示的方法。自监督学习在自然语言处理&#xff08;NLP&#xff09;领域中取得了显著的成果&#xff0c;如BERT、…

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、文件上传监…

pthread多线程

pthread库是linux系统的多线程库。上次写完longjmp和多线程的例子。就想用系统的真线程库改写一下读写线程的例子来和大家分享。真线程库应该比自己写的功能更完善&#xff0c;代码也更容易写吧&#xff01;… … 说到这里。我不知道接下来说什么了… …。也许是自己对多线程库…

ConstraintLayout实现原理分析

ConstraintLayout 是 Android 支持库中提供的一个非常强大的布局管理器&#xff0c;它允许开发者创建复杂的界面布局&#xff0c;并提供了比传统布局更多的灵活性。在 ConstraintLayout 中&#xff0c;视图之间可以通过约束来定义其位置关系&#xff0c;从而使得布局更加动态和…

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

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

OD C卷 - 电脑病毒感染

电脑病毒感染 &#xff08;200&#xff09; 一个局域网内有n台电脑&#xff0c;编号为 1 -> n&#xff0c;电脑之间病毒感染时间用 t 表示&#xff1b;现在网络内已有一台电脑被病毒感染&#xff0c;求其感染所有其他电脑最少的时间&#xff0c;若最后有电脑不会被感染&…

URL重写

目录 步骤1 规则语法 Nginx URL重写规则语法 Apache URL重写规则语法 步骤2 规则配置 Apache URL重写规则配置 启用mod_rewrite模块 配置.htaccess文件 编写重写规则 测试重写规则 Nginx URL重写规则配置 配置server或location块 测试重写规则 步骤1 规则语法 Ngin…

如何使用Redis实现一个缓存策略

使用Redis实现一个缓存策略&#xff0c;主要涉及到数据的存储、读取、更新以及失效处理等方面。下面我将详细介绍如何使用Redis来设计和实现一个基本的缓存策略。 1. 确定缓存的数据结构和键命名规则 首先&#xff0c;你需要决定使用Redis中的哪种数据结构来存储缓存数据&…

解决在from pyhdf.SD import SD,SDC时No module named “hdfext“

文章内容仅用于自己知识学习和分享&#xff0c;如有侵权&#xff0c;还请联系并删除 &#xff1a;&#xff09; 1. 错误原因&#xff1a; 参考了多个博主的解决办法&#xff0c;结合自己的尝试&#xff0c;发现造成这个问题的主要原因是numpy版本太高 2. 解决方法 方法1&…

Day.31 | 1049.最后一块石头的重量II 494.目标和 474.一和零

1049.最后一块石头的重量II 要点&#xff1a;思路与分割等和子集很类似&#xff0c;把总数分成和最接近的两堆&#xff0c;然后用01背包的套路解答 class Solution { public:int lastStoneWeightII(vector<int>& stones) {int sum 0;for (int i : stones)sum i;i…

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

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

Unity Editor免登录启动 无需UnityHub

Unity Editor免登录启动项目无需UnityHub&#xff0c;命令行启动项目。需要开发Unity项目&#xff0c;就必须使用 Unity Hub来管理你的项目&#xff0c;还必须要申请一个免费许可&#xff0c;确实有点麻烦&#xff0c;官方已经提供了相关命令行&#xff0c;来直接使用Unity Edi…

qt--做一个拷贝文件器

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