从一部电影史上的趣事了解 Spring 中的循环依赖问题
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 解决循环依赖是有前置条件的:
- 出现循环依赖的 Bean 必须是单例
- 依赖注入的方式不能全是构造器注入的方式
那么,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):将对象放入一级缓存
- doCreateBean(beanName, mbdToUse, args)
- DefaultSingletonBeanRegistry#getSingleton(beanName, true)
- beforeSingletonCreation(beanName):判断是否需要跳过检查,以及将 beanName 添加到创建中的集合。
- afterSingletonCreation(beanName):判断是否需要跳过检查,以及将 beanName 从创建中的集合移除。
- getObjectForBeanInstance(sharedInstance, name, beanName, mbd):完成单例 Bean 的创建
- AbstractAutowireCapableBeanFactory#createBean(beanName, mbd, args)
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 的创建
- beforePrototypeCreation(beanName)
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 的创建流程。
循环依赖的解决办法
有两种办法:
- 将上述测试代码中,先加载的对象(也就是对象 A)改为注解注入的方式。
- 将上述测试代码中,将两个对象都改为注解注入的方式。
注意:如果只修改一个对象的注入方式,一定要修改加载顺序靠前的对象,否则无法解决循环依赖问题!
@Component
public class A {
@Autowired
private CircularB circularB;
}
@Component
public class B {
private final CircularA circularA;
public CircularB(CircularA circularA) {
this.circularA = circularA;
}
}
循环依赖的运行过程
- 首先根据 Spring 自然排序规则,先去获取 A 对象实例,第一次获取会发现缓存中没有 A 实例对象,返回 null;
- 由于未获取到 A 对象实例,进行创建 A 对象实例;
- 创建 A 对象实例时,发现 A 对象依赖 B 对象,循环获取二级缓存中的对象引用,尝试获取 B 对象实例来注入到 A 对象实例中;
- 由于缓存中没有 B 对象实例,所以会创建 B 对象实例;
- 此时,A 对象实例获取得到 B 对象实例(已实例化,但未注入属性信息,未初始化),A 对象实例加载完成;
- 创建 B 对象实例时,发现 B 对象依赖 A 对象,获取 A 对象实例来注入到 B 对象实例中;
- 此时,B 对象实例加载完成;
从一部电影史上的趣事了解 Spring 中的循环依赖问题的更多相关文章
- 面试必杀技,讲一讲Spring中的循环依赖
本系列文章: 听说你还没学Spring就被源码编译劝退了?30+张图带你玩转Spring编译 读源码,我们可以从第一行读起 你知道Spring是怎么解析配置类的吗? 配置类为什么要添加@Configu ...
- 面试阿里,腾讯,字节跳动90%都会被问到的Spring中的循环依赖
前言 Spring中的循环依赖一直是Spring中一个很重要的话题,一方面是因为源码中为了解决循环依赖做了很多处理,另外一方面是因为面试的时候,如果问到Spring中比较高阶的问题,那么循环依赖必定逃 ...
- 【Spring】Spring中的循环依赖及解决
什么是循环依赖? 就是A对象依赖了B对象,B对象依赖了A对象. 比如: // A依赖了B class A{ public B b; } // B依赖了A class B{ public A a; } ...
- Spring中的循环依赖解决详解
前言 说起Spring中循环依赖的解决办法,相信很多园友们都或多或少的知道一些,但当真的要详细说明的时候,可能又没法一下将它讲清楚.本文就试着尽自己所能,对此做出一个较详细的解读.另,需注意一点,下文 ...
- 一起来踩踩 Spring 中这个循环依赖的坑
1. 前言 2. 典型场景 3. 什么是依赖 4. 什么是依赖调解 5. 为什么要依赖注入 6. Spring的依赖注入模型 7. 非典型问题 参考资料 1. 前言 这两天工作遇到了一个挺有意思的Sp ...
- Spring中解决循环依赖报错的问题
什么是循环依赖 当一个ClassA依赖于ClassB,然后ClassB又反过来依赖ClassA,这就形成了一个循环依赖: ClassA -> ClassB -> ClassA 原创声明 本 ...
- Spring中的循环依赖
循环依赖 在使用Spring时,如果主要采用基于构造器的依赖注入方式,则可能会遇到循环依赖的情况,简而言之就是Bean A的构造器依赖于Bean B,Bean B的构造器又依赖于Bean A.在这种情 ...
- Spring源码-循环依赖源码解读
Spring源码-循环依赖源码解读 笔者最近无论是看书还是从网上找资料,都没发现对Spring源码是怎么解决循环依赖这一问题的详解,大家都是解释了Spring解决循环依赖的想法(有的解释也不准确,在& ...
- Spring 如何解决循环依赖问题?
在关于Spring的面试中,我们经常会被问到一个问题,就是Spring是如何解决循环依赖的问题的. 这个问题算是关于Spring的一个高频面试题,因为如果不刻意研读,相信即使读过源码,面试者也不一定能 ...
随机推荐
- DSC注册Agent失败- InternalServerError
问题 有大概5台Agent Server,注册的时候,发现2台可以成功,其他的不成功. 注册失败的错误日志如下: 初步尝试 首先,Pull Server已经平稳的运行了几年了,此次注册还有部分Agen ...
- 三、Jmeter 定时器
首先需要清楚Jmeter中各个元件的执行顺序: 元件的执行顺序 了解了元件有作用域之后,来看看元件的执行顺序,元件执行顺序的规则很简单,在同一作用域名范围内,测试计划中的元件按照如下顺序执行. (1) ...
- CF1463-D. Pairs
CF1463-D. Pairs 题意: 有从\(1\)到\(2n\)一共\(2n\)个数字,让你将这\(2n\)个数字分成\(n\)组,每组有两个数字.对于这\(n\)组数字,你可以从中挑选\(x\) ...
- 数学知识-欧拉函数&快速幂
欧拉函数 定义 对于正整数n,欧拉函数是小于或等于n的正整数中与n互质的数的数目,记作φ(n). 算法思路 既然求解每个数的欧拉函数,都需要知道他的质因子,而不需要个数 因此,我们只需求出他的质因子, ...
- Prometheus监控k8s企业级应用
Prometheus架构图 常见的镜像 pod 备注 kube-state-metric 用来收集K8S基本状态信息的监控代理 node-exporter 专门用来收集K8S运算节点基础信息,需要部署 ...
- 苹果证书p12和描述文件的创建方法
在2020年之前,我们在使用香蕉云编创建苹果证书的时候,只需要注册苹果开发者账号,但不需要缴费成为开发者. 在2020年之后,需要先缴费成为苹果开发者. 假如你还没有注册苹果开发者账号,可以先参考下下 ...
- 三、mysql主从复制
1 MySQL 主从复制 1.1 主从复制的含义 在 MySQL 多服务器的架构中,至少要有一个主节点(master),跟主节点相对的,我们把它叫做从节点(slave). 主从复制,就是把主节点的数据 ...
- ZOJ 2563 Long Dominoes(状压DP)题解
题意:n*m的格子,用1 * 3的矩形正好填满它,矩形不能重叠,问有几种填法 思路:poj2411进阶版.我们可以知道,当连续两行的摆法确定,那么接下来的一行也确定.当第一行还有空时,这时第三行必须要 ...
- Firewall & Network Security
Firewall & Network Security 防火墙 & 网络安全 NAT Gateway VPC Virtual Private Cloud refs https://en ...
- HTMLMediaElement.srcObject & URL.createObjectURL & HTMLMediaElement.src
HTMLMediaElement.srcObject & URL.createObjectURL & HTMLMediaElement.src Uncaught TypeError: ...