Spring 通过 @Lazy 注解解决构造方法循环依赖问题

news2024/11/24 21:00:22

什么是循环依赖?

先定义两个类 Apple、Orange,如下所示:

@Component
public class Apple{

	@Autowired
	private Orange orange;
}

@Component
public class Orange {

	@Autowired
	private Apple apple;
}

像这种在 Apple 里面有一个属性 Orange、Orange 中有一个属性 Apple,你中有我,我中有你,这样可以称之为循环依赖。循环依赖问题不止在 Spring 中有,在 Mybatis 中也有,解决思想基本一样,都需要借助额外的缓存进行实现。

Spring 对于这种属性注入的循环依赖是支持的,不会有任何问题,今天这里探讨一下 Spring 中构造方法的循环依赖问题,Spring 默认是不支持的,但是也提供了方法解决。

构造方法循环依赖

同样把上面 Apple、Orange 两个类改造下,如下所示:

@Component
public class Apple{

	public Apple(Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
	}
}

@Component
public class Orange {

	public Orange(Apple apple) {
		System.out.println("======>调用 Orange 构造方法");
	}
}

测试类如下:


public class TestCircleMain {

	public static void main(String[] args) {

		ApplicationContext context = new AnnotationConfigApplicationContext(CircleConfig.class);

		Orange orange = context.getBean( Orange.class);

		Apple bean = context.getBean(Apple.class);
	}

}

发现直接就抛出了循环依赖异常如下:

在这里插入图片描述

很显然构造方法的循环依赖,Spring 是不太支持的,但是我非要这样使用,怎么解决呢?

加 @Lazy 注解解决构造方法循环依赖

具体怎么解决构造方法循环依赖问题呢?可以通过加 @Lazy 注解,但是也需要注意一些细节(后面我们会分析到),这里先看加 @Lazy 注解之后能够解决构造方法循环依赖的案例,如下所示:

@Component
public class Apple{
    @Lazy
	public Apple(Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
	}
}

@Component
public class Orange {
	
	public Orange(Apple apple) {
		System.out.println("======>调用 Orange 构造方法");
	}
}

或者加载参数上也行,都表示一个意思,就是后面会临时创建出 Orange 的代理对象

@Component
public class Apple{
    
	public Apple(@Lazy Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
	}
}

@Component
public class Orange {
	
	public Orange(Apple apple) {
		System.out.println("======>调用 Orange 构造方法");
	}
}

加上之后测试结果正常输出,如下:

=====> 调用 Apple 构造方法
======>调用 Orange 构造方法

那么为什么加上 @Lazy 注解就能够解决这样一个问题呢?继续往下分析,这里我们主要看加上这个 @Lazy 注解的执行流程是什么样的?

@Lazy 执行流程源码分析

首先第一次过来的是 Apple 类,先从缓存中查询是否有实例化的对象,源码如下:

在这里插入图片描述在这里插入图片描述

第一次过来很显然是没有的,然后就要开始去创建实例,但是创建实例 Spring 会做一个标识,避免重复创建实例,这个标识标识这个 Apple 类正在创建中,当创建成功之后就会删除,此时创建好的实例就会存在缓存中。

记录标识源码如下:注意如果标识 singletonsCurrentlyInCreation 容器中已经存在,那么会直接添加失败,抛出 BeanCurrentlyInCreationException 异常

在这里插入图片描述
在这里插入图片描述

异常信息也是大家非常熟悉的循环依赖问题,源码如下:

在这里插入图片描述

接着要开始创建实例,如下所示:

在这里插入图片描述

会选择出一个合适的构造方法进行实例化,由于我们只有且仅有一个构造方法,所以肯定就用这个唯一的构造方法了,然后就开始进入 autowireConstructor() 属性注入环节

在这里插入图片描述

拿到构造方法中所有的参数,对每个参数一一遍历进行赋值操作,那么就要格外关注这个方法是怎么做的了

在这里插入图片描述

进入 createArgumentArray() 方法内部逻辑,源码如下(核心部分,无关代码省略):

很显然这里是采用 for 循环对构造方法中参数一一赋值,所以构造方法中如果参数过多,性能也会降低许多,这个得注意了

在这里插入图片描述

看到 resolveDependency() 方法一定要有一种意识,就是极大可能要出发 getBean() 操作了,除了代理不会触发,但是也不一定(后面会讲到)

在这里插入图片描述

然后下面这一段代码是加了 @Lazy 注解的关键处理逻辑了(这段逻辑非常非常重要):

1、就是先判断构造方法中(构造方法上,或者参数上)是否标注了 @Lazy 注解,如果标注了就会创建代理对象,不会立即触发 getBean() 操作
2、反之,就是走正常的逻辑,直接调用 getBean() ,但是这样就会直接报异常了,因为 Spring 是不支持构造方法的循环依赖的(还没有来得及把半 Apple 类的半成品放到三级缓存),只有加了 @Lazy 注解临时通过代理方法可以解决构造方法循环依赖

在这里插入图片描述

然后进入 getLazyResolutionProxyIfNecessary() 方法看是怎么判断,和要怎么创建代理对象的,源码如下:

在这里插入图片描述

可以清楚的看到 @Lazy 为什么可以标注在构造方法上和构造方法的入参上面两种方式,可以从下面这段源码中知道答案,如下所示:

在这里插入图片描述

找到了 @Lazy 注解就会通过 buildLazyResolutionProxy() 方法去创建这个入参的代理对象,如下所示:

代理对象就是对原始目标类的一种增强,注意当使用代理对象调用它的方法时会回调到 getTarget() 方法,这个 getTarget() 方法中调用了 doResolveDependency() 方法,这个方法会触发调用 getBean() 流程实例化 bean,要格外注意,后面会演示如何调用到这个方法的。

在这里插入图片描述

代理对象已经创建好了,现在准备通过构造方法反射调用实例化 Apple 类即可,源码如下:

在这里插入图片描述

其中 argsWithDefaultValues 值是 Orange 的一个代理对象,实例化好之后相当于 Apple 已经创建好了,然后放入到三级缓存中,如下所示:

在这里插入图片描述在这里插入图片描述

然后就是删除 singletonsCurrentlyInCreation 标识容器中的标识位(因为已经实例化完成了,所以标识位可以抹除),如下所示:

在这里插入图片描述在这里插入图片描述

最后在把 Apple 实例化好的 bean 从三级缓存中删除,然后移动到一级缓存中,也就是我们经常所说的单例缓冲池中,如下所示:

在这里插入图片描述
在这里插入图片描述

至此,Apple 类实例化 bean 就已经在 Spring 的单例缓冲池中存在了,其他地方如果想要使用直接从这个单例缓冲池中取值即可。

那么当 Orange 类过来实例化的时候,也是先从容器中查找是否有实例化 bean 存在,源码如下:

在这里插入图片描述在这里插入图片描述

然后打标记,源码如下:

在这里插入图片描述
在这里插入图片描述

异常信息也是大家非常熟悉的循环依赖问题,源码如下:

在这里插入图片描述

接着要开始创建实例,如下所示:

在这里插入图片描述

会选择出一个合适的构造方法进行实例化,由于我们只有且仅有一个构造方法,所以肯定就用这个唯一的构造方法了,然后就开始进入 autowireConstructor() 属性注入环节

在这里插入图片描述

拿到构造方法中所有的参数,对每个参数一一遍历进行赋值操作,那么就要格外关注这个方法是怎么做的了

在这里插入图片描述

然后进入代码核心逻辑,此时因为在 Orange 的构造方法中是没有标注 @Lazy 注解的,所以这里不会进入创建代理的逻辑,而知直接进入 doResolveDependency() 逻辑,前面已经提到很多遍历,这个方法很重要,会触发到 getBean() 流程。

在这里插入图片描述

那么 Orange 构造方法中的入参为 Apple,Apple 在第一遍的时候就已经在单例缓冲池中存在了,所以 Apple 在执行 getBean() 流程的时候,直接就会从一级缓存中获取到 Apple 实例化好的对象,赋值给 Orange 构造方法中的 Apple
变量。

在这里插入图片描述

然后表示就是 Orange 通过反射调用构造方法实例化 Orange 实例

在这里插入图片描述

然后后面的流程 Orange 也是要放入到三级缓存中,然后删除标识位,最后将 Orange 实例从三级缓存中删除,移动到一级缓存(单例缓存池)中。

至此 Apple、Orange 两个构造方法的循环依赖就分析完成了,下面是稍微改动一点,继续分析。

使用 @Lazy 注解注意事项(特别小心)

将 Apple、Orange 类稍微变动一下,如下所示:

@Component
public class Apple{

	public Apple(@Lazy Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
		System.out.println("======>orange="+orange);
	}
}

@Component
public class Orange {

	public Orange(Apple apple) {
		System.out.println("======>调用 Orange 构造方法");
	}
}

经过上的分析,Apple 构造方法中 @Lazy 注解修饰的 Orange,会创建一个代理对象来规避入参 orange 调用 getBean() 流程,从而解决循环依赖问题,现在我们在 Apple 构造方法中,直接把 orange 打印出来。

经过测试直接报错,错误如下:

在这里插入图片描述

发现还是发生了循环依赖问题,下面具体分析下是为什么呢?前面分析过的下面都会直接通过简短描述直接带过

1、Apple 类首先会去缓存中查找是否已经实例化 bean,第一次很显然没有

2、开始记录标记位

3、调用 createBeanInstance() 方法实例化对象

4、给 Apple 类构造方法的入参进行属性赋值,会创建代理类,如下所示:

在这里插入图片描述

注意这里面的 getTarget() 方法,下面会回调到这里,现在代码继续往后走,代理类创建好之后就要开始通过反射调用构造方法创建实例了,源码如下:

在这里插入图片描述

注意此时的 argsWithDefaultValues 是 Orange 代理对象,当我们通过反射调用 Apple 的构造方法时,立即回调到 Apple 的构造方法中的逻辑,如下所示:

@Component
public class Apple{

	public Apple(@Lazy Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
		System.out.println("======>orange="+orange);
	}
}

先执行输出语句,打印出"=====> 调用 Apple 构造方法",然后再打印下一句语句时,请注意,orange 是一个代理对象,在 JVM 执行这条输出语句的时候,其实默认调用了 toString() 方法,你要知道在创建代理对象的时候,并没有限定哪个方法增强,而是对整个 Orange 中的方法增强了,所以在你输出 Orange 的时候就会触发代理对象的对 toString() 方法的增强,所以会回调到代理对象中的 intercept() 方法,然后再 intercept() 方法中有调用了 getTarget() 方法,注意哦在创建代理对象的时候,我特意说明要注意 getTarget() 方法,因为现在就要被回调到了,恰好 getTarget() 方法中又会触发 getBean() 流程,所以最终又导致循环依赖问题的产生。

对于 Apple 类构造方法中的入参 Orange, Spring 是通过 cglib 进行代理对象创建的,具体看 CglibAopProxy 类就知道为什么在执行 toString() 方法最终会回调到 getTarget() 方法,这里就截取一段核心代码,如下:
 
在这里插入图片描述

那么怎么解决这个问题呢?

1、在 Apple 类构造方法中不要调用任何代理对象的方法,比如这样使用,如下所示:

@Component
public class Apple{

	private Orange orange;

	public Apple(@Lazy Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
		this.orange = orange;
	}
	public void sop() {
		System.out.println("this.orange = " + this.orange);
	}
}

@Component
public class Orange {
	private Apple apple;
	public Orange(Apple apple) {
		System.out.println("======>调用 Orange 构造方法");
	}
}

我只是在 Apple 构造方法中使用了一下 Orange 代理对象,并没有调用任何 API,所以不会触发代理对象执行增强逻辑。

2、继续在 Apple 构造方法中触发代理对象回调(调用 toString() 等方法),此时会出现循环依赖问题,就是因为方法 toString() 的增强逻辑触发了 Orange 的 getBean() 操作,然后 Orange 实例化时,又触发了 Orange 构造方法中的入参 Apple 类的实例化,此时你要知道 Apple 类还没有实例化完成呢,缓存中压根也还没有,Apple 类现还停留在System.out.println("======>orange="+orange); 输出语句呢

所以说到这里了,我们也可以在 Orange 中加上 @Lazy 注解,如下所示:

@Component
public class Apple{

	public Apple(@Lazy Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
		System.out.println("this.orange = " + this.orange);
	}
	public void sop() {
		System.out.println("this.orange = " + this.orange);
	}
}

@Component
public class Orange {
	private Apple apple;
	public Orange(@Lazy Apple apple) {
		System.out.println("======>调用 Orange 构造方法");
	}
}

当给 Orange 类构造方法中入参 Apple 赋值先给定一个代理对象,避免 Apple 类触发 getBean() 操作,这样 Orange 构造方法的入参就相当于赋上值,那么 Orange 类就完成了实例化,代码回调上层调用处,就是 Apple 类构造方法中的输出语句 System.out.println("======>orange="+orange); 这条输出语句执行完,相当于 Apple 类构造方法也实例化完成,从而没有发生循环依赖问题。

但是如果在将 Apple、Orange 类变动一下,如下所示:

@Component
public class Apple{

	public Apple(@Lazy Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
		System.out.println("this.orange = " + this.orange);
	}
}

@Component
public class Orange {
	private Apple apple;
	public Orange(@Lazy Apple apple) {
		System.out.println("======>调用 Orange 构造方法");
		System.out.println("this.orange = " + this.orange);
	}	
}

这样是绝对没办法解决了,因为相当于 @Lazy 注解没有加上一样,每个构造方法中都会立即触发 getBean() 操作,此时以为缓存中根本还没来得及放入实例化 bean。

以上只是个人对 @Lazy 的理解,仅供参考。

总结

在构造方法循环依赖问题中,通过 @Lazy 注解,只是临时创建一个代理对象来为属性赋值,避免触发二次 getBean() 调用。

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

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

相关文章

k8s 驱逐eviction机制源码分析

原理部分 1. 驱逐概念介绍 kubelet会定期监控node的内存,磁盘,文件系统等资源,当达到指定的阈值后,就会先尝试回收node级别的资源,比如当磁盘资源不足时会删除不同的image,如果仍然在阈值之上就会开始驱逐…

森林图

森林图 以统计指标和统计分析方法为基础,用数值计算绘制出的图形,通常是在平面直角坐标系中,以一条垂直的无效 线(0或者1)为中心,用平衡于x轴的多条线段描述每个组指标的中值和可信区间,最后一…

星空华文通过聆讯:吃《中国好声音》老本 华人文化是股东

雷递网 雷建平 12月9日星空华文控股有限公司(简称:“星空华文”)今日通过聆讯,准备在香港上市。星空华文的前身是上海灿星文化传媒股份有限公司(简称“灿星文化”)。2018年2月,灿星文化向上海证…

Web端H5播放RTSP

Web端H5播放RTSP一、要实现二、基础介绍1.RTSP是什么?2.RTSP播放测试工具VLC3.主流设备常用的RTSP格式三、方案1. webrtc-streamer2. 安装和配置环境3. 运行demo.html4.存疑5.参考了好多~一、要实现 不用萤石云等类似的平台,实现Web端直接显示监控视频。…

LwIP——以太网描述符

目录 什么是以太网DMA描述符 TX DMA描述符成员变量简介 RX DMA描述符成员变量简介 以太网DMA描述符结构 如何追踪描述符 如何创建Tx/Rx描述符 以太网发送和接收数据流程 总结 在移植LwIP之前有必要了解一下以太网DMA描述符的相关知识,ST以太网模块中的接收/发…

【华为OD机试真题 python】箱子之字形摆放【2022 Q4 | 100分】

■ 题目描述 【箱子之字形摆放】 有一批箱子(形式为字符串,设为str), 要求将这批箱子按从上到下以之字形的顺序摆放在宽度为 n 的空地,请输出箱子的摆放位置。 例如:箱子ABCDEFG,空地宽度为3,摆放结果如图: 则输出结果为:AFGBECD 输入描述 输入一行字符串,通过空…

19. 丢弃法(Dropout)

1. 动机 一个好的模型需要对输入数据的扰动鲁棒(不管一张图片加入多少噪音,也能看清这张图片) 使用有噪音的数据等价于Tikhonov正则丢弃法:在层之间加入噪音 输入数据加入随机扰动可以防止过拟合,泛化性更好&#xf…

Python学习基础笔记四十四——模块

1、看一个例子: 创建一个demo.py文件: print(in demo.py)def hello():print(in hello function) 然后我们在另外一个文件中import这个demo文件: import demo# 调用demo.py文件中的hello()函数 demo.hello() 注意,demo后面没有…

Linux系统网络编程——第二十节 多路复用之epoll 模型

目录 epoll相关系统调用 1、epoll_create: 2、epoll_ctl() 3、epoll_wait epoll模型原理 epoll的使用场景 各位好,博主新建了个公众号《自学编程村》,拉到底部即可看到,有情趣可以关注看看哈哈,关注后还可以加博主…

Redis原理篇

目录Redis数据结构动态字符串SDS整数集合Intset键值型Dict压缩链表 ZipList快速链表QuickList跳表SkipList对象RedisObjectRedis网络模型Redis通信协议-RESP协议Redis内存回收过期key处理内存淘汰策略Redis数据结构 动态字符串SDS Redis构建了一种新的字符串结构,…

从C#5.0说起:再次总结C#异步调用方法发展史

本篇继续介绍WaitHandler类及其子类 Mutex,ManualResetEvent,AutoResetEvent的用法。 .NET中线程同步的方式多的让人看了眼花缭乱,究竟该怎么去理解呢? 其实,我们抛开.NET环境看线程同步,无非是执行两种操…

软件测试基础知识总览【纯知识,建议收藏慢慢学】

1. 软件测试定义 首先要明确测试的定义,所谓测试,就是以检验产品是否满足需求为目标。 而软件测试,自然是为了发现软件(产品)的缺陷而运行软件(产品) 比较标准的软件测试的定义是:在规定的条件下对程序进行操作,以发现错误,对软件质量进行评估。 IEE…

算法总结,不断更新

文章目录摩尔投票法DFS算法BFS算法题源来自于力扣网 摩尔投票法 适用场景 如何在选票无序的情况下,选出获胜者。 例题: 找出数组中,出现次数超过总数一半的数字(出现次数 > n/2)。 输入:[1,1,3,2,4,6,…

10000字吐血总结+24张图带你彻底弄懂线程池

大家好。今天跟大家聊一聊无论是在工作中常用还是在面试中常问的线程池,通过画图的方式来彻底弄懂线程池的工作原理,以及在实际项目中该如何自定义适合业务的线程池。 一、什么是线程池 线程池其实是一种池化的技术的实现,池化技术的核心思…

MVC|JAVA|SSM框架计算机硬件评测交流平台的开发和实现

收藏点赞不迷路 关注作者有好处 文末获取源码 项目编号:BS-PT-070 一,项目简介 计算机硬件在社会上有很多广泛的发烧友,他们急需一个发布专业硬件测评数据的平台并进行交流互动的社区。本次开发实现的计算机硬件交流平台就是作为一个专业的…

Android序列化之Parcel源码分析(2)

文章目录1.Parcel.java2.Parcelable和Parcel的关系3.Parcel写入数据源码分析3.1.java层Parcel创建3.2.native层Parcel创建3.3写入IBinder接口标识符3.4写入String数据4.Parcel读取数据源码分析4.1获取IBinder接口标识符4.2读取String数据1.Parcel.java Android可以通过Parcel进…

【OpenCV学习】第15课:处理卷积边缘问题

仅自学做笔记用,后续有错误会更改 (卷积的概念可以看看第14课) 理论 卷积边缘问题:从下图最右方的结果可以看出,卷积操作之后, 剩余的绿色像素部分, 我们是没有处理到的 那么如何处理这个问题呢&#xf…

论文3:查找文献在指定期刊的引用格式

文章目录说明:1.谷歌学术搜索(可以用一些国内的镜像),并点击被引用次数2.勾选在引用文章中搜索,并在搜索框搜索指定期刊的关键词3.这里指定期刊是RAL即IEEE Robotics and Automation Letters4.任意点开上图中的一篇文章…

支付宝当面付网站对接支付教程

有很多人会开支付宝当面付但是不会配置它老会出现一下情况 第二种情况如下: 如果遇到以上情况可以按照我的步骤就可以解决 详细步骤: 一、应用APPID获取方法 1.打开网站:https://openhome.alipay.com/platform/developerIndex.htm&#x…

Canal配置多个实例以及将Mysql指定表的binlog导入指定的Kafka的Topic

Canal配置多个实例以及将Mysql指定表的binlog导入指定的Kafka的Topic 进入Canal的conf目录 复制模板配置文件 cp -r example/ Ordercp -r example/ Orderdetail修改canal.propertieswenjain vim canal.properties修改内容如下,指定输出模式为kafka canal.serverM…