引言

  1. 基于Spring5+
  2. 什么是循环依赖?
  3. 循环依赖有几种?
  4. Spring可以解决哪几种,为什么不能解决这几种?
  5. Spring是如何判断存在循环依赖的?

什么是循环依赖?

什么是循环依赖?我们都知道Spring最大的作用就是来替我们管理Bean的,当然也包括Bean的创建以及整个生命周期,但是有这么一种情况,假设有三个类A、B、C需要交给Spring来管理,但A实例的创建需要先有B实例,而B实例的创建需要先有C实例,C实例的创建需要先有A实例,这样三个类就自然形成了一个环状结构,如果用代码来表示,如下:

public class TestA {TestB testB;get;set;
}public class TestB {TestC testC;get;set;
}public class TestC {TestA testA;get;set;
}

这样,三个类就彼此形成了一个环状,那么Spring是如何来处理这样的状况呢?

循环依赖有几种?

有三种情况:

  1. 基于构造方法的循环依赖
  2. 基于setter构造的循环依赖(网上也叫field属性依赖)
  3. 基于prototype范围的依赖

Spring可以解决哪些循环依赖,为什么?

首先说一下结论:除了第二种Spring可以帮我们解决,其它两种都不能解决。我们知道Spring为我们完全实例化好一个Bean一定会经过一下三步:

  1. createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象。
  2. populateBean:填充属性,这一步主要是多bean的依赖属性进行填充。
  3. initializeBean:调用默认的或者自定义的init方法。

循环依赖的产生定会发生在步骤1和2中,因为1是利用构造方法,2是利用属性赋值。

基于构造方法的循环依赖

先说结论基于构造器的循环依赖Spring是无法解决的,是因为没有加入提前曝光的集合中,加入集合的条件是已经创建了Bean的包装对象,而构造注入的时候,并没有完成对象的创建,下面会有代码说明。

测试用例:

xml文件:

<bean id="testA" class="com.nmys.story.springCore.loop_dependency.loop01.LoopA"><constructor-arg index="0" ref="testB"/>
</bean><bean id="testB" class="com.nmys.story.springCore.loop_dependency.loop01.LoopB"><constructor-arg index="0" ref="testC"/>
</bean><bean id="testC" class="com.nmys.story.springCore.loop_dependency.loop01.LoopC"><constructor-arg index="0" ref="testA"/>
</bean>

测试类:

/*** description:测试通过有参构造方式注入产生的循环依赖问题* @author 70KG* @date 2018/12/21*/
public class Test02 {@Testpublic void m1() {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test02.xml");}}

分析上面代码:

  1. Spring容器创建testA的Bean实例,首先去"当前创建Bean池",查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数testB,并将testA标识符放到"当前创建Bean池"。
  2. Spring容器创建testB的Bean实例,首先去"当前创建Bean池",查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数testC,并将testB标识符放到"当前创建Bean池"。
  3. Spring容器创建testC的Bean实例,首先去"当前创建Bean池",查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数testA,并将testC标识符放到"当前创建Bean池"。
  4. 到此为止Spring容器要去创建testA,但发现该Bean的标志符在"当前创建Bean池"中,表示了循环依赖,于是抛出BeanCurrentlyInCreationException异常。

其中"当前创建Bean池"就是一个Set集合,DefaultSingletonBeanRegistry类中beforeSingletonCreation方法,代码如下:

protected void beforeSingletonCreation(String beanName) {if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {throw new BeanCurrentlyInCreationException(beanName);}
}

然后我们来到创建Bean实例的地方:

AbstractAutowireCapableBeanFactory类的543行,通过这个方法返回一个这个Bean的包装对象:

--> instanceWrapper = createBeanInstance(beanName, mbd, args);----> 进入这个方法

--> AbstractAutowireCapableBeanFactory类的1129行

// Need to determine the constructor...
// 需要确定构造函数,也就是说构造方法的循环依赖会在这儿return
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null ||mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR ||mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args))  {return autowireConstructor(beanName, mbd, ctors, args);
}// No special handling: simply use no-arg constructor.
// 无需特殊处理,仅使用无参构造即可,setter的循环依赖会在这个地方return
return instantiateBean(beanName, mbd);

在上面代码中返回Bean的包装对象下面紧接着才是将这个对象曝光,也就是加入到SingletonFactory集合中,所以构造方法的循环引用,Spring是无法解决的,来到AbstractAutowireCapableBeanFactory的574行。

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

基于setter构造的循环依赖

首先说结论:Spring是可以为我们解决这样的依赖的,原理说白了就是用了缓存处理,也就是常说的提前曝光,为什么叫提前曝光呢?因为这个缓存中的Bean是一个还未进行赋值的Bean,仅仅是一个引用而已。

xml文件:

<bean id="testA" class="com.nmys.story.springCore.loop_dependency.loop01.LoopA"><property name="loopB" ref="testB"/>
</bean><bean id="testB" class="com.nmys.story.springCore.loop_dependency.loop01.LoopB"><property name="loopC" ref="testC"/>
</bean><bean id="testC" class="com.nmys.story.springCore.loop_dependency.loop01.LoopC"><property name="loopA" ref="testA"/>
</bean>

测试类:

/*** description:通过setter注入产生的循环依赖问题* @author 70KG*/
public class Test03 {@Testpublic void m1() {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test03.xml");}
}

代码分析:

  1. Spring容器创建单例"loopA",首先根据无参构造创建Bean,并暴露到Map(singletonFactories)中,并将"loopA"标志符放到当前创建正在创建的Bean池(singletonsCurrentlyInCreation)中,然后进行setter注入"loopB"。
  2. Spring容器创建单例"loopB",首先根据无参构造创建Bean,并暴露到Map(singletonFactories)中,并将"loopA"标志符放到当前创建正在创建的Bean池(singletonsCurrentlyInCreation)中,然后进行setter注入"loopC"。
  3. Spring容器创建单例"loopC",首先根据无参构造创建Bean,并暴露到Map(singletonFactories)中,并将"loopA"标志符放到当前创建正在创建的Bean池(singletonsCurrentlyInCreation)中,然后进行setter注入"loopA"。在注入"loopA"的时候,由于提前暴露在singletonFactories集合中了,利用它就可以取到"loopA"正在创建的Bean对象。
  4. 最后依赖注入"testB","testA",完成setter注入。

查看控制台输出日志:

// 正在创建testA对象
Creating shared instance of singleton bean 'testA'
Creating instance of bean 'testA'
// 在缓存早期引用,目的是防止循环引用问题
Eagerly caching bean 'testA' to allow for resolving potential circular references
Creating shared instance of singleton bean 'testB'
Creating instance of bean 'testB'
Eagerly caching bean 'testB' to allow for resolving potential circular references
Creating shared instance of singleton bean 'testC'
Creating instance of bean 'testC'
Eagerly caching bean 'testC' to allow for resolving potential circular references
// 在创建testC的时候会去缓存中拿原来存储的testA,并返回,但此时的testA是一个不完全的对象,也就是尚未初始化
Returning eagerly cached instance of singleton bean 'testA' that is not fully initialized yet - a consequence of a circular reference
// 紧接着完成C的创建,顺便其它的也完成了
Finished creating instance of bean 'testC'
Finished creating instance of bean 'testB'
Finished creating instance of bean 'testA'
Returning cached instance of singleton bean 'testB'
Returning cached instance of singleton bean 'testC'

基于setter的循环依赖利用了提前曝光机制,这一步的关键代码,在AbstractAutowireCapableBeanFactory的574行,代码如下:

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

在加入SingletonFactory的前提是此Bean已经创建出来,才能够加入到这个Map集合中,也就是提前曝光,可以让别的Bean在初始化的时候从中拿到。否则是没有机会加入到Map中的。

基于prototype范围的依赖

首先说结论,对于多例情况下的循环依赖,是无法解决的,因为Spring容器不进行缓存,更无法提前暴露。

测试用例:

xml文件:

<bean id="testA" class="com.nmys.story.springCore.loop_dependency.loop01.LoopA" scope="prototype"><property name="loopB" ref="testB"/>
</bean><bean id="testB" class="com.nmys.story.springCore.loop_dependency.loop01.LoopB" scope="prototype"><property name="loopC" ref="testC"/>
</bean><bean id="testC" class="com.nmys.story.springCore.loop_dependency.loop01.LoopC" scope="prototype"><property name="loopA" ref="testA"/>
</bean>

测试类:

/*** description:通过setter注入产生的循环依赖问题* @author 70KG*/
public class Test03 {@Testpublic void m1() {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test03.xml");LoopA loopA = context.getBean(LoopA.class);System.out.println(loopA);}}

会抛出BeanCurrentlyInCreationException异常。

Spring是如何检测循环依赖

来到AbstractBeanFactory的246行,代码如下:

Object sharedInstance = getSingleton(beanName);

这一步是从缓存中获取以前创建的实例,如果发现存在,那么就存在循环依赖。

到此,全文完,自我感觉比其他的整理还算详细,如有疑问,请留言。

Spring源码分析:Spring的循环依赖分析相关推荐

  1. Spring源码系列- Spring Beans - 核心类的基本介绍

    Spring源码系列- Spring Beans - 核心类的基本介绍 读过上一篇文章的读者应该都能对Spring的体系结构有一个大致的了解,在结尾处,我也说过会从spring-beans包开始分析, ...

  2. 【Spring源码】Spring Transactional事务:传播机制(Propagation) 介绍 和 源码剖析

    [Spring源码]Spring Transactional事务:传播机制(Propagation) 源码剖析 关键词 AMethod调用BMethod,转载BMethod的角度来考虑:站在被调用者的 ...

  3. spring源码学习之整合Mybatis原理分析

    本文主要解析spring是如何与mybatis进行整合,整合的过程中需要哪些组件的支持.以前面提到过的配置例子<spring源码学习之aop事物标签解析> 整合的过程中需要使用以下这个依赖 ...

  4. 『互联网架构』软件架构-spring源码之spring结构概述

    spring从07年接触到目前2018年也差不多10年了,一个java的框架为什么能火这么多年.很多人对spring的使用轻车熟路,但是从未解读过spring的源码,老铁跟我一起看看源码,了解下内部的 ...

  5. Spring源码之Spring的大体框架

    Spring源码(一)----了解Spring的大体框架 Spring的源码数量特别多,在我下载了Spring FrameWork源码后,发现有些无从学起,那么就一步一步来,先不管其他,先了解一下Sp ...

  6. Spring源码:Spring源码阅读环境搭建

    本篇内容包括:Mac 环境下 gradle 的安装和配置.源码克隆.新建测试类,测试Spring源码 等内容! 第一步:Mac 环境下 gradle 的安装和配置 1.下载安装包 # 到 GitHub ...

  7. Spring源码剖析-Spring如何处理循环依赖

    前言 你是不是被这个骚气的标题吸引进来的,_ 喜欢我的文章的话就给个好评吧,你的肯定是我坚持写作最大的动力,来吧兄弟们,给我一点动力 Spring如何处理循环依赖?这是最近较为频繁被问到的一个面试题, ...

  8. 【Spring源码】Spring中的AOP底层原理分析

    AOP中的几个概念 Advisor 和 Advice Advice,我们通常都会把他翻译为通知,其实很不好理解,其实他还有另外一个意思,就是"建议",我觉得把Advice理解为&q ...

  9. 【Spring源码】Spring事务原理

    目录 1.什么是事务 2.Spring事务基本概念 2.1.基础配置 2.1.1.Spring事务的基础配置 2.1.2.Spring事务的传播特性 2.1.3.Spring事务的隔离级别 2.2.基 ...

  10. 【Spring源码】 Spring IoC容器启动之Bean创建流程

    上篇已经分析完refresh()中大部分方法,也已经把Bean解析成BeanDefinition注册到IoC容器中,refresh还剩下一个非常重要的方法,就是下面将要分析的:finishBeanFa ...

最新文章

  1. Ubuntu12.04 安装vim出错
  2. STL的forward_list链表
  3. 牛客题霸 [二叉树的最大深度]C++题解/答案
  4. 防抖函数和节流函数的实现,这个是在某保险公司笔试题遇到的。
  5. SharePoint入门识记-整体架构
  6. [转]关于支付宝API开发的一点心得
  7. teamcity和jmeter结合进行接口自动化测试
  8. Windows10 文件夹按分组排序
  9. office_professional_plus_2010安装
  10. 【网易微专业】算法原理与实践 2
  11. 南京南汽技工学校计算机专业是啥,计算机中心附近技术类学校
  12. spark(day05)
  13. 腾讯QQ体验版 for Macv8.4.10.73官方
  14. 数据库中间件Mycat诞生记2
  15. 基于STM32的智能篮球测温记分记时系统
  16. 让OpenAi给我写个JS的set对象的笔记和快速去重方法
  17. TU-Net/TDeepLab:基于RGB和红外的地形分类
  18. 更改技嘉主板开机画面
  19. 【Linux】进程间通信-共享内存
  20. 【无标题】【2023最新版】超详细Sqlmap安装保姆级教程,SQL注入使用指南,收藏这一篇就够了

热门文章

  1. 11210怎么等于24_算24点
  2. Unity HDRP室外场景打光流程分享(下篇)-白天和夜晚场景打光
  3. DOORS和Reqtify — 需求管理和需求追溯工具
  4. Idea 报错: Variable used in lambda expression should be final or effectively final
  5. mysql启动报错之[ERROR] Found option without preceding group in config file /etc/my.cnf at line
  6. Python绘制箱形图全解
  7. python 绘制箱型图_Python数据可视化:Seaborn 绘制箱形图
  8. 腾讯Bugly的使用
  9. @NotNull 和 @NotEmpty 和@NotBlank 区别
  10. 求助 :Not allowed to launch ‘bytedance://dispatch_message/‘ because a user gesture is required.