【Spring】IOC&DI:循环依赖问题及解决方案_di如何解决循环依赖_A minor的博客-程序员宅基地

技术标签: spring  # Spring  

1.什么是循环依赖?

所谓的循环依赖就是A依赖B,B依赖A;或者是A依赖B,B依赖C,C依赖A

在这里插入图片描述

下面来看个实例:

public class InstanceA {
     
    private InstanceB instanceB; 
    // setter...
} 
public class InstanceB {
      
    private InstanceA instanceA; 
    // setter...
}

这里有两个类 A 和 B,A 中有一个成员变量 B,B 中有一个成员变量 A;然后我们在 xml 中通过构造这两个 bean,并通过 setter 来进行依赖注入:

<bean id="instanceA" class="com.tuling.circulardependencies.InstanceA">   
      <property name="instanceB" ref="intanceB"></property>  
</bean> 
    
<bean id="intanceB" class="com.tuling.circulardependencies.InstanceB">     
    <property name="instanceA" ref="instanceA"></property>  
</bean>

可能存在的问题:

IOC容器在创建Bean的时候,按照顺序,先去实例化instanceA。然后突然发现我的instanceA是依赖我的instanceB的。那么IOC容器接着去实例化intanceB,那么在intanceB的时候发现依赖instanceA。若容器不处理的, 那么IOC将无限的执行上述流程。直到内存异常程序奔溃

解决思路:

当然,Spring 是不会让这种情况发生的。在容器发现 beanB 依赖于 beanA 时,容器会获取 beanA 对象的一个早期的引用(early reference),并把这个早期引用注入到 beanB 中,让 beanB 先完成实例化。beanB 完成实例化,beanA 就可以获取到 beanB 的引用,beanA 随之完成实例化。

这里解释一下上面说的早期引用,实际就是说在 bean 完成初始化之前被引用

在这里插入图片描述

注:循环依赖在实际应用中也有,但不会太多,简单的应用场景是: controller注入service,service注入mapper,只有复杂的业务,可能service互相引用,有可能出现循环依赖

那循环依赖该怎么解决呢?

2.循环依赖解决方案

Spring 在启动过程中,使用到了三个 map,称为三级缓存

// 一级缓存:用于存放 beanName 和初始化好的 bean 对象(属性已经初始化好的)  
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256); 

 // 二级缓存:用于存放 beanName 和一个原始 bean 早期 bean(属性未初始化)  
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16); 
 
// 三级缓存:存放 bean 工厂对象,用于解决 AOP 需要代理对象的循环依赖问题
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16); 

一级缓存就不用说了,缓存单例 bean 的。我们下面就来看看二级和三级缓存是如何解决循环依赖的?

首先,我们要清楚的是,在调用 getBean() 后的执行逻辑是

  1. 创建 bean 实例 createBeanInstance()
  2. 对 bean 进行依赖注入 populateBean()(循环依赖就出现在这里)
  3. 对 bean 进行初始化 initializeBean()

2.1 二级缓存

所以,我们解决循环依赖的思路可以是,除了单例池(一级缓存)之外,再创建一个半成品池(二级缓存,earlySingletonObjects),即对象一创建就添加到半成品池

  1. 对象 A 创建之后直接就放入半成品池
  2. 对象 B 创建后,直接从半成品池中拿出 A 对象,然后注入到 B 中
  3. 将注入后的 B 放入单例池
  4. 将单例池中的 B 注入到对象 A 中
  5. 将 A 添加到单例池,并移除半成品池中的 A

但是,问题来了如果 A 使用了 AOP,那么 B 要注入的对象就是 A 的代理对象,而不是 A 本身;所以,单例池以及半成品池,应该存放的是 bean 的代理对象,而不是 bean 本身;那该怎么办呢?

2.2 三级缓存

首先,我们来看看代理对象的创建的时机。

代理对象在 AbstractAutowireCapableBeanFactory#doCreateBean() 方法中创建的,具体调用逻辑是 initializeBean() -> BeanPostProcessor#postProcessAfterInitialization() -> createProxy()。

那么问题来了,入口 initializeBean() 都在依赖注入 populateBean() 之后了,那依赖注入时就根本拿不到代理对象啊…

所以,为了解决循环以依赖的问题,在 doCreateBean() 方法中,createBeanInstance() 之后,populateBean() 之前添加了一个工厂池(三级缓存,singletonFactories),这个工厂池里面缓存了许多 bean 的 FactoryBean,这些工厂不仅可以暴露早期 bean 还可以暴露代理 bean

在这里插入图片描述

  1. 对象 A 创建
  2. 由于 B 要提前引用 A,所以通过工厂池中 A 的 FactoryBean#getObject 创建 A 的代理对象,并将该代理对象放入半成品池
  3. B 从半成品池中获取到 A 的代理对象,并完成依赖注入
  4. 将注入后的 B 放入单例池
  5. 将单例池中的 B 注入到对象 A 中
  6. 将半成品池中 A 的代理对象移到单例池中
  7. 将工厂池中的 A 移除

注:此时对于 A 来说,就没要再进行 BeanPostProcessor 后置处理了,因为已经提前创建过代理对象了

可能有同学要问了,既然三级缓存也能够获取到早期 bean,那就已经能解决循环依赖了,为什么还需要一个二级缓存?

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    
   // 检查缓存中是否有初始化好的bean 
   Object singletonObject = this.singletonObjects.get(beanName);
   // 判断 beanName 对应的 bean 是否正在创建中 
   if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
    
       // 加锁,防止多线程并发创建 
      synchronized (this.singletonObjects) {
    
         // 从 earlySingletonObjects 中获取提前曝光的 bean 
         singletonObject = this.earlySingletonObjects.get(beanName);
         // 没拿到
         if (singletonObject == null && allowEarlyReference) {
    
            // 获取相应的 bean 工厂 
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
    
               // 提前曝光 bean 实例(raw bean)也是早期bean ,用于解决循环依赖 
               singletonObject = singletonFactory.getObject();
               // 将 singletonObject 放入缓存中,并将 singletonFactory 从缓存中移除 
               this.earlySingletonObjects.put(beanName, singletonObject);
               this.singletonFactories.remove(beanName);
            }
         }
      }
   }
   return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

可以从源码中看到,只有当二级缓存 earlySingletonObjects 获取不到提前曝光的 bean 时(主要出现于获取代理对象),才会走到三级缓存 singletonFactories 执行工厂的 getObject() 生成(获取)早期依赖,生成后就将早期对象放入二级缓存,并且删除当前 FactoryBean。

所以,二级缓存 earlySingletonObjects 更注重的是存储,而三级缓存 singletonFactories 更注重的是生产。

再放一个很好的视频参考链接(B站):https://www.bilibili.com/video/BV1ET4y1N7Sp?p=1

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_43935927/article/details/115275080

智能推荐

Python学习资料汇总_python复习资料-程序员宅基地

在学习python的时候,记录了一些经典的网站以及经典帖子。1. python性能优化技巧 http://www.ibm.com/developerworks/cn/linux/l-cn-python-optim/#resources2.python官方文档http://docs.python.org/tutorial/index.html3.python天天美味htt_python复习资料

I - Fire Game_k - fire game-程序员宅基地

题目大意 一块地上面有草和空地,有两个人想要把草烧光,这样他俩就可以开心的OOXX,他俩都要在一块地上放火(每人只能放一次),可以在相同或者不同的位置放,火可以向四周蔓延,蔓延一次话费1分钟,问他俩把这块地上草烧完所用到的最小时间。解题思路 这个思路是我看一个大神的,感觉很好,就是将两人所能放火的位置都试一遍,比较得出所用最短的时间,具体用的是一个结构体数组将所有有草的位置全都记录下来,然_k - fire game

php 上传大文件原理,PHP文件上传进度的实现原理_明明如灼的博客-程序员宅基地

在PHP5.4之前,如果我们要获取文件上传的进度,可以选择的方案有Flash或使用PHP的uploadprogress扩展。这两种方案存在本质的区别,Flash的上传进度是客户端上传的进度,它是基于本地OS的网络传输,最终其本质上也是一次HTTP的multipart/form-data编码的POST请求;uploadprogress扩展需要依靠JS获取服务器提供的进度,这里的进度是服务器接收的文件..._获取上传接口的进度的原理

SpringMVC(八)使用@Validated注解实现后台表单校验-程序员宅基地

依赖jar <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <v...

zend studio for Eclipse 6.1.2安装汉化-程序员宅基地

最终编辑 kahn178zend studio for Eclipse 6.1.2官方下载地址适用于Mac_OS_X的DMG安装格式:http://downloads.zend.com/studio-eclipse/6.1.2/ZendStudioForEclipse-6.1.2.dmg适用于windows的ZIP安装格式:http://downlo

SSM——Spring+Mybtis整合(代理【mapper】开发模式)-程序员宅基地

1. 项目搭建2. 导入项目整合jar包 mybatis-spring-1.2.4.jar commons-dbcp2-2.1.1.jar commons-pool2-2.4.2.jar 3. 在applicationContex.xml配置数据源dataSource、配置SqlSessionFactory、配置SqlSessionTemplate(可省略) <?xml ver...

随便推点

菜鸟学习笔记:Java提升篇6(IO流2——数据类型处理流、打印流、随机流)_io流在emp项目上重新创建employeedaoimpl3类,必须完成如下前3个功能 a.数据存储-程序员宅基地

菜鸟学习笔记:Java IO流2——其他流字节数组输入输出流数据类型处理流基本数据类型引用类型打印流上一节讲解的是我们工作中常用的流,需要大家重点掌握,除此之外Java中还有一些流需要大家了解。字节数组输入输出流ByteArrayInputStream和ByteArrayOutputStream是字节数组的输入输出流,操作与正常输入输出流一致,只是接收的是一个字节数组,我们直接看代码: public static void main(String[] args) throws IOException_io流在emp项目上重新创建employeedaoimpl3类,必须完成如下前3个功能 a.数据存储在

(一)unity中的渲染优化技术——————(影响性能的因素、unity中的渲染分析工具)_unity saved by batching-程序员宅基地

这个专栏我们会阐述一些unity中常见的优化技术,这些技术基本都是和渲染相关的,例如使用批处理、LOD(Level of Detail)技术等。游戏优化不仅是程序员的工作,更需要美工人员在游戏的美术上进行一定的权衡,例如避免使用全屏的屏幕特效,避免使用计算复杂的shader,减少透明混合造成的overdraw等。这是由程序和美工人员等各部分人员共同参与的工作。一、移动平台的特点和PC平..._unity saved by batching

display:inline-block 引发的灵异事件_display:inline-block click事件-程序员宅基地

以下源码来源于张鑫旭的博客:博客原链接以及样式https://www.zhangxinxu.com/study/201708/percent-padding-auto-layout.htmlcopy下来格式化后的代码如下:<!DOCTYPE html><html><head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> &l.._display:inline-block click事件

安装 GraphicsMagick-程序员宅基地

yum -y install GraphicsMagick GraphicsMagick-devel实际试了试,上面yum的方式不好使,下面是我实际安装过程:1、下载最新版 wgetftp://ftp.graphicsmagick.org/pub/GraphicsMagick/GraphicsMagick-LATEST.tar.gz2、解压 tar -xzvf Graphi...

MyBatis相关日志配置_mybatis 日志配置-程序员宅基地

MyBatis相关日志配置什么是MyBatis相关的日志?首先什么叫做与MyBatis相关的日志呢?就是我们在执行sql语句的时候,如果没有MyBatis相关的日志,我们不知道我们实际执行的sql语句长什么样,但是有了MyBatis日志,我们就可以知道我们实际执行的sql语句具体是长什么样子的。配置MyBatis相关的日志第一步引入依赖,如下:<dependency> <groupId>org.slf4j</groupId> _mybatis 日志配置

Android Studio 离线安装 Gradle 的方法-程序员宅基地

Android Studio 在新建工程时,需要匹配的 Gradle 进行相关配置。如果正好你的系统中不存在匹配的 Gradle,新工程一个星期都不一定能创建成功。呵呵,药方子来了(前提是你已经至少尝试了一次在线安装):1. http://services.gradle.org/distributions 选择匹配的 Gradle 版本(下面以1.7为例),自己下载吧(容易断,找个离线