Spring 源码学习(五) 循环依赖(一)

简介: 还记得上一篇笔记,在 bean 加载流程,在创建过程中,出现了依赖循环的监测,如果出现了这个循环依赖,而没有解决的话,代码中将会报错,然后 Spring 容器初始化失败。由于感觉循环依赖是个比较独立的知识点,所以我将它的分析单独写一篇笔记,来看下什么是循环依赖和如何解决它。
  • 前言
  • 循环依赖
  • 构造器循环依赖
  • property 范围的依赖处理
  • setter 循环依赖
  • 代码分析
  • 解决场景
  • 结合关键代码梳理流程
  • 创建原始 bean
  • addSingleFactory
  • populateBean 填充属性
  • getSingleton
  • 总结
  • 参考资料

循环依赖

循环依赖就是循环引用,就是两个或者多个 bean 相互之间的持有对方,最后形成一个环。例如 A 引用了 BB 引用了 CC 引用了 A

可以参照下图理解(图中展示的类的互相依赖,但循环调用指的是方法之间的环调用,下面代码例子会展示方法环调用):

50.png

如果学过数据库的同学,可以将循环依赖简单的理解为死锁,互相持有对方的资源,形成一个环,然后不释放资源,导致死锁发生。

在循环调用中,除非出现终结条件,否则将会无限循环,最后导致内存溢出错误。(我也遇到过一次 OOM,也是无限循环导致的)


书中的例子是用了三个类进行环调用,我为了简单理解和演示,使用了两个类进行环调用:

Spring 中,循环依赖分为以下三种情况:

构造器循环依赖

51.jpg

通过上图的配置方法,在初始化的时候就会抛出 BeanCurrentlyInCreationException 异常

  1. publicstaticvoid main(String[] args){
  2.    // 报错原因: Requested bean is currently in creation: Is there an unresolvable circular reference?
  3.    ApplicationContext context =newClassPathXmlApplicationContext("circle/circle.xml");
  4. }

从上一篇笔记中知道, Spring 容器将每一个正在创建的 bean 标识符放入一个 “当前创建 bean 池( prototypesCurrentlyInCreation)” 中, bean 标识符在创建过程中将一直保持在这个池中。

检测循环依赖的方法:

分析上面的例子,在实例化 circleA 时,将自己 A 放入池中,由于依赖了 circleB,于是去实例化 circleBB 也放入池中,由于依赖了 A,接着想要实例化 A,发现在创建 bean 过程中发现自己已经在 “当前创建 bean” 里时,于是就会抛出 BeanCurrentlyInCreationException 异常。

如图中展示,这种通过构造器注入的循环依赖,是无法解决的


property 范围的依赖处理

property 原型属于一种作用域,所以首先来了解一下作用域 scope 的概念:

Spring 容器中,在Spring容器中是指其创建的 Bean 对象相对于其他 Bean 对象的请求可见范围

我们最常用到的是单例 singleton 作用域的 beanSpring 容器中只会存在一个共享的 Bean 实例,所以我们每次获取同样 id 时,只会返回bean的同一实例。

使用单例的好处有两个:

  1. 提前实例化 bean,将有问题的配置问题提前暴露
  2. bean 实例放入单例缓存 singletonFactories 中,当需要再次使用时,直接从缓存中取,加快了运行效率。

单一实例会被存储在单例缓存 singletonFactories 中,为Spring的缺省作用域.

看完了单例作用域,来看下 property 作用域的概念:在 Spring 调用原型 bean 时,每次返回的都是一个新对象,相当于 newObject()

因为 Spring 容器对原型作用域的 bean 是不进行缓存,因此无法提前暴露一个创建中的 bean,所以也是无法解决这种情况的循环依赖。


setter 循环依赖

对于 setter 注入造成的依赖可以通过 Spring 容器提前暴露刚完成构造器注入但未完成其他步骤(如 setter 注入)的 bean 来完成,而且只能解决单例作用域的 bean 依赖。

在类的加载中,核心方法 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean,在这一步中有对循环依赖的校验和处理。

跟进去方法能够发现,如果 bean 是单例,并且允许循环依赖,那么可以通过提前暴露一个单例工厂方法,从而使其他 bean 能引用到,最终解决循环依赖的问题。

还是按照上面新建的两个类, CircleACircleB,来讲下 setter 解决方法:

配置:

  1. <!--注释 5.3 setter 方法注入-->
  2. <beanid="circleA"class="base.circle.CircleA">
  3.    <propertyname="circleB"ref="circleB"/>
  4. </bean>

  5. <beanid="circleB"class="base.circle.CircleB">
  6.    <propertyname="circleA"ref="circleA"/>
  7. </bean>

执行 Demo 和输出:

  1. publicstaticvoid main(String[] args){
  2.    ApplicationContext context =newClassPathXmlApplicationContext("circle/circle.xml");
  3.    CircleA circleA =(CircleA) context.getBean("circleA");
  4.    circleA.a();
  5. }

  6. a 方法中,输出 A,在 b 方法中,输出B,下面是执行 demo 输出的结果:
  7. 错误提示是因为两个方法互相调用进行输出,然后打印到一定行数提示 main 函数栈溢出了=-=

  8. A
  9. B
  10. A
  11. B
  12. *** java.lang.instrument ASSERTION FAILED ***:"!errorOutstanding" with message transform method call failed at JPLISAgent.c line:844
  13. *** java.lang.instrument ASSERTION FAILED ***:"!errorOutstanding" with message transform method call failed at JPLISAgent.c line:844
  14. *** java.lang.instrument ASSERTION FAILED ***:"!errorOutstanding" with message transform method call failed at JPLISAgent.c line:844
  15. Exception in thread "main" java.lang.StackOverflowError

可以看到通过 setter 注入,成功解决了循环依赖的问题,那解决的具体代码是如何实现的呢,下面来分析一下:


代码分析

为了更好的理解循环依赖,首先来看下这三个变量(也叫缓存,可以全局调用的)的含义和用途:

  1. /** Cache of singleton objects: bean name to bean instance. */
  2. privatefinalMap<String,Object> singletonObjects =newConcurrentHashMap<>(256);

  3. /** Cache of singleton factories: bean name to ObjectFactory. */
  4. privatefinalMap<String,ObjectFactory<?>> singletonFactories =newHashMap<>(16);

  5. /** Cache of early singleton objects: bean name to bean instance. */
  6. privatefinalMap<String,Object> earlySingletonObjects =newHashMap<>(16);
变量 用途
singletonObjects 用于保存 BeanName 和创建 bean 实例之间的关系, bean-name --> instanct
singletonFactories 用于保存 BeanName 和创建 bean工厂 之间的关系, bean-name --> objectFactory
earlySingletonObjects 也是保存 beanName 和创建 bean 实例之间的关系,与 singletonObjects不同之处在于,当一个单例 bean 被放入到这里之后,那么其他 bean在创建过程中,就能通过 getBean 方法获取到,目的是用来检测循环引用

之前讲过类加载的机制了,下面定位到创建 bean 时,解决循环依赖的地方:

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean

  1. // 是否需要提前曝光,用来解决循环依赖时使用
  2. boolean earlySingletonExposure =(mbd.isSingleton()&&this.allowCircularReferences &&
  3.        isSingletonCurrentlyInCreation(beanName));
  4. if(earlySingletonExposure){
  5.    if(logger.isTraceEnabled()){
  6.        logger.trace("Eagerly caching bean '"+ beanName +
  7.                "' to allow for resolving potential circular references");
  8.    }
  9.    // 注释 5.2 解决循环依赖 第二个参数是回调接口,实现的功能是将切面动态织入 bean
  10.    addSingletonFactory(beanName,()-> getEarlyBeanReference(beanName, mbd, bean));
  11. }

  12. protectedvoid addSingletonFactory(String beanName,ObjectFactory<?> singletonFactory){
  13. Assert.notNull(singletonFactory,"Singleton factory must not be null");
  14.    synchronized(this.singletonObjects){
  15.        // 判断 singletonObjects 不存在 beanName
  16.        if(!this.singletonObjects.containsKey(beanName)){
  17.        // 注释 5.4 放入 beanName -> beanFactory,到时在 getSingleton() 获取单例时,可直接获取创建对应 bean 的工厂,解决循环依赖
  18.        this.singletonFactories.put(beanName, singletonFactory);
  19.        // 从提前曝光的缓存中移除,之前在 getSingleton() 放入的
  20.        this.earlySingletonObjects.remove(beanName);
  21.        // 往注册缓存中添加 beanName
  22.        this.registeredSingletons.add(beanName);
  23.    }
  24. }
  25. }

先来看 earlySingletonExposure 这个变量: 从字面意思理解就是需要提前曝光的单例

有以下三个判断条件:

  • mbd 是否是单例
  • 该容器是否允许循环依赖
  • 判断该 bean 是否在创建中。

如果这三个条件都满足的话,就会执行 addSingletonFactory 操作。要想着,写的代码都有用处,所以接下来看下这个操作解决的什么问题和在哪里使用到吧


解决场景

用一开始创建的 CircleACircleB 这两个循环引用的类作为例子:

52.jpg

A 类中含有属性 BB 类中含有属性 A,这两个类在初始化的时候经历了以下的步骤:

  1. 创建 beanA,先记录对应的 beanName 然后将 beanA创建工厂 beanFactoryA 放入缓存中
  2. beanA 的属性填充方法 populateBean,检查到依赖 beanB,缓存中没有 beanB 的实例或者单例缓存,于是要去实例化 beanB
  3. 开始实例化 beanB,经历创建 beanA 的过程,到了属性填充方法,检查到依赖了 beanA
  4. 调用 getBean(A) 方法,在这个函数中,不是真正去实例化 beanA,而是先去检测缓存中是否有已经创建好的对应的 bean,或者已经创建好的 beanFactory
  5. 检测到 beanFactoryA 已经创建好了,而是直接调用 ObjectFactory 去创建 beanA


相关文章
|
1天前
|
JavaScript Java Maven
理解固化的Maven依赖:spring-boot-starter-parent 与 spring-boot-dependencies
理解固化的Maven依赖:spring-boot-starter-parent 与 spring-boot-dependencies
6 1
|
3天前
|
消息中间件 安全 Java
学习认识Spring Boot Starter
在SpringBoot项目中,经常能够在pom文件中看到以spring-boot-starter-xx或xx-spring-boot-starter命名的一些依赖。例如:spring-boot-starter-web、spring-boot-starter-security、spring-boot-starter-data-jpa、mybatis-spring-boot-starter等等。
17 4
|
4天前
|
XML 安全 Java
Spring 基础知识学习
Spring 基础知识学习
|
4天前
|
Java Spring 容器
Spring5系列学习文章分享---第六篇(框架新功能系列+整合日志+ @Nullable注解 + JUnit5整合)
Spring5系列学习文章分享---第六篇(框架新功能系列+整合日志+ @Nullable注解 + JUnit5整合)
5 0
|
4天前
|
XML Java 数据库
Spring5系列学习文章分享---第五篇(事务概念+特性+案例+注解声明式事务管理+参数详解 )
Spring5系列学习文章分享---第五篇(事务概念+特性+案例+注解声明式事务管理+参数详解 )
6 0
|
4天前
|
SQL Java 数据库连接
Spring5系列学习文章分享---第四篇(JdbcTemplate+概念配置+增删改查数据+批量操作 )
Spring5系列学习文章分享---第四篇(JdbcTemplate+概念配置+增删改查数据+批量操作 )
6 0
|
4天前
|
XML Java 数据格式
Spring5系列学习文章分享---第三篇(AOP概念+原理+动态代理+术语+Aspect+操作案例(注解与配置方式))
Spring5系列学习文章分享---第三篇(AOP概念+原理+动态代理+术语+Aspect+操作案例(注解与配置方式))
7 0
|
4天前
|
XML druid Java
Spring5系列学习文章分享---第二篇(IOC的bean管理factory+Bean作用域与生命周期+自动装配+基于注解管理+外部属性管理之druid)
Spring5系列学习文章分享---第二篇(IOC的bean管理factory+Bean作用域与生命周期+自动装配+基于注解管理+外部属性管理之druid)
8 0
|
4天前
|
XML Java 数据格式
Spring5系列学习文章分享---第一篇(概述+特点+IOC原理+IOC并操作之bean的XML管理操作)
Spring5系列学习文章分享---第一篇(概述+特点+IOC原理+IOC并操作之bean的XML管理操作)
13 1
|
4天前
|
Java 程序员 Spring
Spring 源码阅读 一
Spring 源码阅读 一