title: 从一部电影史上的趣事了解 Spring 中的循环依赖问题

date: 2021-03-10

updated: 2021-03-10

categories:

  • Spring

    tags:
  • Spring

前言

今天,我们从电影史上一则有趣的故事来了解 Spring 中的循环依赖问题。



1998 年的某一天,《喜剧之王》和《玻璃樽》两部电影进入了拍摄阶段。

在《喜剧之王》需要成龙友情客串一个替身演员,而《玻璃樽》需要周星驰客串一个被警犬拖着的警察。

那么,我们想象一下:如果当《喜剧之王》在香港开拍时,《玻璃樽》剧组还在广州,会怎么样?

在现实生活中,我们可能会调整时间安排来解决这种戏份冲突的问题,但在 Spring 对象加载过程中,对象的加载是顺序性的,并不能像我们现实生活中那么灵活。

我们将《喜剧之王》和《玻璃樽》分别看做对象 A 和对象 B,将周星驰和成龙分别看做对象 A 中的 资源 x 和对象 B 中的资源 y。

  • 《喜剧之王》(对象 A)中需要成龙(对象 B 中的资源 y)客串完成。

  • 《玻璃樽》(对象 B)中需要周星驰(对象 A 中的资源 x)客串完成。

也就是说:对象 A 加载时,需要存在对象 B,对象 A 才能顺利加载。而对象 B 的加载也是相同的情况。

但由于对象 A 和对象 B 加载顺序一定是一前一后,所以如果不做一定处理,加载是一定不成功的。这也就是我们所说的循环依赖问题

前置条件

在 Spring 解决循环依赖是有前置条件的:

  1. 出现循环依赖的 Bean 必须是单例
  2. 依赖注入的方式不能全是构造器注入的方式

那么,Spring 如何解决循环依赖问题的呢?这个问题有些抽象,下面举例说明。

测试循环依赖报错问题

测试使用的依赖:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.0</version>
</dependency>
</dependencies>

创建启动类

@SpringBootApplication
public class SpringApplication { public static void main(String[] args) {
org.springframework.boot.SpringApplication.run(SpringApplication.class, args);
}
}

创建以下两个类 A、B,其中 A 依赖 B,B 依赖 A。

@Component
public class A { private final CircularB circularB; public CircularA(CircularB circularB) {
this.circularB = circularB;
}
} @Component
public class B { private final CircularA circularA; public CircularB(CircularA circularA) {
this.circularA = circularA;
}
}

启动应用,发现如下报错。

2021-03-10 20:18:52.637  INFO 38500 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2021-03-10 20:18:52.652 ERROR 38500 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter : ***************************
APPLICATION FAILED TO START
*************************** Description: The dependencies of some of the beans in the application context form a cycle: ┌─────┐
| circularA defined in file [/Users/lihuiming/git/xs/xs-learning/xs-learning-spring/target/classes/com/xs/learning/spring/dependency/CircularA.class]
↑ ↓
| circularB defined in file [/Users/lihuiming/git/xs/xs-learning/xs-learning-spring/target/classes/com/xs/learning/spring/dependency/CircularB.class]
└─────┘

Bean 的创建流程

首先,我们根据源码了解一下 Bean 的创建流程:

  • AbstractBeanFactory#getBean()
  • AbstractBeanFactory#doGetBean(a)
    • DefaultSingletonBeanRegistry#getSingleton(beanName)

      • getSingleton(beanName, true)

        • singletonObjects:一级缓存尝试获取目标对象。存储的是所有创建好了的单例 Bean。

        • earlySingletonObjects:二级缓存尝试获取目标对象。对象完成实例化,但未进行属性注入及初始化的对象。

        • singletonFactories:三级缓存尝试获取目标对象。若获取到对象,将对象从三级缓存中删除,并放入二级缓存。

    • if (sharedInstance != null && args == null)

      • mbd.isSingleton():创建单例 Bean

        • AbstractAutowireCapableBeanFactory#createBean(beanName, mbd, args)

          • doCreateBean(beanName, mbdToUse, args)

            • createBeanInstance(beanName, mbd, args):创建 Bean 实例
            • allowCircularReferences:允许循环引用
            • isSingletonCurrentlyInCreation(beanName):查找 beanName 是否在创建中的集合内。
            • getEarlyBeanReference(beanName, mbd, bean):循环获取二级缓存中的对象引用
            • addSingletonFactory(beanName, singletonFactory):将对象放入一级缓存
        • DefaultSingletonBeanRegistry#getSingleton(beanName, true)
          • beforeSingletonCreation(beanName):判断是否需要跳过检查,以及将 beanName 添加到创建中的集合。
          • afterSingletonCreation(beanName):判断是否需要跳过检查,以及将 beanName 从创建中的集合移除。
        • getObjectForBeanInstance(sharedInstance, name, beanName, mbd):完成单例 Bean 的创建
      • mbd.isPrototype():创建原型 Bean

        • beforePrototypeCreation(beanName)

          • prototypesCurrentlyInCreation.get():获取当前线程的创建对象信息
          • if (curVal == null):若创建对象信息为 null
            • prototypesCurrentlyInCreation.set(beanName):设置当前线程的创建对象信息为 beanName
          • else if (curVal instanceof String):若实例对象为 String 类型
            • beanNameSet.add((String)curVal):将现有对象转为字符串存储
            • beanNameSet.add(beanName):将当前 beanName 追加到集合中
            • prototypesCurrentlyInCreation.set(beanNameSet):,设置当前线程的创建对象信息为集合对象
          • else
            • beanNameSet.add(beanName):在当前线程的创建对象信息中追加 beanName
        • AbstractAutowireCapableBeanFactory#createBean(beanName, mbd, args):与单例 Bean 对应方法一致
        • afterPrototypeCreation(beanName)
          • prototypesCurrentlyInCreation.get():获取当前线程的创建对象信息
          • if (curVal instanceof String):若当前线程的创建对象信息为 String
            • prototypesCurrentlyInCreation.remove():移除当前线程的创建对象信息
          • else if (curVal instanceof Set):若当前线程的创建对象信息为 Set 集合
            • beanNameSet.remove(beanName):移除当前线程的创建对象信息中指定 beanName
            • if (beanNameSet.isEmpty()):若 Set 集合为空
              • prototypesCurrentlyInCreation.remove():移除当前线程的创建对象信息
        • getObjectForBeanInstance(prototypeInstance, name, beanName, mbd):完成原型 Bean 的创建
      • mbd.getScope():根据作用域创建 Bean

        • if (scope == null):找不到对应的 Scope 报错
        • beforePrototypeCreation(beanName):与原型 Bean 对应方法一致
        • AbstractAutowireCapableBeanFactory#createBean(beanName, mbd, args):与单例 Bean 对应方法一致
        • afterPrototypeCreation(beanName):与原型 Bean 对应方法一致
        • scope.get(beanName, objectFactory):获取 Scope 实例
        • getObjectForBeanInstance(scopedInstance, name, beanName, mbd):完成 Scope Bean 的创建

如上所示,这就是一次 Bean 的创建流程。

循环依赖的解决办法

有两种办法:

  1. 将上述测试代码中,先加载的对象(也就是对象 A)改为注解注入的方式。
  2. 将上述测试代码中,将两个对象都改为注解注入的方式。

注意:如果只修改一个对象的注入方式,一定要修改加载顺序靠前的对象,否则无法解决循环依赖问题!

@Component
public class A { @Autowired
private CircularB circularB;
} @Component
public class B { private final CircularA circularA; public CircularB(CircularA circularA) {
this.circularA = circularA;
}
}

循环依赖的运行过程

  1. 首先根据 Spring 自然排序规则,先去获取 A 对象实例,第一次获取会发现缓存中没有 A 实例对象,返回 null;
  2. 由于未获取到 A 对象实例,进行创建 A 对象实例
  3. 创建 A 对象实例时,发现 A 对象依赖 B 对象,循环获取二级缓存中的对象引用,尝试获取 B 对象实例来注入到 A 对象实例中;
  4. 由于缓存中没有 B 对象实例,所以会创建 B 对象实例
  5. 此时,A 对象实例获取得到 B 对象实例(已实例化,但未注入属性信息,未初始化),A 对象实例加载完成;
  6. 创建 B 对象实例时,发现 B 对象依赖 A 对象,获取 A 对象实例来注入到 B 对象实例中;
  7. 此时,B 对象实例加载完成;

从一部电影史上的趣事了解 Spring 中的循环依赖问题的更多相关文章

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

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

  2. 面试阿里,腾讯,字节跳动90%都会被问到的Spring中的循环依赖

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

  3. 【Spring】Spring中的循环依赖及解决

    什么是循环依赖? 就是A对象依赖了B对象,B对象依赖了A对象. 比如: // A依赖了B class A{ public B b; } // B依赖了A class B{ public A a; } ...

  4. Spring中的循环依赖解决详解

    前言 说起Spring中循环依赖的解决办法,相信很多园友们都或多或少的知道一些,但当真的要详细说明的时候,可能又没法一下将它讲清楚.本文就试着尽自己所能,对此做出一个较详细的解读.另,需注意一点,下文 ...

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

    1. 前言 2. 典型场景 3. 什么是依赖 4. 什么是依赖调解 5. 为什么要依赖注入 6. Spring的依赖注入模型 7. 非典型问题 参考资料 1. 前言 这两天工作遇到了一个挺有意思的Sp ...

  6. Spring中解决循环依赖报错的问题

    什么是循环依赖 当一个ClassA依赖于ClassB,然后ClassB又反过来依赖ClassA,这就形成了一个循环依赖: ClassA -> ClassB -> ClassA 原创声明 本 ...

  7. Spring中的循环依赖

    循环依赖 在使用Spring时,如果主要采用基于构造器的依赖注入方式,则可能会遇到循环依赖的情况,简而言之就是Bean A的构造器依赖于Bean B,Bean B的构造器又依赖于Bean A.在这种情 ...

  8. Spring源码-循环依赖源码解读

    Spring源码-循环依赖源码解读 笔者最近无论是看书还是从网上找资料,都没发现对Spring源码是怎么解决循环依赖这一问题的详解,大家都是解释了Spring解决循环依赖的想法(有的解释也不准确,在& ...

  9. Spring 如何解决循环依赖问题?

    在关于Spring的面试中,我们经常会被问到一个问题,就是Spring是如何解决循环依赖的问题的. 这个问题算是关于Spring的一个高频面试题,因为如果不刻意研读,相信即使读过源码,面试者也不一定能 ...

随机推荐

  1. 国产smartbits版本-minismb测试高恪路由器IP限速

    Minismb测试仪表是复刻smartbits的国产版本,是一款专门用于测试智能路由器,网络交换机的性能和稳定性的软硬件相结合的工具.可以通过此工具测试任何ip网络设备的端口吞吐率,带宽,并发连接数和 ...

  2. Java容器--2021面试题系列教程(附答案解析)--大白话解读--JavaPub版本

    Java容器--2021面试题系列教程(附答案解析)--大白话解读--JavaPub版本 前言 序言 再高大上的框架,也需要扎实的基础才能玩转,高频面试问题更是基础中的高频实战要点. 适合阅读人群 J ...

  3. keras fit_generator 并行

    虽然已经走在 torch boy 的路上了, 还是把碰到的这个坑给记录一下 数据量较小时,我们可直接把整个数据集 load 到内存里,用 model.fit() 来拟合模型. 当数据集过大比如几十个 ...

  4. Python——控制鼠标键盘

    一.安装包 pip install pynput 二.引用包 from pynput import mouse,keyboard 三.控制鼠标 from pynput.mouse import But ...

  5. JavaScript 实现 (ECMAScript 6)

    JavaScript 的核心 ECMAScript 描述了该语言的语法和基本对象: DOM 描述了处理网页内容的方法和接口: BOM 描述了与浏览器进行交互的方法和接口. ECMAScript.DOM ...

  6. console.warn All In One

    console.warn All In One ️ FBI 警告 // console 简介 // consoleCtt: function () { // if (window.console &a ...

  7. Object 循环引用 All In One

    Object 循环引用 All In One circular reference bug var a = {}; a.a = a; refs deep copy bug https://segmen ...

  8. ts 遍历Class上的属性和方法

    interface Type<T> extends Function { new (...args: any[]): T; } class Data { name = "ajan ...

  9. postman 发送数组

    原文 users[]:aa users[]:22 object[] // { users: [ { name: ' "ajanuw"', pwd: ' "aaa" ...

  10. 从微信小程序到鸿蒙js开发【08】——表单组件&注册登录模块

    目录: 1.登录模块 2.注册模块 3.系列文章导读 牛年将至,祝大家行行无bug,页页so easy- 在微信小程序中,提供了form组件,可以将input.picker.slider.button ...