当 Spring 循环依赖碰上 Aysnc,调试过程中出现 BeanCurrentlyInCreationException,有点意思
开心一刻
前两天有个女生加我,我同意了
第一天,她和我聊文学,聊理想,聊篮球,聊小猫小狗
第二天,她和我说要看我腹肌
吓我一跳,我反手就删除拉黑,我特喵一肚子的肥肉,哪来的腹肌!

循环依赖
关于 Spring 的循环依赖,我已经写了 4 篇
Spring 的循环依赖,源码详细分析 → 真的非要三级缓存吗
此时你们是不是有点慌,莫非要来五探了,还有完没完了?我先给你们打一针强心剂,今天我们不聊循环依赖,而是来看看在调试循环依赖过程中遇到的小插曲
首先声明下,这是来自园友(@飞的很慢的牛蛙 )的素材,已经过他同意
循环依赖案例很简单
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.qsl</groupId>
<artifactId>spring-circle</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>spring-circle-async</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
</dependencies>
</project>
Spring 的版本用的是:5.2.12.RELEASE
Circle.java
/**
* @author: 青石路
*/
@Component
public class Circle {
@Autowired
private Loop loop;
public Loop getLoop() {
return loop;
}
public void sayHello(String name) {
System.out.println("circle sayHello, " + name);
}
}
Loop.java
/**
* @author: 青石路
*/
@Component
public class Loop {
@Autowired
@Lazy
private Circle circle;
public Circle getCircle() {
return circle;
}
public void sayHello(String name) {
System.out.println("loop sayHello, " + name);
}
}
为了兼容 Spring 的各种版本,加了 @Lazy
CircleTest.java
/**
* @author: 青石路
*/
@ComponentScan(basePackages = "com.qsl")
public class CircleTest {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(CircleTest.class);
Circle circle = ctx.getBean(Circle.class);
Loop loop = ctx.getBean(Loop.class);
System.out.println(circle.getLoop());
System.out.println(loop);
}
}
main
跑起来是没问题滴
完整代码:spring-circle-async
调试插曲
正常调试,想看看 Spring 是如何处理循环依赖的;在 AbstractAutowireCapableBeanFactory#doCreateBean
的 606 行打个断点,同时给断点加个 Condition

开始调试,为了方便查看三级缓存中的内容,我们添加三个 watch

将三级缓存都添加进来

此时我们来看第二级缓存 earlySingletonObjects

是没有内容的,我们再看下第三级缓存

circle 怎么会到第三级缓存中,跟循环依赖有关;接下来去看下第一级缓存,找到 loop

点一下 circle
的 toStrng()
,然后我们 F8
一下(代码 606 行执行完毕,来到 607 行,607行并未执行),再去看第二级缓存

第二级缓存竟然有元素了,那第三级缓存的 circle
还存在吗

很显然,是有什么操作将第三级缓存中的 circle
提前曝光到第二级缓存了,回顾下这期间我们做了哪些操作?
- 点了 circle 的 toString()
- F8,执行了代码 606 行:if (earlySingletonExposure)
这就很明显了,肯定是点了 circle 的 toString() 导致的,怎么验证了?其实很简单,重新开始调试,来到 AbstractAutowireCapableBeanFactory 606 行后,啥也别动,直接在 DefaultSingletonBeanRegistry#getSingleton
182 行打个断点

然后再回到 AbstractAutowireCapableBeanFactory 606,再去第一级缓存中找 loop,然后点击它的 circle 的 toString,IDEA 会提示如下信息

Skipped breakpoint at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry:182 because it happened inside debugger evaluation Troubleshooting guide
翻译过来就是
忽略 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry:182 的断点,因为它发生在调试器内部,详情请看 Troubleshooting guide
提前曝光就提前曝光呗,放开断点,程序能够正常执行完毕,有什么关系呢?那我就再给你们加点料,CircleTest.java 上加上 @EnableAsync
/**
* @author: 青石路
*/
@ComponentScan(basePackages = "com.qsl")
@EnableAsync
public class CircleTest {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(CircleTest.class);
Circle circle = ctx.getBean(Circle.class);
Loop loop = ctx.getBean(Loop.class);
System.out.println(circle.getLoop());
System.out.println(loop);
}
}
Circle.java 的 sayHello 方法上加上 @Async
/**
* @author: 青石路
*/
@Component
public class Circle {
@Autowired
private Loop loop;
public Loop getLoop() {
return loop;
}
@Async
public void sayHello(String name) {
System.out.println("circle sayHello, " + name);
}
}
重复之前的调试过程(记得去找第一级缓存中的 loop
的 circle
,然后点其 toString()
),取消所有断点后 F9
,BeanCurrentlyInCreationException
它就来了
Exception in thread "main" org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'circle': Bean with name 'circle' has been injected into other beans [loop] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:623)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:89)
at com.qsl.CircleTest.main(CircleTest.java:16)
异常信息已经说的很清楚了
创建名为 circle 的bean时出错:注入给 loop bean 的是 circle 的代理实例,而非最终进入到第一级缓存的 circle bean
相当于注入给 loop bean 的是 circle 的代理对象实例,而提前曝光的是 circle 的半成品对象,两处不一致;究其原因还是我们操作 circle 的 toString,导致半成品对象提前曝光了
我们来梳理下 Circle
和 Loop
的实例创建过程。根据 Spring
的扫描规则,Circle 是被先扫描到的
三探循环依赖 → 记一次线上偶现的循环依赖问题 有介绍扫描规则
所以 Circle
实例会先被创建,因为 @Async
(底层实现:代理),第三级缓存提前创建 Circle 代理对象

接着填充 Circle 半成品对象的属性 Loop loop
,所以继续创建 Loop 实例,第三级缓存提前创建 Loop 代理对象(用不到,后续直接 remove)

此时我们看下当前线程的栈帧

接着填充 Loop 半成品对象的属性 Circle circle
,此时 circle 还没创建完,所以填充给 loop 的 circle 肯定是第三级缓存中 circle 的代理对象

填充完后,loop 实例创建完毕,会添加到第一级缓存中,并移除第三级缓存中的 loop(呼应前面说到的:用不到,后续直接 remove)和第二级缓存中的 loop(没有)

此时 loop 来到了第一级缓存,成为了 成品
实例,而 circle 还在第三级缓存中,第二级缓存仍是空;loop 实例创建好之后,回到 circle 的属性填充,将 loop 成品填充给半成品 circle

初始化 circle 完成后,此时 circle 的曝光对象(exposedObject)是

此时已经到 606 行了,大家知道该做什么了吧,去第一级缓存中找到 loop,然后点击它的 circle 的 toString()

然后我们进入 getSingleton
方法,此时 circle 在缓存中的位置发生了变化

正是这个变化,导致了接下来的流程发生了变化;我们继续往下看,getSingleton 方法返回了二级缓存中的 circle,而非正常流程下的 null

exposedObject
不等于 bean
,会来到 else if 分支判断是否有依赖 circle 的 bean,很显然有(loop),最后就来到异常分支
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
}
凡是涉及到代理的,最终在第一级缓存中的都是实例的代理对象,比如 circle,我们取消掉所有断点,只在 CircleTest.java 上打一个断点,看看 circle 和 loop 实例就清楚了

总结
Spring 调试过程中不要随便去点代理对象的
toString
,它可能会导致对象的提前曝光,打乱了 Spring bean 的创建过程,最终导致异常;抛异常倒是够直观,就怕不抛异常,然后运行过程中出现各种奇葩问题IDEA 调试配置
有些版本默认是勾上的,这就会导致调试后过程中,我们去查看对象的时候自动调用对象的
toString
方法,可能引发一些异常,比如上文中介绍的循环依赖 circle 提前曝光的问题实际工作中,大家基本遇不到文中的情况,看看图个乐就行
当 Spring 循环依赖碰上 Aysnc,调试过程中出现 BeanCurrentlyInCreationException,有点意思的更多相关文章
- Spring 循环依赖
循环依赖就是循环引用,就是两个或多个Bean相互之间的持有对方,比如CircleA引用CircleB,CircleB引用CircleC,CircleC引用CircleA,则它们最终反映为一个环.此处不 ...
- Springboot源码分析之Spring循环依赖揭秘
摘要: 若你是一个有经验的程序员,那你在开发中必然碰到过这种现象:事务不生效.或许刚说到这,有的小伙伴就会大惊失色了.Spring不是解决了循环依赖问题吗,它是怎么又会发生循环依赖的呢?,接下来就让我 ...
- 一张图彻底搞懂Spring循环依赖
1 什么是循环依赖? 如下图所示: BeanA类依赖了BeanB类,同时BeanB类又依赖了BeanA类.这种依赖关系形成了一个闭环,我们把这种依赖关系就称之为循环依赖.同理,再如下图的情况: 上图中 ...
- 高频面试题:一张图彻底搞懂Spring循环依赖
1 什么是循环依赖? 如下图所示: BeanA类依赖了BeanB类,同时BeanB类又依赖了BeanA类.这种依赖关系形成了一个闭环,我们把这种依赖关系就称之为循环依赖.同理,再如下图的情况: 上图中 ...
- 面试必问:Spring循环依赖的三种方式
引言:循环依赖就是N个类中循环嵌套引用,如果在日常开发中我们用new 对象的方式发生这种循环依赖的话程序会在运行时一直循环调用,直至内存溢出报错.下面说一下Spring是如果解决循环依赖的. 第一种: ...
- Spring 循环依赖的三种方式(三级缓存解决Set循环依赖问题)
本篇文章解决以下问题: [1] . Spring循环依赖指的是什么? [2] . Spring能解决哪种情况的循环依赖?不能解决哪种情况? [3] . Spring能解决的循环依赖原理(三级缓存) 一 ...
- Spring循环依赖的解决
## Spring循环依赖的解决 ### 什么是循环依赖 循环依赖,是依赖关系形成了一个圆环.比如:A对象有一个属性B,那么这时候我们称之为A依赖B,如果这时候B对象里面有一个属性A.那么这时候A和B ...
- 这个 Spring 循环依赖的坑,90% 以上的人都不知道
1. 前言 这两天工作遇到了一个挺有意思的Spring循环依赖的问题,但是这个和以往遇到的循环依赖问题都不太一样,隐藏的相当隐蔽,网络上也很少看到有其他人遇到类似的问题.这里权且称他非典型Spring ...
- Spring — 循环依赖
读完这篇文章你将会收获到 Spring 循环依赖可以分为哪两种 Spring 如何解决 setter 循环依赖 Spring 为何是三级缓存 , 二级不行 ? Spring 为啥不能解决构造器循环依赖 ...
- spring 循环依赖的一次 理解
前言: 在看spring 循环依赖的问题中,知道原理,网上一堆的资料有讲原理. 但今天在看代码过程中,又产生了疑问. 疑问点如下: // 疑问点: 先进行 dependon 判断String[] de ...
随机推荐
- Js 实现导航li列表,选中时,显示选中样式
结合Django项目实现 实现步骤: html页面部分,使用bootstrap.css中的样式(不用可忽略,主要class样式),要引用bootstrap.css,使用到actvie样式: <l ...
- ARC 170_A 题解
AT_arc170_a [ARC170A] Yet Another AB Problem 题解 这道题做了我七天 (同时也是我第一到通过的 ARC 题) 太酷了 其实还是比较好理解的 原题题干 原题题 ...
- P2045 方格取数加强版题解
题目链接:P2045 方格取数加强版 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 题目: 出一个 n*n 的矩阵,每一格有一个非负整数 A{i,j}且A{i,j} <=10 ...
- 构建高可用性、高性能和可扩展的Zabbix Server架构
简介 本教程讲解了一下如何设计构建一个高性能.高可靠.高扩展的Zabbix 监控集群. 架构图 架构图PDF下载: https://songxwn.com/file/Zabbix_HA.pdf Pig ...
- Python性能测试框架:Locust实战教程
01认识Locust Locust是一个比较容易上手的分布式用户负载测试工具.它旨在对网站(或其他系统)进行负载测试,并确定系统可以处理多少个并发用户,Locust 在英文中是 蝗虫 的意思:作者的想 ...
- python rsa-oaep加密示例
代码: 1 from Crypto.PublicKey import RSA 2 from Crypto.Cipher import PKCS1_OAEP 3 import base64 4 rsa_ ...
- Linux使用Mysql数据库
启动MySQL: service mysqld start 或者使用如下命令: /etc/init.d/mysqld start 重新启动MySQL: service mysqld restart 或 ...
- [oeasy]python0117 文字的演化_埃及圣书体_象形文字_楔形文字
埃及圣书体 回忆上次内容 两河流域 苏美尔文明 所使用的 楔形文字 不是象形文字 添加图片注释,不超过 140 字(可选) 楔形文字的字型 究竟是怎么来的呢? 巴别塔 苏美尔的 ...
- oeasy教您玩转linux 010216 随机诗词 fortunezh
我们来回顾一下 上一部分我们都讲了什么? 下载fortune 输出重定向到cowsay 多重输出重定向 fortune的细节 有没有中️文的fortune呢 # 搜索一下fortune apt sea ...
- oeasy教您玩转vim - 43 - # 替换模式
替换模式 回忆上节课内容 上次我们学到的最有用的就是c 他和d的区别就是删除之后进入到插入模式 c可以配合motion 可以用ciw来快速删除当前光标所在的单词 可i和a 配合的文字块 w wor ...