當 Spring 迴圈依賴碰上 Aysnc,除錯過程中出現 BeanCurrentlyInCreationException,有點意思

青石路發表於2024-08-12

開心一刻

前兩天有個女生加我,我同意了

第一天,她和我聊文學,聊理想,聊籃球,聊小貓小狗

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

嚇我一跳,我反手就刪除拉黑,我特喵一肚子的肥肉,哪來的腹肌!

就離譜

迴圈依賴

關於 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

相關文章