通知:即日起,禁止携带Note 7登机,且不得作为航空货物托运或承运
06-17
1。事情是从一位同事提出的问题开始的。
最近,项目组的一位同事遇到了一个问题,向我征求意见,这立即引起了我的兴趣,因为我是第一次遇到这个问题。平时以为自己对spring的循环依赖问题有比较好的理解,但是遇到这个以及后面的问题之后,刷新了自己的理解。
我们先看一下导致问题的代码片段: 代码语言: javascript copy @Servicepublic class TestService1 { @Autowired private TestService2 testService2; @Async public void test1() { }} 代码语言:javascript copy @Servicepublic class TestService2 { @Autowired private TestService1 testService1; public void test2() { }} 这两段代码中定义了两个Service类:TestService1和TestService2。 TestService2 的实例被注入到 TestService1 中,TestService1 的实例被注入到 TestService2 中。
这形成了一个循环。依靠。
不过,这并不是普通的循环依赖,因为TestService1的test1方法中添加了@Async注解。猜猜程序启动后会发生什么?代码语言: javascriptCopy org.springframework.beans.factory.BeanCurrentlyInCreationException:创建名称为“testService1”的bean时出错:名称为“testService1”的Bean已作为循环引用的一部分注入到其原始版本中的其他bean [testService2]中,但是最终被包裹。
这意味着所述其他 bean 不使用该 bean 的最终版本。这通常是过度渴望类型匹配的结果 - 例如,考虑使用“getBeanNamesOfType”并关闭“allowEagerInit”标志。
报告了错误。 。
。原因是循环依赖的发生。
“这不科学,Spring号称可以解决循环依赖问题,为什么还是会出现这种情况?”如果将上面的代码稍微调整一下: 代码语言:javascript copy @Servicepublic class TestService1 { @Autowired private TestService2 testService2; public void test1( ) { }}去掉TestService1的test1方法上的@Async注解。 TestService1和TestService2都需要注入对方的实例,这也构成了循环依赖。
但重启项目发现运行正常。这是为什么呢?带着这两个问题,让我们开始探索Spring循环依赖的旅程。
2.什么是循环依赖?循环依赖:说白了就是一个或多个对象实例之间存在直接或间接的依赖关系。这种依赖关系就构成了循环调用。
第一种情况:自己依赖自己的直接依赖。第二种情况:两个对象之间的直接依赖。
第三种情况:多个对象之间的间接依赖。前两种情况的直接循环依赖比较直观,也很好。
识别,但第三种间接循环依赖有时不太容易识别,因为业务代码调用层次很深。 3、循环依赖的N种场景。
spring中出现循环依赖的主要场景有:单例setter注入。这种注入方式应该是spring最常用的。
代码如下: 代码语言:javascript copy @Servicepublic class TestService1 { @Autowired private TestService2 testService2; public void test1() { }} 代码语言:javascript copy @Servicepublic class TestService2 { @Autowired private TestService1 testService1; public void test2() { }} 这是一个经典的循环依赖,但是它工作正常,感谢spring的内部机制,我们甚至无法察觉到它有问题,因为spring默默地帮我们解决了。 spring内部有三级缓存: singletonObjects一级缓存,用于保存已经实例化、注入、初始化的bean实例 EarlySingletonObjects二级缓存,用于保存已经实例化的bean实例 singletonFactories 三级缓存,用于保存已经实例化的bean实例bean创建工厂,以便以后的扩展有机会创建代理对象。
下图告诉大家spring是如何解决循环依赖的: 图1 细心的朋友可能会发现,二级缓存在这种场景下并没有什么用处。那么问题来了,为什么要使用二级缓存呢?试想一下,如果出现以下情况,我们该如何处理呢?代码语言: javascript copy @Servicepublic class TestService1 { @Autowired private TestService2 testService2; @Autowired私有TestService3 testService3; public void test1() { }} 代码语言:javascript copy @Servicepublic class TestService2 { @Autowired private TestService1 testService1; public void test2( ) { }}代码语言:javascript copy @Servicepublic class TestService3 { @Autowired private TestService1 testService1; public void test3() { }}TestService1 依赖于 TestService2 和 TestService3,而 TestService2 依赖于 TestService1,TestService3 也依赖于 TestService1。
按照上图的流程,可以将TestService1注入到TestService2中,并从三级缓存中获取TestService1的实例。假设不使用二级缓存,则将TestService1注入TestService3的流程如下:说白了,两次从三级缓存获取的都是ObjectFactory对象,通过它创建的实例对象可能每次都不一样。
这不是问题吗?为了解决这个问题,spring引入了二级缓存。在上图1中,TestService1对象的实例实际上已经被添加到二级缓存中了。
当TestService1注入TestService3时,只需要从二级缓存中获取对象即可。? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 看到图3还有一个问题,为什么我们需要将ObjectFactory对象添加到三级缓存中?直接保存实例对象不就可以了吗?答:不能,因为如果要增强添加到三级缓存的实例对象,直接使用实例对象是行不通的。
spring如何处理这种情况?答案就在AbstractAutowireCapableBeanFactory类的doCreateBean方法的这段代码中:它定义了一个匿名内部类,并通过getEarlyBeanReference方法获取代理对象。其实底层是通过AbstractAutoProxyCreator类的getEarlyBeanReference生成代理对象的。
多实例setter注入是一种偶尔会发生的注入方式,尤其是在多线程场景下。具体代码如下: 代码语言: javascript copy @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Servicepublic class TestService1 { @Autowired private TestService2 testService2; public void test1() { }}代码语言:javascript copy @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)@Servicepublic class TestService2 { @Autowired private TestService1 testService1; public void test2() { }}很多人说这样的话,spring容器启动的时候就会报错。
事实上,这是错误的。我很负责任的告诉你,程序可以正常启动。
为什么?其实答案在AbstractApplicationContext类的refresh方法中就已经告诉了。它将调用 finishBeanFactoryInitialization 方法。
该方法的目的是在spring容器启动时提前初始化一些bean。方法内部调用了preInstantiateSingletons方法。
在红色区域可以清楚地看到:只有非抽象、单例和非延迟加载的类可以提前初始化bean。多实例类是SCOPE_PROTOTYPE类型的类,它不是单例,不会提前初始化bean,因此程序可以正常启动。
如何让他提前初始化bean呢?你只需要定义另一个单例类并将TestService1注入其中即可。代码语言: javascript copy @Servicepublic class TestService3 { @Autowired private TestService1 testService1;} 重新启动程序,执行结果: 代码语言: javascript copy 请求的bean当前正在创建中: 是否存在无法解析的循环引用?果然存在循环依赖。
注意:这个循环依赖问题无法解决,因为它不使用缓存,每次都会生成一个新对象。构造函数注入是一种现在很少使用的注入方式,但我们还是需要了解一下。
看一下下面的代码: 代码语言: javascript copy @Servicepublic class TestService1 { public TestService1(TestService2 testService2) { }} 代码语言: javascript copy @Servicepublic class TestService2 { public TestService2(TestService1 testService1) { }} 运行结果: Code语言: javascript 复制 请求的 bean 当前正在创建中: 是否存在无法解析的循环引用?出现循环依赖,为什么?从图中的流程可以看出,构造函数注入未能添加到三级缓存,并且没有使用缓存,因此无法解决循环依赖问题。单例代理对象setter注入其实是一种比较常见的注入方式。
例如,在使用@Async注解的场景下,会通过AOP自动生成代理对象。我同事的情况也是如此。
代码语言:javascript copy @Servicepublic class TestService1 { @Autowired private TestService2 testService2; @Async public void test1() { }} 代码语言:javascript copy @Servicepublic class TestService2 { @Autowired private TestService1 testService1; public void test2() { }} 从前面了解到,程序启动时会报错,出现循环依赖: 代码语言:javascript Copy org.springframework.beans.factory.BeanCurrentlyInCreationException: Error create bean with name “testService1”:名称为“testService1”的 Bean 已作为循环引用的一部分注入其原始版本中的其他 Bean [testService2],但最终已被包装。这意味着所述其他 bean 不使用该 bean 的最终版本。
这通常是过度渴望类型匹配的结果 - 例如,考虑使用“getBeanNamesOfType”并关闭“allowEagerInit”标志。为什么会出现循环依赖呢?答案就在下图中: 说白了,bean初始化完成之后,还有一步要检查:二级缓存和原始对象是否相等。
由于它对前面的流程无关紧要,所以在前面的流程图中省略了,但这里是重点。我们重点说一下:同事的问题恰好是当他来到这段代码时,发现二级缓存和原来的对象不相等,所以抛出了循环依赖异常。
如果此时将TestService1的名称更改为:TestService6,则其他一切保持不变。代码语言:javascript copy @Servicepublicclass TestService6 { @Autowired private TestService2 testService2; @Async public void test1() { }} 重新启动程序,就会神奇地好起来。
什么?这是为什么呢?这从 spring 的 bean 加载顺序开始。默认情况下,spring按照文件的完整路径递归搜索,按路径+文件名排序,第一个先加载。
所以TestService1先于TestService2加载,而更改文件名后,TestService2先于TestService6加载。为什么TestService2先加载TestService6就没有问题?答案如下图所示:这种情况下,testService6中的二级缓存实际上是空的,不需要从原始对象来判断,所以不会抛出循环依赖。
DependsOn 循环依赖还有一个比较特殊的场景。例如,我们需要在实例化Bean A之前实例化Bean B。
这种情况下,我们可以使用@DependsOn注解。代码语言: javascript copy @DependsOn(value = "testService2")@Servicepublic class TestService1 { @Autowired private TestService2 testService2; public void test1() { }} 代码语言:javascript copy @DependsOn(value = "testService1")@Servicepublic class TestService2 { @Autowired private TestService1 testService1; public void test2() { }}程序启动后,执行结果为: 代码语言:javascript 复制 'testService2' 和 'testService1' 之间的循环依赖关系 在这个例子中,如果 TestService1 和 TestService2 都没有问题如果不添加@DependsOn注解,但添加该注解会导致循环依赖问题。
这是为什么呢?答案就在AbstractBeanFactory类的doGetBean方法的这段代码中:它检查dependsOn的实例是否存在循环依赖,如果存在循环依赖则抛出异常。 4、如何解决循环依赖问题?如果项目中存在循环依赖问题,则说明spring默认无法解决循环依赖。
要看项目的打印日志,看是属于哪种循环依赖。目前包括以下几种情况:通过生成代理对象而产生的循环依赖。
对于这种循环依赖问题有很多解决方案。主要包括:使用@Lazy注解、使用@DependsOn注解进行延迟加载、指定加载顺序、修改文件名、更改循环依赖类等。
加载顺序使用@DependsOn生成的循环依赖关系。对于这样的循环依赖问题,需要找到@DependsOn注解循环依赖的地方,强制其没有循环依赖才能解决问题。
多实例循环依赖等循环依赖问题可以通过将bean改为单实例来解决。使用@Lazy注解可以解决构造函数循环依赖等循环依赖问题。
版权声明:本文内容由互联网用户自发贡献,本站不拥有所有权,不承担相关法律责任。如果发现本站有涉嫌抄袭的内容,欢迎发送邮件 举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。
标签:
相关文章
06-21
06-18
06-18
最新文章
【玩转GPU】ControlNet初学者生存指南
【实战】获取小程序中用户的城市信息(附源码)
包雪雪简单介绍Vue.js:开学
Go进阶:使用Gin框架简单实现服务端渲染
线程池介绍及实际案例分享
JMeter 注释 18 - JMeter 常用配置组件介绍
基于Sentry的大数据权限解决方案
【云+社区年度征文集】GPE监控介绍及使用