spring源码阅读笔记09:循环依赖
前面的文章一直在研究Spring创建Bean的整个过程,创建一个bean是一个非常复杂的过程,而其中最难以理解的就是对循环依赖的处理,本文就来研究一下spring是如何处理循环依赖的。
1. 什么是循环依赖
不管之前是否研究过循环依赖,这里先对这个知识做一点回顾。
循环依赖就是循环引用,就是两个或者多个bean相互之间的持有对方,比如A引用B,B引用C,C引用A,则它们最终反映为一个环,参考下图:
了解了什么是循环依赖之后,我们知道这是一种不可避免会出现的情况,那作为Bean容器的Spring又是怎么处理这一问题呢?我们接着往下看。
2. Spring如何处理循环依赖
Spring容器循环依赖包括构造器循环依赖和setter循环依赖,那Spring容器又是如何解决循环依赖的呢?我们来测试一下,首先我们来定义循环引用类:
public class TestA{
private TestB testB; public void a(){
testB.b();
} public TestB getTestB(){
return testB;
} public void setTestB(TestB testB){
this.testB = testB;
}
} public class TestB{
private TestC testC; public void b(){
testC.c();
} public TestC getTestC(){
return testC;
} public void setTestC(TestC testC){
this.testC = testC;
}
} public class TestC{
private TestA testA; public void c(){
testA.a();
} public TestA getTestA(){
return testA;
} public void setTestA(TestA testA){
this.testA = testA;
}
}
在Spring中将循环依赖的处理分成了3种情况:
2.1 构造器循环依赖处理
这表示通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。
比如在创建TestA类时,构造器需要TestB类,那么将去创建TestB,在创建TestB类时又发现需要TestC类,则又去创建TestC,最终在创建TestC时发现又需要TestA,从而形成一个环,没办法创建。
Spring容器将每一个正在创建的bean标识符放在一个“当前创建bean池”中,bean标识符在创建过程中将一直保持在这个池中,因此如果在创建bean的过程中发现自己已经在“当前创建bean池”里时,则抛出BeanCurrentlyInCreationException异常表示出现了循环依赖;而对于创建完毕的bean将从“当前创建bean池”中清除掉,这个“当前创建bean池”实际上是一个ConcurrentHashMap,即DefaultSingletonBeanRegistry中的singletonsCurrentlyInCreation。
我们通过一个直观的测试用例来进行分析:
xml配置如下:
<bean id = "testA" class = "xxx.xxx">
<constructor-arg index = "0" ref = "testB"/>
</bean>
<bean id = "testB" class = "xxx.xxx">
<constructor-arg index = "0" ref = "testC"/>
</bean>
<bean id = "testC" class = "xxx.xxx">
<constructor-arg index = "0" ref = "testA"/>
</bean>
创建测试用例:
public static void main(String[] args) {
try{
new ClassPathXmlApplicationContext("beans.xml");
}catch (Exception e){
e.printStackTrace();
}
}
这个执行过程中会抛出异常BeanCurrentlyInCreationException,通过debug可以快速找到异常抛出的位置在getSingleton()方法中的beforeSingletonCreation():
protected void beforeSingletonCreation(String beanName) {
if (!this.inCreationCheckExclusions.containsKey(beanName) &&
this.singletonsCurrentlyInCreation.put(beanName, Boolean.TRUE) != null) {
throw new BeanCurrentlyInCreationException(beanName);
}
}
由此可知,Spring在对构造器循环依赖的处理策略上是选择了直接抛异常,而且对循环依赖的判断是发生在加载单例时调用ObjectFactory的getObject()方法实例化bean之前。
2.2 setter循环依赖处理
这个表示通过setter注入方式构成的循环依赖。对于setter注入造成的循环依赖Spring是通过提前暴露刚完成构造器注入但还未完成其他步骤(如setter注入)的bean来完成的,而且只能解决单例作用域的bean循环代码,我们这里来详细分析一下Spring是如何处理的。
关于这部分的处理逻辑,在AbstractAutowireCapableBeanFactory的doCreateBean()方法中有一段代码,如下所示:
// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isDebugEnabled()) {
logger.debug("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
// 为避免后期循环依赖,可以在bean初始化完成前将创建实例的ObjectFactory加入工厂
addSingletonFactory(beanName, new ObjectFactory<Object>() {
public Object getObject() throws BeansException {
// 对bean再一次依赖引用,主要应用SmartInstantiationAwareBeanPostProcessor,
// 其中我们熟知的AOP就是在这里将advice动态织入bean中,若没有则直接返回bean,不做任何处理
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}
这段代码不是很复杂,但是如果是一开始看这段代码的时候不太容易理解其作用,因为仅仅从函数中去理解是很难弄懂其中的含义,这里需要从全局的角度去思考Spring的依赖解决办法才能更好理解。
- earlySingletonExposure:从字面的意思理解就是是否提早曝光单例
- mbd.isSingleton():是否是单例
- this.allowCircularReference:是否允许循环依赖,在AbstractRefreshableApplicationContext中提供了设置函数,可以通过硬编码的方式进行设置或者可以通过自定义命名空间进行配置,硬编码的方式代码如下:
ClassPathXmlApplicationContext bf = new ClassPathXmlApplicationContext("aspectTest.xml");
bf.setAllowBeanDefinitionOverriding(false);
- isSingletonCurrentlyInCreation(beanName):该bean是否在创建中。在Spring中,会有一个专门的属性(类DefaultSingletonBeanRegistry中的singletonsCurrentlyInCreation)来记录bean的加载状态,在bean开始创建前会将beanName记录在属性中,在bean创建结束后会将beanName从属性中移除。我们跟随代码一路走来或许对这个属性的记录并没有多少印象,不经会拍脑门问这个状态是在哪里记录的呢?不同scope的记录位置并不一样,我们以singleton为例,在singleton下记录属性的函数是在DefaultSingletonBeanRegistry类的getSingleton(String beanName,ObjectFactory singletonFactory)函数中的beforeSingletonCreation(beanName)和afterSingletonCreation(beanName)中,在这两段函数中分别通过this.singlesCurrentlyInCreation.add(beanName)与this.singlesCurrentlyInCreation.remove(beanName)来进行状态的记录与移除。
经过上面的分析可以知道变量earlySingletonExposure为是否是单例、是否允许循环依赖、是否对应的bean正在创建这三个条件的综合。当这3个条件都满足时会执行addSingletonFactory操作,那么加入SingletonFactory的作用又是什么呢?
这里还是用一个最简单的AB循环依赖为例,类A中含有属性类B,而类B中又会含有属性类A,那么初始化beanA的过程如下图所示:
上图展示了创建beanA的流程,图中我们看到,在创建A的时候首先会记录类A所对应的beanName,并将beanA的创建工厂加入缓存中,而在对A的属性填充也就是调用populate()方法的时候又会再一次的对B进行递归创建。同样的,因为在B中同样存在A属性,因此在实例化B时的populate()方法中又会再次地初始化A,也就是图形的最后,调用getBean(A)。关键就是在这里,在这个getBean()函数中并不是直接去实例化A,而是先去检测缓存中是否有已经创建好的对应bean,或者是否有已经创建好的ObjectFactory,而此时对于A的ObjectFactory我们早已创建好了,所以便不会再去向后执行,而是直接调用ObjectFactory去获取A。
到这里基本可以理清Spring处理循环依赖的解决办法,这里再从代码层面总结一下:
在创建bean的过程中,实例化bean结束之后,属性注入之前,有一段这样的代码(代码位置为AbstractAutowireCapableBeanFactory类中的doCreateBean()方法中bean实例化之后):
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isDebugEnabled()) {
logger.debug("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, new ObjectFactory<Object>() {
public Object getObject() throws BeansException {
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}
这段代码前面也说过,主要做的事情是在addSingletonFactory()方法中,即在必要的时候将创建bean的ObjectFactory添加到缓存中。再结合前面的例子来看,在第一次创建beanA时,这里是会将ObjectFactory加入到singletonFactories中,当创建beanB时,在对beanB的属性注入时又会调用getBean()去获取beanA,同样是前面说到过,会先去缓存获取beanA,这时候是可以获取到刚才放到缓存中的ObjectFactory的,这时候就会把实例化好但是还未完成属性注入的beanA找出来注入到beanB中去,这样就解决了循环依赖的问题,需要结合下面的代码细品一下。
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException { final String beanName = transformedBeanName(name);
Object bean; // Eagerly check singleton cache for manually registered singletons.
Object sharedInstance = getSingleton(beanName); ...
} protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
2.3 prototype范围的依赖处理
对于"prototype"作用域的bean,Spring容器并不会对其进行缓存,因此无法提前暴露一个创建中的bean,所以也是通过抛出异常的方式来处理循环依赖,这里仍然是用一个demo来测试一下代码是在哪抛的异常。
配置文件:
<bean id = "testA" class = "xxx" scope = "prototype">
<property name = "testB" ref = "testB"/>
</bean>
<bean id = "testB" class = "xxx" scope = "prototype">
<property name = "testC" ref = "testC"/>
</bean>
<bean id = "testC" class = "xxx" scope = "prototype">
<property name = "testA" ref = "testA"/>
</bean>
测试代码:
public static void main(String[] args) {
try{
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
System.out.println(ctx.getBean("testA"));
}catch (Exception e){
e.printStackTrace();
}
}
同样通过断点我们可以定位异常的抛出位置是在AbstractBeanFactory类的doGetBean方法中,在方法开始获取缓存失败之后(prototype不会加入到缓存中),会首先判断prototype的bean是否已创建,如果是就认为存在循环依赖,抛出BeanCurrentlyInCreationException异常。
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
3. 总结
Spring中对于循环依赖的处理存在3中场景:
- 构造器循环依赖处理;
- setter循环依赖处理;
- prototype范围的依赖处理;
其中对于构造器和prototype范围的循环依赖,Spring是直接抛出异常。而对于单例的setter循环依赖,Spring是通过在bean加载过程中提前将bean的ObjectFactory加入到singletonFactories这个缓存用的map中来解决循环依赖的。
spring源码阅读笔记09:循环依赖的更多相关文章
- Spring源码阅读笔记02:IOC基本概念
上篇文章中我们介绍了准备Spring源码阅读环境的两种姿势,接下来,我们就要开始探寻这个著名框架背后的原理.Spring提供的最基本最底层的功能是bean容器,这其实是对IoC思想的应用,在学习Spr ...
- Spring源码阅读笔记
前言 作为一个Java开发者,工作了几年后,越发觉力有点不从心了,技术的世界实在是太过于辽阔了,接触的东西越多,越感到前所未有的恐慌. 每天捣鼓这个捣鼓那个,结果回过头来,才发现这个也不通,那个也不精 ...
- Spring源码阅读笔记01:源码阅读环境准备
1. 写在前面 对于做Java开发的同学来说,Spring就像是一条绕不过去的路,但是大多数也只是停留在对Spring的简单使用层面上,对于其背后的原理所知不多也不愿深究,关于这个问题,我在平时的生活 ...
- spring源码阅读笔记06:bean加载之准备创建bean
上文中我们学习了bean加载的整个过程,我们知道从spring容器中获取单例bean时会先从缓存尝试获取,如果缓存中不存在已经加载的单例bean就需要从头开始bean的创建,而bean的创建过程是非常 ...
- Spring源码阅读笔记03:xml配置读取
前面的文章介绍了IOC的概念,Spring提供的bean容器即是对这一思想的具体实现,在接下来的几篇文章会侧重于探究这一bean容器是如何实现的.在此之前,先用一段话概括一下bean容器的基本工作原理 ...
- Spring源码阅读笔记05:自定义xml标签解析
在上篇文章中,提到了在Spring中存在默认标签与自定义标签两种,并且详细分析了默认标签的解析,本文就来分析自定义标签的解析,像Spring中的AOP就是通过自定义标签来进行配置的,这里也是为后面学习 ...
- Spring源码阅读笔记04:默认xml标签解析
上文我们主要学习了Spring是如何获取xml配置文件并且将其转换成Document,我们知道xml文件是由各种标签组成,Spring需要将其解析成对应的配置信息.之前提到过Spring中的标签包括默 ...
- spring源码阅读笔记08:bean加载之创建bean
上文从整体视角分析了bean创建的流程,分析了Spring在bean创建之前所做的一些准备工作,并且简单分析了一下bean创建的过程,接下来就要详细分析bean创建的各个流程了,这是一个比较复杂的过程 ...
- spring源码阅读笔记10:bean生命周期
前面的文章主要集中在分析Spring IOC容器部分的原理,这部分的核心逻辑是和bean创建及管理相关,对于单例bean的管理,从创建好到缓存起来再到销毁,其是有一个完整的生命周期,并且Spring也 ...
随机推荐
- SpringCloud微服务架构和SOA架构
1,传统的三层架构 在传统的架构中,SSH,SSM,主要分为web 控制层,业务逻辑层,数据库访问层,单点项目,项目没有拆分,所有的开发任务全部写在一个项目中,耦合度比价高,如果程序中的一个功能出现了 ...
- 模块 pillow图像处理
Pillow概况 PIL是Python的一种图像处理工具. PIL支持大部分的图像格式,高效并强大. 核心库设计用来高速访问基于基于像素的数据存储,给这个通用的图像处理工具提供了坚实的基础. 一.读. ...
- MATLAB 图像打开保存
一.图片读取保存 (1)读取 clear all [filename,pathname]=uigetfile({'*.jpg';'*.bmp';'*.gif'},'选择图片'); if isequal ...
- 面试必备:详解Java I/O流,掌握这些就可以说精通了?
@TOC Java IO概述 IO就是输入/输出.Java IO类库基于抽象基础类InputStream和OutputStream构建了一套I/O体系,主要解决从数据源读入数据和将数据写入到目的地问题 ...
- 使用IDEA创建SpringBoot项目
SpringBoot学习第一步:搭建基础 IDEA对SpringBoot的项目支持可以说是点击就能完成基础的搭建,方便的不得了, 流程如下 1.左上角File选项,New project,选择Spri ...
- CCF2018 12 2题,小明终于到家了
最近在愁着备考,拿CCF刷题,就遇到这个难题,最后搜索了一下大佬们的方法,终于解决, 问题描述 一次放学的时候,小明已经规划好了自己回家的路线,并且能够预测经过各个路段的时间.同时,小明通过学校里安装 ...
- 拿万元月薪必备的书单,学JAVA的程序员必看的5本书!
点击蓝色"程序员黄小斜"关注我哟 加个"星标",每天带你读好书! 文/黄小斜 转载请注明出处 每一年的年初都是买书学习热情高涨的时候,虽然不知道你们是让这些书吃 ...
- PTA数据结构与算法题目集(中文) 7-42整型关键字的散列映射 (25 分)
PTA数据结构与算法题目集(中文) 7-42整型关键字的散列映射 (25 分) 7-42 整型关键字的散列映射 (25 分) 给定一系列整型关键字和素数P,用除留余数法定义的散列函数将关键字映射 ...
- fdisk分区规划和添加wap交换空间
分区规划和添加wap交换空间 1 案例1:硬盘分区及格式化 注意:fdisk只能分区小容量的磁盘 1.1 问题 本例要求熟悉硬盘分区结构,使用fdisk分区工具在磁盘 /dev/vdb 上按以下要 ...
- 独立Web站点的快速部署
独立Web站点的快速部署 1案例1:独立Web站点的快速部署 1.1问题 本 ...