開心一刻
前兩天有個女生加我,我同意了
第一天,她和我聊文學,聊理想,聊籃球,聊小貓小狗
第二天,她和我說要看我腹肌
嚇我一跳,我反手就刪除拉黑,我特喵一肚子的肥肉,哪來的腹肌!
迴圈依賴
關於 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
開始除錯,為了方便檢視三級快取中的內容,我們新增三個 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 提前曝光的問題 -
實際工作中,大家基本遇不到文中的情況,看看圖個樂就行