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


相关文章
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
87 2
|
2月前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
2月前
|
缓存 架构师 Java
图解 Spring 循环依赖,一文吃透!
Spring 循环依赖如何解决,是大厂面试高频,本文详细解析,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
图解 Spring 循环依赖,一文吃透!
|
1月前
|
存储 缓存 Java
Spring面试必问:手写Spring IoC 循环依赖底层源码剖析
在Spring框架中,IoC(Inversion of Control,控制反转)是一个核心概念,它允许容器管理对象的生命周期和依赖关系。然而,在实际应用中,我们可能会遇到对象间的循环依赖问题。本文将深入探讨Spring如何解决IoC中的循环依赖问题,并通过手写源码的方式,让你对其底层原理有一个全新的认识。
54 2
|
2月前
|
前端开发 Java 开发者
Spring生态学习路径与源码深度探讨
【11月更文挑战第13天】Spring框架作为Java企业级开发中的核心框架,其丰富的生态系统和强大的功能吸引了无数开发者的关注。学习Spring生态不仅仅是掌握Spring Framework本身,更需要深入理解其周边组件和工具,以及源码的底层实现逻辑。本文将从Spring生态的学习路径入手,详细探讨如何系统地学习Spring,并深入解析各个重点的底层实现逻辑。
71 9
|
3月前
|
前端开发 Java 数据库
SpringBoot学习
【10月更文挑战第7天】Spring学习
45 9
|
2月前
|
Java Kotlin 索引
学习Spring框架特性及jiar包下载
Spring 5作为最新版本,更新了JDK基线至8,修订了核心框架,增强了反射和接口功能,支持响应式编程及Kotlin语言,引入了函数式Web框架,并提升了测试功能。Spring框架可在其官网下载,包括文档、jar包和XML Schema文档,适用于Java SE和Java EE项目。
36 0
|
3月前
|
XML Java 数据格式
Spring学习
【10月更文挑战第6天】Spring学习
29 1
|
3月前
|
Java 测试技术 开发者
springboot学习四:Spring Boot profile多环境配置、devtools热部署
这篇文章主要介绍了如何在Spring Boot中进行多环境配置以及如何整合DevTools实现热部署,以提高开发效率。
116 2
|
3月前
|
前端开发 Java 程序员
springboot 学习十五:Spring Boot 优雅的集成Swagger2、Knife4j
这篇文章是关于如何在Spring Boot项目中集成Swagger2和Knife4j来生成和美化API接口文档的详细教程。
299 1