首页 > 其他分享 >当 Spring 循环依赖碰上 Aysnc,调试过程中出现 BeanCurrentlyInCreationException,有点意思

当 Spring 循环依赖碰上 Aysnc,调试过程中出现 BeanCurrentlyInCreationException,有点意思

时间:2024-08-12 08:55:34浏览次数:12  
标签:缓存 java Spring Aysnc Loop BeanCurrentlyInCreationException circle Circle loop

开心一刻

前两天有个女生加我,我同意了

第一天,她和我聊文学,聊理想,聊篮球,聊小猫小狗

第二天,她和我说要看我腹肌

吓我一跳,我反手就删除拉黑,我特喵一肚子的肥肉,哪来的腹肌!

就离谱

循环依赖

关于 Spring 的循环依赖,我已经写了 4 篇

Spring 的循环依赖,源码详细分析 → 真的非要三级缓存吗

再探循环依赖 → Spring 是如何判定原型循环依赖和构造方法循环依赖的

三探循环依赖 → 记一次线上偶现的循环依赖问题

四探循环依赖 → 当循环依赖遇上 BeanPostProcessor,爱情可能就产生了!

此时你们是不是有点慌,莫非要来五探了,还有完没完了?我先给你们打一针强心剂,今天我们不聊循环依赖,而是来看看在调试循环依赖过程中遇到的小插曲

首先声明下,这是来自园友(@飞的很慢的牛蛙 )的素材,已经过他同意

循环依赖案例很简单

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

断点condition

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

添加watch

将三级缓存都添加进来

三级缓存watch

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

二级缓存空的

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

第三级缓存非空

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

第一级缓存loop 点击toString

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

第二级缓存非空_有circle

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

第三级缓存_circle没了

很显然,是有什么操作将第三级缓存中的 circle 提前曝光到第二级缓存了,回顾下这期间我们做了哪些操作?

  1. 点了 circle 的 toString()
  2. F8,执行了代码 606 行:if (earlySingletonExposure)

这就很明显了,肯定是点了 circle 的 toString() 导致的,怎么验证了?其实很简单,重新开始调试,来到 AbstractAutowireCapableBeanFactory 606 行后,啥也别动,直接在 DefaultSingletonBeanRegistry#getSingleton 182 行打个断点

DefaultSingletonBeanRegistry

然后再回到 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);
    }
}

重复之前的调试过程(记得去找第一级缓存中的 loopcircle,然后点其 toString()),取消所有断点后 F9BeanCurrentlyInCreationException 它就来了

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,导致半成品对象提前曝光了

我们来梳理下 CircleLoop 的实例创建过程。根据 Spring 的扫描规则,Circle 是被先扫描到的

三探循环依赖 → 记一次线上偶现的循环依赖问题 有介绍扫描规则

所以 Circle 实例会先被创建,因为 @Async (底层实现:代理),第三级缓存提前创建 Circle 代理对象

circle代理对象存入三级缓存

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

Loop代理对象存入第三级缓存

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

创建loop时的栈帧

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

loop的circle属性

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

loop实例加入第一级缓存

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

loop填充到circle中

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

circle曝光对象

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

点击circle toString

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

circle来到第二级缓存

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

circle_问题关键点

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 实例就清楚了

circle是代理对象而loop不是

总结

  1. Spring 调试过程中不要随便去点代理对象的 toString,它可能会导致对象的提前曝光,打乱了 Spring bean 的创建过程,最终导致异常;抛异常倒是够直观,就怕不抛异常,然后运行过程中出现各种奇葩问题

  2. IDEA 调试配置

    debug对象toString默认调用开关

    有些版本默认是勾上的,这就会导致调试后过程中,我们去查看对象的时候自动调用对象的 toString 方法,可能引发一些异常,比如上文中介绍的循环依赖 circle 提前曝光的问题

  3. 实际工作中,大家基本遇不到文中的情况,看看图个乐就行

    20240128194820

标签:缓存,java,Spring,Aysnc,Loop,BeanCurrentlyInCreationException,circle,Circle,loop
From: https://www.cnblogs.com/youzhibing/p/18350601

相关文章

  • Spring框架中的@Bean注解详解
    Spring框架中的@Bean注解详解在Java的Spring框架中,@Bean是一个非常重要的注解,它允许开发者在注解方法中创建和配置对象,这些对象随后会被Spring容器管理。本文将通过一个简单的实例来详细解释@Bean注解的使用方法和它在Spring框架中的作用。什么是@Bean注解?@Bean注解是一......
  • 基于SpringBoot框架的企业财务管理系统设计与实现(论文+源码)_kaic
    摘  要在快速增长的信息时代,每个企业都在紧随其后,不断改进其办公模式。与此同时,各家企业的传统管理模式也逐步发生变化,政府和企业都将需要一个更加自动化和现代化的财务管理系统。这能够便利员工之间的信息交流和公司的工作任务进而提高工作效率。因此对现有的系统进行调......
  • Spring Cloud服务注册的守护神:一致性保障策略
    标题:SpringCloud服务注册的守护神:一致性保障策略在微服务架构中,服务注册中心扮演着核心角色,而SpringCloud作为微服务的集大成者,其服务注册机制的一致性保障尤为关键。本文将深入探讨SpringCloud中的服务注册信息一致性保障策略,揭示如何通过精妙的设计和实现确保服务注册......
  • Spring Boot配置类的注解
    SpringBoot中,若某类只用@ConfigurationProperties注解,然后该类:没有在扫描路径下或没用@Component等注解就会导致无法被扫描为bean,须在配置类用@EnableConfigurationProperties注解去指定这个类,才能使@ConfigurationProperties生效,并作为一个bean添加进Spring......
  • 云中韧性:Spring Cloud服务调用重试机制深度解析
    标题:云中韧性:SpringCloud服务调用重试机制深度解析在微服务架构中,服务间的调用可能会因为网络问题、服务不可达、资源竞争等原因失败。SpringCloud作为微服务架构的主流实现框架,提供了一套完整的服务调用重试机制,以增强系统的健壮性和可靠性。本文将详细探讨SpringCloud......
  • 基于Java的考试信息报名系统 SpringBoot考试报名管理系统 Vue前后端分离【Java毕业设
    ⛄博主介绍:⚡全栈开发工程师,精通Web前后端技术、数据库、架构设计。专注于Java技术领域和小程序领域的开发,毕业设计、课程设计项目中主要包括定制化开发、源代码、代码讲解、文档报告辅导、安装调试等。✅文末获取联系✅目录1项目介绍 2技术选型 3系统总体设计 4......
  • 微服务的多面手:Spring Cloud 多数据中心支持全解析
    标题:微服务的多面手:SpringCloud多数据中心支持全解析在微服务架构中,服务的高可用性和弹性伸缩是设计的核心。随着业务的全球化发展,企业经常需要在多个数据中心部署应用以满足不同地区的用户需求,确保服务的快速响应和数据的低延迟访问。SpringCloud作为微服务架构的佼佼......
  • Spring 中的InitializingBean
      InitializingBean 是Spring框架中的一个接口,用于在Spring容器中初始化bean时执行特定的初始化逻辑。这个接口定义了一个方法 afterPropertiesSet(),当bean的所有属性被设置后(即依赖注入完成后),Spring容器会调用这个方法。通过实现这个接口,你可以在bean初始化完......
  • springboot+vue体检套餐定制系统的设计与实现【程序+论文+开题】-计算机毕业设计
    系统程序文件列表开题报告内容研究背景随着人们健康意识的不断提升,定期体检已成为现代人维护健康、预防疾病的重要手段。然而,市场上现有的体检套餐往往存在一刀切的问题,难以满足不同年龄、性别、职业及健康状况人群的个性化需求。此外,体检预约流程繁琐、体检结果解读困难等......
  • 计算机毕业设计 校园失物招领网站 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解
    ......