本文结合《Spring源码深度解析》来分析Spring 5.0.6版本的源代码。若有描述错误之处,欢迎指正。

实例化bean是一个非常复杂的过程,而其中比较难以理解的就是对循环依赖的解决, 不管之前读者有没有循环依赖方面的研究,这里有必要先对此知识点稍作回顾。

一. 什么是循环依赖

循环依赖就是循环引用,就是两个或多个bean相互之间的持有对方,比如CirdeA引用 CircleB,CircleB引用CircleC,CircleC引用CircleA,则它们最终反映为一个环。此处不是循环调用,循环调用是方法之间的环调用,如下图所示。

循环调用是无法解决的,除非有终结条件,否则就是死循环,最终导致内存溢出错误。

二. Spring如何解决循环依赖

Spring容器循环依赖包括构造器循环依赖和setter循环依赖,那Spring容器如何解决循环依赖呢?首先让我们来定义循环引用类:

public class TestA {

    private TestB testB;

    public TestA() {
} 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 TestB() {
} 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 TestC() {
} public void c() {
testA.a();
} public TestA getTestA() {
return testA;
} public void setTestA(TestA testA) {
this.testA = testA;
}
}

在Spring中将循环依赖的处理分成了3种情况。

1. 构造器循环依赖

表示通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。

如在创建TestA类时,构造器需要TestB类,那将去创建TestB,在创建TestB类时又发现需要TestC类,则又去创建TestC,最终在创建TestC时发现又需要TestA,从而形成一个环, 没办法创建。

Spring容器将每一个正在创建的bean标识符放在一个“当前创建bean池”中,bean标识符在创建过程中将一直保持在这个池中,因此如果在创建bean过程中发现自己已经在“当前创建bean池”里时,将抛出BeanCurrentlylnCreationFexception异常表示循环依赖;而对于创建完毕的bean将从“当前创建bean池”中淸除掉。

我们通过一个直观的测试用例来进行分析。

(1)创建配置文件。

<bean id = "testA" class = "com.bean.TestA">
<constructor-arg index = "0" ref = "testB"/>
</bean>
<bean id = "testB" class = "com.bean.TestB">
<constructor-arg index="0" ref = "testC"/>
</bean>
<bean id = "testC" class = "com.bean.TestC">
<constructor-arg index="0" ref="testA"/>
</bean>

(2)创建测试用例。

@Test(expected = BeanCurrentlylnCreationException.class)
public void testCircleByConstructor() throws Throwable {
try {
new ClassPathXmlApplicationContext("test-xml");
} catch(Exception e) {
// 因为要在创建testC时抛出
Throwable el = e.getCause().getCause().getCause{);
throw el;
}
}

针对以上代码的分析如下:

  • Spring容器创建“testA” bean,首先去“当前创建bean池”查找是否当前bean正在创建,如果没发现,则继续准备其需要的构造器参数“testB”,并将“testA”标识符放到“当前创建bean池”。
  • Spring容器创建“testB” bean,首先去“当前创建bean池”查找是否当前bean正在创建,如果没发现,则继续准备其需要的构造器参数“testC”,并将“testB”标识符放到“当前创建bean池。
  • Spring容器创建“testC” bean,首先去“当前创建bean池”查找是否当前bean正在创建,如果没发现,则继续准备其需要的构造器参数“testA”,并将“testC”标识符放到“当前创建Bean池”。
  • 到此为止Spring容器要去创建“testA” bean,发现该bean标识符在“当前创建bean 池”中,因为表示循环依赖,抛出BeanCurrentlylnCreationException。

2. setter循环依赖

表示通过setter注入方式构成的循环依赖。对于setter注入造成的依赖是通过Spring容器提前暴露刚完成构造器注人但未完成其他步骤(如setter注人)的bean来完成的,而且只能解决单例作用域的bean循环依赖。通过提前暴露一个单例工厂方法,从而使其他bean能引用到该bean,如下代码所示:

addSingletonFactory(beanName, new ObjectFactory() {
public Object getObject() throws BeansCxception {
return getEarlyBeanReference(beanName, mbdr bean);
});

具体步骤如下:

(1)Spring容器创建单例“testA” bean,首先根据无参构造器创建bean,并暴露一个 “ObjectFactory”用于返回一个提前暴露的创建中的bean,并将“testA”标识符放到“当前创建bean池”,然后进行setter注人“testB”。

(2)Spring容器创建单例“testB” bean,首先根据无参构造器创建bean,并暴露一个 “ObjectFactory”用于返回一个提前暴露的创建中的bean,并将“testB”标识符放到“当前创建bean池”,然后进行setter注人“testC”。

(3)Spring容器创建单例“testC” bean,首先根据无参构造器创建bean,并暴露一个 “ObjectFactory”用于返回一个提前暴露的创建中的bean,并将“testC”标识符放到“当前创建bean池',然后进行setter注人“testA”。进行注人“testA”时由于提前暴露了 “ObjectFactory” 工厂,从而使用它返回提前暴露的创建中的bean。

(4)最后在依赖注入“testB”和“testA”,完成setter注人。

3. prototype范围的依赖处理

对于“prototype”作用域bean,Spring容器无法完成依赖注人,因为Spring容器不进行缓存“prototype”作用域的bean,因此无法提前暴露一个创建中的bean。示例如下:

(1)创建配置文件。

<bean id = "testA" class = "com.bean.CircleA" scope = "prototype">
<property name = "testB" ref = "testB"/>
</bean>
<bean id = "testB" class = "com.bean.CircleB" scope = "prototype">
<property name = "testC" ref = "testC"/>
</bean>
<bean id = "testC" class = "com.bean.CircleC" scope="prototype">
<property name = "testA" ref = "testA"/>
</bean>

(2)创建测试用例。

@Test(expected = BeanCurrentlylnCreationException.class)
public void testCircleBySetterAndPrototype() throws Throwable {
try {
ApplicationContext context = new ClassPathXmlApplicationContext("test-prototype.xml");
System.out.println(context.getBean("testA"));
} catch(Exception e) {
Throwable el = e.getCause().getCause().getCause{);
throw el;
}
}

对于 “singleton” 作用域 bean,可以通过 “setAllowCircularReferences(false); ”来禁用循环引用。

Spring源码分析(十七)循环依赖的更多相关文章

  1. Spring源码分析之循环依赖及解决方案

    Spring源码分析之循环依赖及解决方案 往期文章: Spring源码分析之预启动流程 Spring源码分析之BeanFactory体系结构 Spring源码分析之BeanFactoryPostPro ...

  2. Spring源码-IOC部分-循环依赖-用实例证明去掉二级缓存会出现什么问题【7】

    实验环境:spring-framework-5.0.2.jdk8.gradle4.3.1 Spring源码-IOC部分-容器简介[1] Spring源码-IOC部分-容器初始化过程[2] Spring ...

  3. Spring 源码分析之 bean 依赖注入原理(注入属性)

         最近在研究Spring bean 生命周期相关知识点以及源码,所以打算写一篇 Spring bean生命周期相关的文章,但是整理过程中发现涉及的点太多而且又很复杂,很难在一篇文章中把Spri ...

  4. Spring源码分析之Bean的创建过程详解

    前文传送门: Spring源码分析之预启动流程 Spring源码分析之BeanFactory体系结构 Spring源码分析之BeanFactoryPostProcessor调用过程详解 本文内容: 在 ...

  5. Spring源码分析——BeanFactory体系之抽象类、类分析(二)

    上一篇分析了BeanFactory体系的2个类,SimpleAliasRegistry和DefaultSingletonBeanRegistry——Spring源码分析——BeanFactory体系之 ...

  6. Spring源码分析——BeanFactory体系之抽象类、类分析(一)

    上一篇介绍了BeanFactory体系的所有接口——Spring源码分析——BeanFactory体系之接口详细分析,本篇就接着介绍BeanFactory体系的抽象类和接口. 一.BeanFactor ...

  7. 【spring源码分析】IOC容器初始化(总结)

    前言:在经过前面十二篇文章的分析,对bean的加载流程大致梳理清楚了.因为内容过多,因此需要进行一个小总结. 经过前面十二篇文章的漫长分析,终于将xml配置文件中的bean,转换成我们实际所需要的真正 ...

  8. 【spring源码分析】IOC容器初始化(七)

    前言:在[spring源码分析]IOC容器初始化(六)中分析了从单例缓存中加载bean对象,由于篇幅原因其核心函数 FactoryBeanRegistrySupport#getObjectFromFa ...

  9. spring源码分析系列

    spring源码分析系列 (1) spring拓展接口BeanFactoryPostProcessor.BeanDefinitionRegistryPostProcessor spring源码分析系列 ...

  10. Spring源码分析(十八)创建bean

    本文结合<Spring源码深度解析>来分析Spring 5.0.6版本的源代码.若有描述错误之处,欢迎指正. 目录 一.创建bean的实例 1. autowireConstructor 2 ...

随机推荐

  1. python学习之老男孩python全栈第九期_day016作业

    1. 请利用filter()过滤出1~100中平方根是整数的数,即结果应该是: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] import math def func( ...

  2. js处理包含中文的字符串

    场景: js中String类型自带的属性length获取的是字符串的字符数目,但是前端经常会需要限制字符串的显示长度,一个中文字符又大概占两个英文小写字符的显示位置,所以中英文混合的情况下用lengt ...

  3. p2p项目工具类

    1.用于存放当前用户的上下文UserContext package com.xmg.p2p.base.util; import javax.servlet.http.HttpSession; impo ...

  4. opencv3.2.0 分离颜色通道&多通道图像混合

    ##名称:分离颜色通道&多通道图像混合 ##平台:QT5.7.1+OpenCV3.2.0 ##时间:2017年12月11日 /***************创建QT控制台程序********* ...

  5. aix系统下的websphere的静默安装

     一:环境 aix5.3,websphere6(ND版本,WebSphereV6.1_for_AIX_64-bit_Support.tar),注意:aix和websphere的版本问题 二:安装 ...

  6. logminer系列文章一(logminer的使用)

    转自 http://blog.itpub.net/26613085/viewspace-1064008/ 1.安装logminer以及生成logminer数据字典所需要的包(需使用sys用户) [or ...

  7. Django 请求参数

    Django 请求参数 1.获取URL路径中的参数 需求:假设用户访问127.0.0.1/user/1/2,你想获取1,2.应该怎么操作呢? (1)未命名参数(位置参数) # 在项目下的urls.py ...

  8. Django 简单用户注册

    项目名 macboy app名 t1 1.项目macboy urls.py from django.conf.urls import url, include urlpatterns = [ url( ...

  9. 【Java】数组使用

    package aaa; public class aaa { public static void main(String args[]) { int a[]={1,2,3,4}; for(int ...

  10. 将虚拟网络连接到 ExpressRoute 线路

    本文通过使用 Resource Manager 部署模型和 Azure 门户,帮助将虚拟网络 (VNets) 链接到 Azure ExpressRoute 线路. 虚拟网络可以在同一个订阅中,也可以属 ...