Spring是如何解决循环依赖的?
1、案发情况
@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}
}
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}
这两段代码中定义了两个Service类:TestService1
和TestService2
,在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
注解去掉,TestService1
和TestService2
都需要注入对方的实例,同样构成了循环依赖。
但是重新启动项目,发现它能够正常运行。这又是为什么?
带着这两个问题,让我们一起开始spring循环依赖的探秘之旅。
2.什么是循环依赖?
循环依赖:说白是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用。
3.循环依赖的N种场景
spring中出现循环依赖主要有以下场景:
单例的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是如何解决循环依赖的:
图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的流程如图:
图2
TestService1注入到TestService3又需要从第三级缓存中获取实例,而第三级缓存里保存的并非真正的实例对象,而是ObjectFactory
对象。说白了,两次从三级缓存中获取都是ObjectFactory
对象,而通过它创建的实例对象每次可能都不一样的。
这样不是有问题?
为了解决这个问题,spring引入的第二级缓存。上面图1其实TestService1对象的实例已经被添加到第二级缓存中了,而在TestService1注入到TestService3时,只用从第二级缓存中获取该对象即可。
图3
还有个问题,第三级缓存中为什么要添加 ObjectFactory
对象,直接保存实例对象不行吗?
答:不行,因为假如你想对添加到三级缓存中的实例对象进行增强,直接用实例对象是行不通的。
针对这种场景spring是怎么做的呢?
答案就在 AbstractAutowireCapableBeanFactory
类 doCreateBean
方法的这段代码中:
它定义了一个匿名内部类,通过 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
publicclass 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默认无法解决的循环依赖,要看项目的打印日志,属于哪种循环依赖。目前包含下面几种情况
生成代理对象产生的循环依赖
这类循环依赖问题解决方法很多,主要有:
- 使用
@Lazy
注解,延迟加载 - 使用
@DependsOn
注解,指定加载先后关系 - 修改文件名称,改变循环依赖类的加载顺序
使用@DependsOn产生的循环依赖
这类循环依赖问题要找到@DependsOn
注解循环依赖的地方,迫使它不循环依赖就可以解决问题。
多例循环依赖
这类循环依赖问题可以通过把bean改成单例的解决。
构造器循环依赖
这类循环依赖问题可以通过使用@Lazy
注解解决。
Spring是如何解决循环依赖的?相关推荐
- Spring 是如何解决循环依赖的?
1.由同事抛的一个问题开始 最近项目组的一个同事遇到了一个问题,问我的意见,一下子引起的我的兴趣,因为这个问题我也是第一次遇到.平时自认为对spring循环依赖问题还是比较了解的,直到遇到这个和后面的 ...
- 互相引用 spring_听说你还不知道Spring是如何解决循环依赖问题的?
作者:Vt 前言 Spring如何解决的循环依赖,是近两年流行起来的一道Java面试题. 其实笔者本人对这类框架源码题还是持一定的怀疑态度的. 如果笔者作为面试官,可能会问一些诸如"如果注入 ...
- Spring是如何解决循环依赖的
在关于Spring的面试中,我们经常会被问到一个问题,就是Spring是如何解决循环依赖的问题的.这个问题算是关于Spring的一个高频面试题,因为如果不刻意研读,相信即使读过源码,面试者也不一定能够 ...
- 万字长文带你吃透Spring是怎样解决循环依赖的
在Spring框架中,处理循环依赖一直是一个备受关注的话题.这是因为Spring源代码中为了解决循环依赖问题,进行了大量的处理和优化.同时,循环依赖也是Spring高级面试中的必考问题,回答得好可以成 ...
- Spring三级缓存解决循环依赖问题详解
spring三级缓存解决循环依赖问题详解 前言 这段时间阅读了spring IOC部分的源码.在学习过程中,自己有遇到过很多很问题,在上网查阅资料的时候,发现很难找到一份比较全面的解答.现在自己刚学习 ...
- Spring的getBean解决循环依赖
Spring是如何解决循环依赖的? 通过三级缓存提前暴露对象解决的. 三级缓存存放了哪些对象信息? 一级缓存存放的是完整对象. 二级缓存存放的是那些属性还没赋值的对象. 三级缓存存放的是ObjectF ...
- Spring IOC 如何解决循环依赖?
前言 假设对象A.B 之间相互依赖,Spring IOC是如何解决A.B两个对象的实例化的?答案是三级缓存. 三级缓存 SpringIOC 通过三级缓存来解决循环依赖问题,三级缓存指的是三个Map: ...
- 框架源码专题:Spring是如何解决循环依赖的?
文章目录 1.什么是循环依赖? 2.解决循环依赖思路 3. 使用了三级缓存还有什么问题?怎么解决的? 4. 手写伪代码解决缓存依赖 5. 二级缓存能否解决循环依赖,三级缓存存在的意义 6. Sprin ...
- Spring三级缓存解决循环依赖
1. 前言 循环依赖:就是N个类循环(嵌套)引用. 通俗的讲就是N个Bean互相引用对方,最终形成闭环.用一副经典的图示可以表示成这样(A.B.C都代表对象,虚线代表引用关系): 其实可以N=1,也就 ...
最新文章
- java工程中的.classpathaaaaaaaaaaaaaaaa转载
- 单片机红外通信c语言,基于C语言的计算机与多单片机红外无线串口通信的实现.doc...
- Old ST-LINK firmware detected.do you want to upgrade it?已解决,stlink升级
- torch.nn.Embedding()的固定化
- C++11特性补充笔记1
- 人生苦短,请用 Chrome!
- centos7上mycat安装_Mysql+Mycat实现数据库主从同步与读写分离
- 超级详细的iptables介绍
- 修炼系列(八),你真的会写注释吗
- 日记侠:知识付费创富没赶上,看别人赚钱你急不急?
- aspose实现word,excel在线预览
- 共克时艰|链下思考系列之一区块链能做点什么
- 中国移动光宽带 光猫 超级用户管理员账号密码
- 11.0_[Java 继承]-继承/重写/抽象类/抽象方法/ final 修饰符
- Lytro发布光场影片,让你感受真正的沉浸式视频体验
- platform设备驱动简介
- C#--浮点数取小数点后两位和保留两位
- SDWAN的技术流派:纯软型、资源型、设备型、广域网优化型
- Ruby on rails 实战圣经:Part 1: 入门导览-Ruby on Rails 简介
- 网站推广的29种常用方法