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

1.由同事抛的一个问题开始

最近项目组的一个同事遇到了一个问题,问我的意见,一下子引起的我的兴趣,因为这个问题我也是第一次遇到。平时自认为对spring循环依赖问题还是比较了解的,直到遇到这个和后面的几个问题后,重新刷新了我的认识。

我们先看看当时出问题的代码片段:

@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}
}@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}

这两段代码中定义了两个Service类:TestService1TestService2,在TestService1中注入了TestService2的实例,同时在TestService2中注入了TestService1的实例,这里构成了循环依赖

只不过,这不是普通的循环依赖,因为TestService1的test1方法上加了一个@Async注解。

大家猜猜程序启动后运行结果会怎样?

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1':Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

报错了。。。原因是出现了循环依赖。

「不科学呀,spring不是号称能解决循环依赖问题吗,怎么还会出现?」

如果把上面的代码稍微调整一下:

@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}
}

把TestService1的test1方法上的@Async注解去掉,TestService1TestService2都需要注入对方的实例,同样构成了循环依赖。

但是重新启动项目,发现它能够正常运行。这又是为什么?

带着这两个问题,让我们一起开始spring循环依赖的探秘之旅。

2.什么是循环依赖?

循环依赖:说白是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用。

第一种情况:自己依赖自己的直接依赖

img

第二种情况:两个对象之间的直接依赖

img

第三种情况:多个对象之间的间接依赖

img

前面两种情况的直接循环依赖比较直观,非常好识别,但是第三种间接循环依赖的情况有时候因为业务代码调用层级很深,不容易识别出来。

3.循环依赖的N种场景

spring中出现循环依赖主要有以下场景:

img

单例的setter注入

这种注入方式应该是spring用的最多的,代码如下:

@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}
}@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}

这是一个经典的循环依赖,但是它能正常运行,得益于spring的内部机制,让我们根本无法感知它有问题,因为spring默默帮我们解决了。

spring内部有三级缓存:

  • singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例
  • earlySingletonObjects 二级缓存,用于保存实例化完成的bean实例
  • singletonFactories 三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。

下面用一张图告诉你,spring是如何解决循环依赖的:

img

图1

细心的朋友可能会发现在这种场景中第二级缓存作用不大。

那么问题来了,为什么要用第二级缓存呢?

试想一下,如果出现以下这种情况,我们要如何处理?

@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;@Autowiredprivate TestService3 testService3;public void test1() {}
}@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}@Service
public class TestService3 {@Autowiredprivate TestService1 testService1;public void test3() {}
}

TestService1依赖于TestService2和TestService3,而TestService2依赖于TestService1,同时TestService3也依赖于TestService1。

按照上图的流程可以把TestService1注入到TestService2,并且TestService1的实例是从第三级缓存中获取的。

假设不用第二级缓存,TestService1注入到TestService3的流程如图:

img

图2

TestService1注入到TestService3又需要从第三级缓存中获取实例,而第三级缓存里保存的并非真正的实例对象,而是ObjectFactory对象。说白了,两次从三级缓存中获取都是ObjectFactory对象,而通过它创建的实例对象每次可能都不一样的。

这样不是有问题?

为了解决这个问题,spring引入的第二级缓存。上面图1其实TestService1对象的实例已经被添加到第二级缓存中了,而在TestService1注入到TestService3时,只用从第二级缓存中获取该对象即可。

在这里插入图片描述

图3

还有个问题,第三级缓存中为什么要添加ObjectFactory对象,直接保存实例对象不行吗?

答:不行,因为假如你想对添加到三级缓存中的实例对象进行增强,直接用实例对象是行不通的。

针对这种场景spring是怎么做的呢?

答案就在AbstractAutowireCapableBeanFactorydoCreateBean方法的这段代码中:

在这里插入图片描述

它定义了一个匿名内部类,通过getEarlyBeanReference方法获取代理对象,其实底层是通过AbstractAutoProxyCreator类的getEarlyBeanReference生成代理对象。

多例的setter注入

这种注入方法偶然会有,特别是在多线程的场景下,具体代码如下:

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}
}@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}

很多人说这种情况spring容器启动会报错,其实是不对的,我非常负责任的告诉你程序能够正常启动。

为什么呢?

其实在AbstractApplicationContext类的refresh方法中告诉了我们答案,它会调用finishBeanFactoryInitialization方法,该方法的作用是为了spring容器启动的时候提前初始化一些bean。该方法的内部又调用了preInstantiateSingletons方法

在这里插入图片描述

标红的地方明显能够看出:非抽象、单例 并且非懒加载的类才能被提前初始bean。

而多例即SCOPE_PROTOTYPE类型的类,非单例,不会被提前初始化bean,所以程序能够正常启动。

如何让他提前初始化bean呢?

只需要再定义一个单例的类,在它里面注入TestService1

@Service
public class TestService3 {@Autowiredprivate TestService1 testService1;
}

重新启动程序,执行结果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

果然出现了循环依赖。

注意:这种循环依赖问题是无法解决的,因为它没有用缓存,每次都会生成一个新对象。

构造器注入

这种注入方式现在其实用的已经非常少了,但是我们还是有必要了解一下,看看如下代码:

@Service
public class TestService1 {public TestService1(TestService2 testService2) {}
}@Service
public class TestService2 {public TestService2(TestService1 testService1) {}
}

运行结果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

出现了循环依赖,为什么呢?

在这里插入图片描述

从图中的流程看出构造器注入没能添加到三级缓存,也没有使用缓存,所以也无法解决循环依赖问题。

单例的代理对象setter注入

这种注入方式其实也比较常用,比如平时使用:@Async注解的场景,会通过AOP自动生成代理对象。

我那位同事的问题也是这种情况。

@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}
}@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}

从前面得知程序启动会报错,出现了循环依赖:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean 
with name 'testService1': Bean with name 'testService1' has been injected into other 
beans [testService2] in its raw version as part of a circular reference, but has 
eventually been wrapped. This means that said other beans do not use the final version 
of the bean. This is often the result of over-eager type matching - consider using 
'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

为什么会循环依赖呢?

答案就在下面这张图中:

在这里插入图片描述

说白了,bean初始化完成之后,后面还有一步去检查:第二级缓存 和 原始对象 是否相等。由于它对前面流程来说无关紧要,所以前面的流程图中省略了,但是在这里是关键点,我们重点说说:

在这里插入图片描述

那位同事的问题正好是走到这段代码,发现第二级缓存 和 原始对象不相等,所以抛出了循环依赖的异常。

如果这时候把TestService1改个名字,改成:TestService6,其他的都不变。

@Service
public class TestService6 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}
}

再重新启动一下程序,神奇般的好了。

what? 这又是为什么?

这就要从spring的bean加载顺序说起了,默认情况下,spring是按照文件完整路径递归查找的,按路径+文件名排序,排在前面的先加载。所以TestService1比TestService2先加载,而改了文件名称之后,TestService2比TestService6先加载。

为什么TestService2比TestService6先加载就没问题呢?

答案在下面这张图中:

在这里插入图片描述

这种情况testService6中其实第二级缓存是空的,不需要跟原始对象判断,所以不会抛出循环依赖。

DependsOn循环依赖

还有一种有些特殊的场景,比如我们需要在实例化Bean A之前,先实例化Bean B,这个时候就可以使用@DependsOn注解。

@DependsOn(value = "testService2")
@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}
}@DependsOn(value = "testService1")
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}

程序启动之后,执行结果:

Circular depends-on relationship between 'testService2' and 'testService1'

这个例子中本来如果TestService1和TestService2都没有加@DependsOn注解是没问题的,反而加了这个注解会出现循环依赖问题。

这又是为什么?

答案在AbstractBeanFactory类的doGetBean方法的这段代码中:

在这里插入图片描述

它会检查dependsOn的实例有没有循环依赖,如果有循环依赖则抛异常。

4.出现循环依赖如何解决?

项目中如果出现循环依赖问题,说明是spring默认无法解决的循环依赖,要看项目的打印日志,属于哪种循环依赖。目前包含下面几种情况:

在这里插入图片描述

生成代理对象产生的循环依赖

这类循环依赖问题解决方法很多,主要有:

  1. 使用@Lazy注解,延迟加载
  2. 使用@DependsOn注解,指定加载先后关系
  3. 修改文件名称,改变循环依赖类的加载顺序

使用@DependsOn产生的循环依赖

这类循环依赖问题要找到@DependsOn注解循环依赖的地方,迫使它不循环依赖就可以解决问题。

多例循环依赖

这类循环依赖问题可以通过把bean改成单例的解决。

构造器循环依赖

这类循环依赖问题可以通过使用@Lazy注解解决。

转载:https://www.zhihu.com/question/438247718/answer/1730527725(看评论)

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

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

相关文章

java按钮触发另一个页面_前端跨页面通信,你知道哪些方法?

戳蓝字「前端技术优选」关注我们哦! 引言在浏览器中,我们可以同时打开多个Tab页,每个Tab页可以粗略理解为一个“独立”的运行环境,即使是全局对象也不会在多个Tab间共享。然而有些时候,我们希望能在这些“独立”的Tab页…

【Java用法】java 8两个List集合取交集、并集、差集、去重并集

在业务的开发过程中会经常用到两个List集合相互取值的情况&#xff0c;于是记录在此&#xff0c;方便后续使用哦~~~ public class ListTest {public static void main(String[] args) {ArrayList<String> listA CollectionUtil.toList("a", "b", &…

jsonp react 获取返回值_Django+React全栈开发:文章列表

React现在我们有了一个属于文章的API&#xff0c;可以添加、修改、删除、查看文章&#xff0c;但是对于我们的网站来说&#xff0c;还需要一个用户界面才行。现在开始探索一下ReactJS吧。经常听到有前端三大框架Angular、React、Vue的说法&#xff0c;不过React官网对自己的介绍…

24个经典的MySQL索引问题,你都遇到过哪些?

1、什么是索引&#xff1f; 索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分)&#xff0c;它们包含着对数据表里所有记录的引用指针。 索引是一种数据结构。数据库索引&#xff0c;是数据库管理系统中一个排序的数据结构&#xff0c;以协助快速查询、更新数…

java 3 4_Java-3/4_树.md at master · yrcDream/Java-3 · GitHub

树二叉树二叉树具有唯一根节点二叉树每个节点最多有两个孩子&#xff0c;最多有一个父亲二叉树具有天然递归结构二叉树不一定是 “满” 的&#xff1a;一个节点也是二叉树、空节点也是二叉树二叉搜索树(BST)BST 的基本功能public class BST> {private Node root;private int…

python模块导入_python模块导入

不同的执行方式&#xff1a; 从IDE中执行&#xff0c;python程序由IDE设置环境决定。 从系统中执行&#xff0c;python程序由环境变量中的系统变量path决定&#xff0c;从上往下选择。 模块导入顺序&#xff1a; 系统包优先级最高 > 同目录 > sys.path&#xff0c;之所以…

再也不怕SVN冲突:轻松解决SVN冲突

什么时候容易出现冲突&#xff1f; 多个人同时修改了同个文件中的同一行代码 无法进行对比的二进制文件&#xff0c;比如图片等 如何解决冲突&#xff1f; 如上图&#xff0c;test_conflict.py文件发生了冲突&#xff0c;并且多出了几个文件&#xff0c;其中.mine是我本地修…

手机型号大全_2020值得入手的三款手机。每个优秀,选择哪一个?励志故事名言视频...

如今&#xff0c;手机等数码产品更新很快。各种新的技能&#xff0c;让用户真正体验到科技的力量&#xff0c;它可以被描述为“具有多种功能的一个装置。”然而&#xff0c;这么多车型&#xff0c;难免有些人不知道如何选择。当4G和5G手机**的对峙&#xff0c;很多朋友也问小中…

AspectJ

Aspectj与Spring AOP比较 XML配置方式 <aop:aspect>&#xff1a; 定义切面, 包括通知和切点. 是一般的bean//定义切面 public class SleepHelperAspect{public void beforeSleep(){System.out.println("睡觉前要脱衣服&#xff01;");}public void afterSleep…

aixs1 生成java代码_通过axis1.4 来生成java客户端代码

1.首先下载axis-1.4所有的jar包&#xff0c;2.我是直接打开cmd&#xff0c;进入到该jar包的目录下&#xff0c;3.直接运行命令(运行这个命令之前要确定java的环境变量都已配置好)&#xff1a;java -Djava.ext.dirs${lib的目录} org.apache.axis.wsdl.WSDL2Java -o${代码输出路径…

windows分屏_windows内到底藏了多少好东西?

恭喜!点开这篇文章&#xff0c;你将解锁 WIN10 系统内那些不为人知的高效的冷知识&#xff01;相信所有的职场人都会搜索过这样的问题&#xff1a;有哪些高效的办公神器&#xff1f;在之前的文章中&#xff0c;我分享过很多高效神器&#xff0c;如果你感兴趣的话&#xff0c;点…

@Aspect中@Pointcut 12种用法

本文主要内容&#xff1a;掌握Pointcut的12种用法。 Aop相关阅读 阅读本文之前&#xff0c;需要先掌握下面3篇文章内容&#xff0c;不然会比较吃力。 Spring系列第15篇&#xff1a;代理详解&#xff08;java动态代理&CGLIB代理)Spring系列第30篇&#xff1a;jdk动态代理…

asp.net接受表单验证格式后再提交数据_看滴普科技大前端如何玩转el-form-renderer 表单渲染器1.14.0

DEEPEXI 大前端常人道&#xff0c;一入开发深似海&#xff0c;技术学习无止境。在新技术层出不穷的前端开发领域&#xff0c;有一群身怀绝技的开发&#xff0c;他们在钻研前沿技术的同时&#xff0c;也不忘分享他们的成果&#xff0c;回馈社区。下面&#xff0c;就由小水滴带大…

测试用例设计方法_黑盒测试——测试用例设计方法

黑盒测试也称为功能测试或数据驱动测试。通过软件的外部表现来发现其缺陷和错误。在测试时&#xff0c;把被测程序视为一个不能打开的盒子&#xff0c;在完全不考虑程序内部逻辑结构和内部特性的情况下进行。它是在已知产品所应具有的功能前提下&#xff0c;通过测试来检测每个…

SpringAop @Pointcut(“@annotation“)\@Aspect练习

切面记录日志 切面类 Slf4j Aspect Component public class AspectForFeign {Pointcut("execution(public * com.keke.remote..*Feign.*(..))")public void pointcut() {}Around("pointcut()")public Object around(ProceedingJoinPoint joinPoint) thro…

Mybatis缓存机制详解与实例分析

前言&#xff1a; 本篇文章主要讲解Mybatis缓存机制的知识。该专栏比较适合刚入坑Java的小白以及准备秋招的大佬阅读。 如果文章有什么需要改进的地方欢迎大佬提出&#xff0c;对大佬有帮助希望可以支持下哦~ 小威在此先感谢各位小伙伴儿了&#x1f601; 以下正文开始 Mybat…

delphi语言转为汇编语言_每天5分钟,轻松建立技术图谱 编程语言黑历史

阿T课堂开播啦&#xff01;这里只有干货干锅&#xff0c;没有水坑没有套路&#xff01;计算机编程语言的发展&#xff0c;也是随着计算机本身发展而发展。人类不断的提高科技的同时&#xff0c;也必须使工具的使用越来越简化&#xff0c;从而提高整个社会效率&#xff0c;这其中…

水系图一般在哪里找得到_进展 | 水系钠离子电池研究取得重要进展

水系钠离子电池兼具钠资源储量丰富和水系电解液本质安全的双重优势被视为一种理想的大规模静态储能技术。此前&#xff0c;我们针对这水系钠离子电池体系做了一些探索(Nature Communications 2015, 6, 6401&#xff1b;Advanced Energy Materials 2015, 5, 1501005&#xff1b;…

@Around简单使用示例——SpringAOP增强处理

Around的作用 既可以在目标方法之前织入增强动作&#xff0c;也可以在执行目标方法之后织入增强动作&#xff1b;可以决定目标方法在什么时候执行&#xff0c;如何执行&#xff0c;甚至可以完全阻止目标目标方法的执行&#xff1b;可以改变执行目标方法的参数值&#xff0c;也…

python numpy逆_Python使用numpy计算矩阵特征值、特征向量与逆矩阵

原标题&#xff1a;Python使用numpy计算矩阵特征值、特征向量与逆矩阵 Python扩展库numpy.linalg的eig()函数可以用来计算矩阵的特征值与特征向量&#xff0c;而numpy.linalg.inv()函数用来计算可逆矩阵的逆矩阵。 >>> importnumpy as np >>> x np.matrix([…