Spring 的循环依赖问题
什么是循环依赖
什么是循环依赖呢?可以把它拆分成循环和依赖两个部分来看,循环是指计算机领域中的循环,执行流程形成闭合回路;依赖就是完成这个动作的前提准备条件,和我们平常说的依赖大体上含义一致。放到 Spring 中来看就一个或多个 Bean 实例之间存在直接或间接的依赖关系,构成循环调用,循环依赖可以分为直接循环依赖和间接循环依赖,直接循环依赖的简单依赖场景:Bean A 依赖于 Bean B,然后 Bean B 又反过来依赖于 Bean A(Bean A -> Bean B -> Bean A),间接循环依赖的一个依赖场景:Bean A 依赖于 Bean B,Bean B 依赖于 Bean C,Bean C 依赖于 Bean A,中间多了一层,但是最终还是形成循环(Bean A -> Bean B -> Bean C -> Bean A)。
循环依赖的类型
第一种是自依赖,自己依赖自己从而形成循环依赖,一般情况下不会发生这种循环依赖,因为它很容易被我们发现。
第二种是直接依赖,发生在两个对象之间,比如:Bean A 依赖于 Bean B,然后 Bean B 又反过来依赖于 Bean A,如果比较细心的话肉眼也不难发现。
第三种是间接依赖,这种依赖类型发生在 3 个或者以上的对象依赖的场景,间接依赖最简单的场景:Bean A 依赖于 Bean B,Bean B 依赖于 Bean C,Bean C 依赖于 Bean A,可以想象当中间依赖的对象很多时,是很难发现这种循环依赖的,一般都是借助一些工具排查。
Spring 对几种循环依赖场景支持情况
在介绍 Spring 对几种循环依赖场景的处理方式之前,先来看看在 Spring 中循环依赖会有哪些场景,大部分常见的场景总结如下图所示:
有句话说得好,源码之下无秘密,下面就通过源码探究这些场景 Spring 是否支持,以及支持的原因或者不支持的原因,话不多说,下面进入正题。
第 ① 种场景——单例 Bean 的 setter 注入
这种使用方式也是最常用的方式之一,假设有两个 Service 分别为 OrderService(订单相关业务逻辑)和 TradeService(交易相关业务逻辑),代码如下:
/**
* @author mghio
* @since 2021-07-17
*/
@Service
public class OrderService {
@Autowired
private TradeService tradeService;
public void testCreateOrder() {
// omit business logic ...
}
}
/**
* @author mghio
* @since 2021-07-17
*/
@Service
public class TradeService {
@Autowired
private OrderService orderService;
public void testCreateTrade() {
// omit business logic ...
}
}
这种循环依赖场景,程序是可以正常运行的,从代码上看确实是有循环依赖了,也就是说 Spring 是支持这种循环依赖场景的,这里我们察觉不到循环依赖的原因是 Spring 已经默默地解决了。
假设没有做任何处理,按照正常的创建逻辑来执行的话,流程是这样的:容器先创建 OrderService,发现依赖于 TradeService,再创建 OrderService,又发现依赖于 TradeService ... ,发生无限死循环,最后发生栈溢出错误,程序停止。为了支持这种常见的循环依赖场景,Spring 将创建对象分为如下几个步骤:
- 实例化一个新对象(在堆中),但此时尚未给对象属性赋值
- 给对象赋值
- 调用 BeanPostProcessor 的一些实现类的方法,在这个阶段,Bean 已经创建并赋值属性完成。这时候容器中所有实现 BeanPostProcessor 接口的类都会被调用(e.g. AOP)
- 初始化(如果实现了 InitializingBean,就会调用这个类的方法来完成类的初始化)
- 返回创建出来的实例
为此,Spring 引入了三级缓存来处理这个问题(三级缓存定义在 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry 中),第一级缓存 singletonObjects 用于存放完全初始化好的 Bean,从该缓存中取出的 Bean 可以直接使用,第二级缓存 earlySingletonObjects 用于存放提前暴露的单例对象的缓存,存放原始的 Bean 对象(属性尚未赋值),用于解决循环依赖,第三级缓存 singletonFactories 用于存放单例对象工厂的缓存,存放 Bean 工厂对象,用于解决循环依赖。上述实例使用三级缓存的处理流程如下所示:
如果你看过三级缓存的定义源码的话,可能也有这样的疑问:为什么第三级的缓存的要定义成 Map<String, ObjectFactory<?>>,不能直接缓存对象吗?这里不能直接保存对象实例,因为这样就无法对其做增强处理了。详情可见类 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean 方法部分源码如下:
第 ② 种场景——多例 Bean 的 setter 注入
这种方式平常使用得相对较少,还是使用前文的两个 Service 作为示例,唯一不同的地方是现在都声明为多例了,示例代码如下:
/**
* @author mghio
* @since 2021-07-17
*/
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class OrderService {
@Autowired
private TradeService tradeService;
public void testCreateOrder() {
// omit business logic ...
}
}
/**
* @author mghio
* @since 2021-07-17
*/
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class TradeService {
@Autowired
private OrderService orderService;
public void testCreateTrade() {
// omit business logic ...
}
}
如果你在 Spring 中运行以上代码,是可以正常启动成功的,原因是在类 org.springframework.beans.factory.support.DefaultListableBeanFactory 的 preInstantiateSingletons() 方法预实例化处理时,过滤掉了多例类型的 Bean,方法部分代码如下:
但是如果此时有其它单例类型的 Bean 依赖到这些多例类型的 Bean 的时候,就会报如下所示的循环依赖错误了。
第 ③ 种场景——代理对象的 setter 注入
这种场景也会经常碰到,有时候为了实现异步调用会在 XXXXService 类的方法上添加 @Async 注解,让方法对外部变成异步调用(前提要是要在启用类上添加启用注解哦 @EnableAsync),示例代码如下:
/**
* @author mghio
* @since 2021-07-17
*/
@EnableAsync
@SpringBootApplication
public class BlogMghioCodeApplication {
public static void main(String[] args) {
SpringApplication.run(BlogMghioCodeApplication.class, args);
}
}
/**
* @author mghio
* @since 2021-07-17
*/
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class OrderService {
@Autowired
private TradeService tradeService;
@Async
public void testCreateOrder() {
// omit business logic ...
}
}
/**
* @author mghio
* @since 2021-07-17
*/
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class TradeService {
@Autowired
private OrderService orderService;
public void testCreateTrade() {
// omit business logic ...
}
}
在标有 @Async 注解的场景下,在添加启用异步注解(@EnableAsync)后,代理对象会通过 AOP 自动生成。以上代码运行会抛出 BeanCurrentlyInCreationException 异常。运行的大致流程如下图所示:
源码在 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory 类的方法 doCreateBean 中,会判断第二级缓存 earlySingletonObjects 中的对象是否等于原始对象,方法判断部分的源码如下:
二级缓存存放的对象是 AOP 生成出来的代理对象,和原始对象不相等,所以抛出了循环依赖错误。如果细看源码的话,会发现如果二级缓存是空的话会直接返回(因为比较的对象都没有,根本无法校验了),就不会报循环依赖的错误了,默认情况下,Spring 是按照文件全路径递归搜索,按路径 + 文件名 排序,排序靠前先加载,所以我们只要调整这两个类名称,让方法标有 @Async 注解的类排序在后面即可。
第 ④ 种场景——构造器注入
构造器注入的场景很少,到目前为止我所接触过的公司项目和开源项目中还没遇到使用构造器注入的,虽然用得不多,但是需要知道 Spring 为什么不支持这种场景的循环依赖,构造器注入的示例代码如下:
/**
* @author mghio
* @since 2021-07-17
*/
@Service
public class OrderService {
private TradeService tradeService;
public OrderService(TradeService tradeService) {
this.tradeService = tradeService;
}
public void testCreateOrder() {
// omit business logic ...
}
}
/**
* @author mghio
* @since 2021-07-17
*/
@Service
public class TradeService {
private OrderService orderService;
public TradeService(OrderService orderService) {
this.orderService = orderService;
}
public void testCreateTrade() {
// omit business logic ...
}
}
构造器注入无法加入到第三级缓存当中,Spring 框架中的三级缓存在此场景下无用武之地,所以只能抛出异常,整体流程如下(虚线表示无法执行,为了直观也把下一步画出来了):
第 ⑤ 种场景——DependsOn 循环依赖
这种 DependsOn 循环依赖场景很少,一般情况下不怎么使用,了解一下会导致循环依赖的问题即可,@DependsOn 注解主要是用来指定实例化顺序的,示例代码如下:
/**
* @author mghio
* @since 2021-07-17
*/
@Service
@DependsOn("tradeService")
public class OrderService {
@Autowired
private TradeService tradeService;
public void testCreateOrder() {
// omit business logic ...
}
}
/**
* @author mghio
* @since 2021-07-17
*/
@Service
@DependsOn("orderService")
public class TradeService {
@Autowired
private OrderService orderService;
public void testCreateTrade() {
// omit business logic ...
}
}
通过上文,我们知道,如果这里的类没有标注 @DependsOn 注解的话是可以正常运行的,因为 Spring 支持单例 setter 注入,但是加了示例代码的 @DependsOn 注解后会报循环依赖错误,原因是在类 org.springframework.beans.factory.support.AbstractBeanFactory 的方法 doGetBean() 中检查了 dependsOn 的实例是否有循环依赖,如果有循环依赖则抛出循环依赖异常,方法判断部分代码如下:
总结
本文主要介绍了什么是循环依赖以及 Spring 对各种循环依赖场景的处理,文中只列出了部分涉及到的源码,都标了所在源码中的位置,感兴趣的朋友可以去看看完整源码,最后 Spring 对各种循环依赖场景的支持情况如下图所示(P.S. Spring 版本:5.1.9.RELEASE):
Spring 的循环依赖问题的更多相关文章
- Spring的循环依赖问题
spring容器循环依赖包括构造器循环依赖和setter循环依赖,那Spring容器如何解决循环依赖呢?首先让我们来定义循环引用类: 在Spring中将循环依赖的处理分成了3种情况: 构造器循环依赖 ...
- Spring之循环依赖
转:http://my.oschina.net/tryUcatchUfinallyU/blog/287936 概述 如何检测循环依赖 循环依赖如何解决 Spring如何解决循环依赖 主要的几个缓存 主 ...
- 再谈spring的循环依赖是怎么造成的?
老生常谈,循环依赖!顾名思义嘛,就是你依赖我,我依赖你,然后就造成了循环依赖了!由于A中注入B,B中注入A导致的吗? 看起来没毛病,然而,却没有说清楚问题!甚至会让你觉得你是不清楚spring的循环依 ...
- Spring解决循环依赖
1.Spring解决循环依赖 什么是循环依赖:比如A引用B,B引用C,C引用A,它们最终形成一个依赖环. 循环依赖有两种 1.构造器循环依赖 构造器注入导致的循环依赖,Spring是无法解决的,只能抛 ...
- Spring当中循环依赖很少有人讲,今天一起来学习!
网上关于Spring循环依赖的博客太多了,有很多都分析的很深入,写的很用心,甚至还画了时序图.流程图帮助读者理解,我看了后,感觉自己是懂了,但是闭上眼睛,总觉得还没有完全理解,总觉得还有一两个坎过不去 ...
- Spring的循环依赖,学就完事了【附源码】
目录 啥是循环依赖? Spring可以解决循环依赖的条件 Spring如何去解决循环依赖 SpringBean的创建流程 Spring维护的三级缓存 getSingleton getSingleton ...
- Spring的循环依赖
本文简要介绍了循环依赖以及Spring解决循环依赖的过程 一.定义 循环依赖是指对象之间的循环依赖,即2个或以上的对象互相持有对方,最终形成闭环.这里的对象特指单例对象. 二.表现形式 对象之间的循环 ...
- 详解Spring DI循环依赖实现机制
一个对象引用另一个对象递归注入属性即可实现后续的实例化,同时如果两个或者两个以上的 Bean 互相持有对⽅,最终形成闭环即所谓的循环依赖怎么实现呢属性的互相注入呢? Spring bean生命周期具体 ...
- 【spring源码分析】spring关于循环依赖的问题
引言:循环依赖就是N个类中循环嵌套引用,如果在日常开发中我们用new 对象的方式发生这种循环依赖的话程序会在运行时一直循环调用,直至内存溢出报错.下面说一下Spring是如果解决循环依赖的. 第一种: ...
随机推荐
- Django(39)使用redis配置缓存
前言 动态网站的基本权衡是,它们是动态的.每次用户请求页面时,Web服务器都会进行各种计算 - 从数据库查询到模板呈现再到业务逻辑 - 以创建站点访问者看到的页面.从处理开销的角度来看,这比标准的 ...
- sentinel入门
sentinel下载:sentinel-dashboard-1.8.1.jar 下载完成后进入sentinel-dashboard-1.8.1.jar的文件夹,在cmd中输入java -jar sen ...
- RADAR和LIDAR区别分析
RADAR和LIDAR区别分析 如果一直关注自动驾驶汽车的新闻,可能已经注意到许多自动驾驶汽车制造商正在使用LIDAR(光成像检测和测距)进行车载物体检测.对于许多自动 驾驶汽车应用而言,LIDAR比 ...
- 最全JVM知识点思维导图,看这一篇就够了
此处是转发别人的,别人花了二个月, 我花一天时间看完, 觉得很有用 https://www.processon.com/view/link/5eea141cf346fb1ae56a44e7
- P1828 [USACO3.2]香甜的黄油 Sweet Butter
题目描述 农夫$John$发现做出全威斯康辛州最甜的黄油的方法:糖.把糖放在一片牧场上,他知道$N(1\leqslant N\leqslant 500)$只奶牛会过来舔它,这样就能做出能卖好价钱的超甜 ...
- 【NX二次开发】Block UI 选择表达式
属性说明 属性 类型 描述 常规 BlockID String 控件ID Enable Logical 是否可操作 Group ...
- 【无线通信篇01 | Zstack协议栈】CC2530 Zigbee Zstack协议栈组网项目及详细讲解篇
演示视频:https://www.bilibili.com/video/BV1Ew411o7Fp 物联网无线通信技术,ZigBee无线传感网络 CC2530最大的特点就是一个拥有无线收发器(RF)的单 ...
- 迁移Report Server DataBase时遇到的坑
1.项目背景 由于历史原因,公司部分系统的Report是基于SQL Server Report Service搭建的,且Reporting Services 和Report Server DataBa ...
- sql数据库新建作业,新建步骤时报错从 IClassFactory 为 CLSID 为 {AA40D1D6-CAEF-4A56-B9BB-D0D3DC976BA2} 的 COM 组件创建实例失败,原因是出现以下错误: c001f011。 (Microsoft.SqlServer.ManagedDTS)
简单粗暴的重启sql数据库 其他网上找的方法 32位操作系统: 打开运行(命令提示符), 一.输入 cd c:\windows\system32 进入到c:\windows\system32路径中 二 ...
- Ubuntu安装部署Kafka
Ubuntu安装部署Kafka 环境: Ubuntu 18.04.4 LTS ,JDK1.8,kafka_2.12-2.3.1 确保已经安装了JDK,JDK安装过程不再赘述.可参考文章xxxx 一.下 ...