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 对象实例加载完成;

最新文章

  1. CentOS 7.0安装配置Vsftp服务器
  2. Win10 Migrate apps to the Universal Windows Platform (UWP)
  3. qt-creator astyle Peizhi
  4. nodejs初探(四)实现一个多人聊天室
  5. 新公司入职第一天遇到的 关于 CSS 单行溢出文本显示省略号...的问题
  6. 高性能JavaScript-JS脚本加载与执行对性能的影响
  7. List应用举例
  8. oracle学习总结
  9. android开发之bitmap使用
  10. logstash indexer和shipper的配置
  11. G - Self Numbers(2.2.1)
  12. C# WinForm开发系列 - WebBrowser
  13. vs2012中程序集生成无法自动在网站Bin目录下生成Dll文件?(已解决!)
  14. 每天一个JS 小demo之商品筛选。主要知识点:DOM方法综合运用
  15. IDEA中运行DirectKafkaWordCount程序
  16. js 简单弹框toast
  17. KVO实现原理
  18. 关于ArcMap中打开ArcToolbox导致闪退的解决办法
  19. pyglet and opengl -- 纹理映射以及动画
  20. 9个绚丽多彩的HTML5进度条动画赏析

热门文章

  1. 获取csc.exe路径
  2. 揭秘井井有条的流水线(ZooKeeper 原理篇)
  3. 2.API的理解和使用
  4. Java之先行发生原则与volatile关键字详解
  5. hdu 5874
  6. (转载)RTMP协议中的AMF数据 http://blog.csdn.net/yeyumin89/article/details/7932585
  7. Mybatis-02 CRUD
  8. 蓝桥杯——试题 算法训练 Sereja and Squares
  9. 微前端 &amp; 微前端实践 &amp; 微前端教程
  10. SameSite cookies explained