目录

1 什么是循环依赖?

1.1 构造器循环依赖

1.2 field属性注入循环依赖

1.3 field属性注入循环依赖(prototype)

2 循环依赖处理

2.1 构造器循环依赖(无法解决)

2.2  setter循环依赖(可以解决)

2.3 prototype范围的依赖处理(无法解决)

3 Spring是如何解决的循环依赖?

简单的循环依赖(没有AOP)

结合了AOP的循环依赖

4 面试答案


1 什么是循环依赖?

循环依赖就是循环引用,指两个或多个bean互相持有对方,比如说TestA引用TestB、TestB引用TestA,最终形成一个闭环。

注意:循环依赖不是指循环调用。

从字面上来理解就是A依赖B的同时B也依赖了A,就像下面这样

循环调用:指方法之间的环调用,循环调用是无解的,除非有终结条件,否则就是死循环,最终会导致内存溢出异常。

两种Spring容器循环依赖:

  1. 构造器循环依赖
  2. setter方法循环依赖

体现到代码层次就是这个样子

1.1 构造器循环依赖

@Service
public class A {  public A(B b) {  }
}@Service
public class B {  public B(C c) {  }
}@Service
public class C {  public C(A a) {  }
}

结果:项目启动失败,发现了一个cycle

1.2 field属性注入循环依赖

@Service
public class A1 {  @Autowired  private B1 b1;
}@Service
public class B1 {  @Autowired  public C1 c1;
}@Service
public class C1 {  @Autowired  public A1 a1;
}

结果:项目启动成功

1.3 field属性注入循环依赖(prototype)

@Service
@Scope("prototype")
public class A1 {  @Autowired  private B1 b1;
}@Service
@Scope("prototype")
public class B1 {  @Autowired  public C1 c1;
}@Service
@Scope("prototype")
public class C1 {  @Autowired  public A1 a1;
}

结果:项目启动失败,发现了一个cycle。

现象总结:同样对于循环依赖的场景,构造器注入和prototype类型的属性注入都会初始化Bean失败。因为@Service默认是单例的,所以单例的属性注入是可以成功的

2 循环依赖处理

2.1 构造器循环依赖(无法解决)

表示通过构造器注入构成的循环依赖,此依赖是无解的,强行依赖只能抛出异常(BeanCreationException);

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

下面我们通过一段代码来印证上述理论。

创建application.xml配置文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="testA" class="com.chenpt.springFrameWork.TestA"><constructor-arg name="testB" ref="testB"></constructor-arg></bean><bean id="testB" class="com.chenpt.springFrameWork.TestB"><constructor-arg name="testA" ref="testA"></constructor-arg></bean></beans>

客户端测试用例:

public class MainTest {public static void main(String[] args){ApplicationContext context = new FileSystemXmlApplicationContext("classpath:spring/applicationContext.xml");}
}

 执行结果如下图(仅截了部分错误)

2.2  setter循环依赖(可以解决)

指通过setter注入方式构成的循环依赖。

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

代码示例:

首先需要去掉构造器注入的参数。

//bean1
public class TestA {private TestB testB;TestA(){}public TestB getTestB() {return testB;}public void setTestB(TestB testB) {this.testB = testB;}
}
//bean2
public class TestB {private TestA testA;TestB(){}public TestA getTestA() {return testA;}public void setTestA(TestA testA) {this.testA = testA;}
}

xml示例:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="testA" class="com.chenpt.springFrameWork.TestA" scope="singleton"><property name="testB" ref="testB"/></bean><bean id="testB" class="com.chenpt.springFrameWork.TestB" scope="singleton"><property name="testA" ref="testA"/></bean></beans>

客户端执行(自行演示,无输出错误)

2.3 prototype范围的依赖处理(无法解决)

对于prototype作用域bean,spring容器无法完成依赖注入,因为spring容器不进行缓存prototype作用域的bean,因此无法提前暴露一个正在创建中的bean。

示例代码如下:

xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="testA" class="com.chenpt.springFrameWork.TestA" scope="prototype"><property name="testB" ref="testB"/></bean><bean id="testB" class="com.chenpt.springFrameWork.TestB" scope="prototype"><property name="testA" ref="testA"/></bean></beans>

客户端示例

public class MainTest {public static void main(String[] args){ApplicationContext context = new FileSystemXmlApplicationContext("classpath:spring/applicationContext.xml");TestB testB = context.getBean("testB",TestB.class);}
}

执行结果,抛出异常

针对上述的作用域(scope)分析

beanFactory除了拥有ioc的职责外,还有着对象生命周期管理。

scope用来声明容器中对象所应该处的限定场景或者该对象的存活时间,即容器在对象进入其相应的scope之前,生成并装配这些对象,在对象不再处于scope的限定之后,容器通常会销毁这些对象。

spring容器提供了几种scope类型?

  • singleton:在spring容器中只存在一个实例,所有对象的引用将共享这个实例。(注:不要和单例模式搞混)
  • prototype:容器每次都会生成一个新的对象实例给请求方。
  • request (限定在web应用中使用):为每个http请求创建一个全新的request-processor对象供当前请求使用,请求结束,实例生命周期即结束。
  • session (限定在web应用中使用):为每个独立的session创建一个全新的UserPreference对象实例。
  • global session (限定在web应用中使用):只有应用在基于portlet的Web应用程序中才有意义,它映射到portlet的global范围的session。如果在普通的基于servlet的Web应用中使用了这个类型的scope,容器会将其作为普通的session类型的scope对待。

下面主要说明第二种情况中循环依赖的解决方案

  步骤一:beanA进行初始化,并且将自己进行初始化的状态记录下来,并提前向外暴露一个单例工程方法,从而使其他bean能引用到该bean(可能读完这一句,您仍然心存疑惑,没关系,继续往下读)

  步骤二:beanA中有beanB的依赖,于是开始初始化beanB。

  步骤三:初始化beanB的过程中又发现beanB依赖了beanA,于是又进行beanA的初始化,这时发现beanA已经在进行初始化了,程序发现了存在的循环依赖,然后通过步骤一中暴露的单例工程方法拿到beanA的引用(注意,此时的beanA只是完成了构造函数的注入但为完成其他步骤),从而beanB拿到beanA的引用,完成注入,完成了初始化,如此beanB的引用也就可以被beanA拿到,从而beanA也就完成了初始化。

  spring进行bean的加载的时候,首先进行bean的初始化(调用构造函数),然后进行属性填充。在这两步中间,spring对bean进行了一次状态的记录,也就是说spring会把指向只完成了构造函数初始化的bean的引用通过一个变量记录下来,明白这一点对之后的源码理解至关重要。

3 Spring是如何解决的循环依赖?

关于循环依赖的解决方式应该要分两种情况来讨论

  1. 简单的循环依赖(没有AOP)
  2. 结合了AOP的循环依赖

简单的循环依赖(没有AOP)

首先,我们要知道Spring在创建Bean的时候默认是按照自然排序来进行创建的,所以第一步Spring会去创建A

与此同时,我们应该知道,Spring在创建Bean的过程中分为三步

  1. 实例化,对应方法:AbstractAutowireCapableBeanFactory中的createBeanInstance方法

  2. 属性注入,对应方法:AbstractAutowireCapableBeanFactorypopulateBean方法

  3. 初始化,对应方法:AbstractAutowireCapableBeanFactoryinitializeBean

  1. 实例化,简单理解就是new了一个对象
  2. 属性注入,为实例化中new出来的对象填充属性
  3. 初始化,执行aware接口中的方法,初始化方法,完成AOP代理

从上图中我们可以看到,虽然在创建B时会提前给B注入了一个还未初始化的A对象,但是在创建A的流程中一直使用的是注入到B中的A对象的引用,之后会根据这个引用对A进行初始化,所以这是没有问题的。

结合了AOP的循环依赖

  1. 在给B注入的时候为什么要注入一个代理对象?

答:当我们对A进行了AOP代理时,说明我们希望从容器中获取到的就是A代理后的对象而不是A本身,因此把A当作依赖进行注入时也要注入它的代理对象

  1. 明明初始化的时候是A对象,那么Spring是在哪里将代理对象放入到容器中的呢?

在完成初始化后,Spring又调用了一次getSingleton方法,这一次传入的参数又不一样了,false可以理解为禁用三级缓存,前面图中已经提到过了,在为B中注入A时已经将三级缓存中的工厂取出,并从工厂中获取到了一个对象放入到了二级缓存中,所以这里的这个getSingleton方法做的时间就是从二级缓存中获取到这个代理后的A对象。exposedObject == bean可以认为是必定成立的,除非你非要在初始化阶段的后置处理器中替换掉正常流程中的Bean,例如增加一个后置处理器:

@Component
public class MyPostProcessor implements BeanPostProcessor {@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {if (beanName.equals("a")) {return new A();}return bean;}
}

不过,请不要做这种骚操作,徒增烦恼!

  1. 初始化的时候是对A对象本身进行初始化,而容器中以及注入到B中的都是代理对象,这样不会有问题吗?

答:不会,这是因为不管是cglib代理还是jdk动态代理生成的代理类,内部都持有一个目标类的引用,当调用代理对象的方法时,实际会去调用目标对象的方法,A完成初始化相当于代理对象自身也完成了初始化

  1. 三级缓存为什么要使用工厂而不是直接使用引用?换而言之,为什么需要这个三级缓存,直接通过二级缓存暴露一个引用不行吗?

答:这个工厂的目的在于延迟对实例化阶段生成的对象的代理,只有真正发生循环依赖的时候,才去提前生成代理对象,否则只会创建一个工厂并将其放入到三级缓存中,但是不会去通过这个工厂去真正创建对象

4 面试答案

”Spring是如何解决的循环依赖?“

答:Spring通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects),二级缓存为早期曝光对象earlySingletonObjects,三级缓存为早期曝光对象工厂(singletonFactories)。当A、B两个类发生循环引用时,在A完成实例化后,就使用实例化后的对象去创建一个对象工厂,并添加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象。当A进行属性注入时,会去创建B,同时B又依赖了A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取,第一步,先获取到三级缓存中的工厂;第二步,调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到B中。紧接着B会走完它的生命周期流程,包括初始化、后置处理器等。当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。至此,循环依赖结束!

面试官:”为什么要使用三级缓存呢?二级缓存能解决循环依赖吗?“

答:如果要使用二级缓存解决循环依赖,意味着所有Bean在实例化后就要完成AOP代理,这样违背了Spring设计的原则,Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理,而不是在实例化后就立马进行AOP代理。

Spring中的循环依赖解决详解相关推荐

  1. Spring三级缓存解决循环依赖问题详解

    spring三级缓存解决循环依赖问题详解 前言 这段时间阅读了spring IOC部分的源码.在学习过程中,自己有遇到过很多很问题,在上网查阅资料的时候,发现很难找到一份比较全面的解答.现在自己刚学习 ...

  2. 面试:讲一讲Spring中的循环依赖

    前言 Spring中的循环依赖一直是Spring中一个很重要的话题,一方面是因为源码中为了解决循环依赖做了很多处理,另外一方面是因为面试的时候,如果问到Spring中比较高阶的问题,那么循环依赖必定逃 ...

  3. 面试必杀技,讲一讲Spring中的循环依赖

    本系列文章: 听说你还没学Spring就被源码编译劝退了?30+张图带你玩转Spring编译 读源码,我们可以从第一行读起 你知道Spring是怎么解析配置类的吗? 配置类为什么要添加@Configu ...

  4. Spring中的循环依赖

    目录 一.什么是循环依赖? 二.Bean的生命周期 2.1 Spring Bean 的生命周期 2.2 Bean 的生成步骤 三.三级缓存 3.1三个缓存分别有什么作用 四.思路分析 4.1 为什么 ...

  5. 一起来踩踩 Spring 中这个循环依赖的坑!

    作者:Mythsman blog.mythsman.com/post/5d838c7c2db8a452e9b7082c/ 1. 前言 2. 典型场景 3. 什么是依赖 4. 什么是依赖调解 5. 为什 ...

  6. Spring中的循环依赖问题

    Spring的的循环依赖问题 文章目录 Spring的的循环依赖问题 一. 简介 1.什么是循环依赖问题? 2.循环依赖有什么影响? 二. 循环依赖复现 三. 解决方案 1. 重新设计 2 使用 @L ...

  7. 面试——Spring中的循环依赖

    1 什么是Spring循环依赖 // A依赖了B,B是A对象中的一个属性 class A{public B b; }// B依赖了A class B{public A a; } 在普通的代码中,对象之 ...

  8. Spring中的循环依赖及解决,2021Java精选面试实战总结整理

    那么在创建B类的Bean的过程中,如果B类中存在一个A类的a属性,那么在创建B的Bean的过程中就需要A类对应的Bean,但是,触发B类Bean的创建的条件是A类Bean在创建过程中的依赖注入,所以这 ...

  9. Spring循环依赖问题详解

    一.循环依赖产生的原因 我们知道Spring最具盛名的就是依赖注入,而循环依赖就是指多个bean相互依赖,形成了一个闭环,比如:A依赖于B.B依赖于C.C依赖于A. 简单看代码 class A {B ...

最新文章

  1. 第十七课:js数据缓存系统的原理
  2. centos 7 忘记密码
  3. 工业用微型计算机(28)-dos和bios功能调用(2)-int 21h
  4. 对CAN、USART、SPI、SCI等常见总线的简单介绍
  5. C++:基于范围的for循环
  6. 开源 计划管理_公司开源计划的三大好处
  7. OpenShift 4 - DevSecOps Workshop (7) - 为Pipeline增加向Nexus制品库推送任务
  8. html自学学多久,html自学教程(一)初识html
  9. Python: hashlib库、sha256、md5
  10. 7z 7Zip 命令行压缩,解压缩文件
  11. ps联盟服务器无响应怎么办,PS联盟网新手教程视频
  12. 【redis】跟我一起动手玩玩redis主从复制和哨兵模式
  13. 那些年常见的前端bug (持续更新)
  14. 腾讯云轻量应用服务器下使用RPM包方式安装GreatSQL单主环境
  15. 嵌入式人工智能唱响2020年中国嵌入式技术大会!
  16. iOS 手势的用法
  17. Unity3D教程:手游开发常用排序算法 -下
  18. 生物信息学在感染和疫苗研究中的应用
  19. C#语言实例源码系列-实现动态图标闪烁显示
  20. 灰色预测GM(1,1)代码

热门文章

  1. 面试——测试用例设计
  2. Python实现文件复制
  3. anki筛选牌组语法
  4. 「实在RPA·电力数字员工」助推电力行业提质增效
  5. 代码随想录算法训练营day7| 454.四数相加II,383. 赎金信 ,15. 三数之和,18. 四数之和
  6. Java UDP 广播、组播使用--系列2-多网卡监听问题
  7. asio非boos版本使用
  8. 把“伞”话“团队”;把“伞”话“发展”
  9. 基于文字情感的民族音乐智能生成项目Bert+Magenta【音乐生成部分】(一)
  10. 企业搜索领域专业名词翻译