使用@Async非同步註解導致該Bean在迴圈依賴時啟動報BeanCurrentlyInCreationException異常的根本原因分析,以及提供解決方案

hellozhxy發表於2020-12-03

前言

今天在自己工程中使用@Async的時候,碰到了一個問題:Spring迴圈依賴(circular reference)問題。 或許剛說到這,有的小夥伴就會大驚失色了。Spring不是解決了迴圈依賴問題嗎,它是支援迴圈依賴的呀?怎麼會呢?

不可否認,在這之前我也是這麼堅信的,而且每次使用得也屢試不爽。倘若你目前也和我有一樣堅挺的想法,那麼相信本文能讓你大有收貨~~

不得不提,關於@Async的使用姿勢,請參閱: 【小家Spring】Spring非同步處理@Async的使用以及原理、原始碼分析(@EnableAsync) 關於Spring Bean的迴圈依賴問題,請參閱: 【小家Spring】一文告訴你Spring是如何利用"三級快取"巧妙解決Bean的迴圈依賴問題的

我通過實驗總結出,出現使用@Async導致迴圈依賴問題的必要條件:

  1. 已開啟@EnableAsync的支援
  2. @Async註解所在的Bean被迴圈依賴了

背景

若你是一個有經驗的程式設計師,那你在開發中必然碰到過這種現象:事務不生效

關於事務不生效方面的原因,可參考:【小家java】Spring事務不生效的原因大解讀

本文場景的背景也一樣,我想呼叫本類的非同步方法(標註有@Async註解),很顯然我知道為了讓於@Async生效,我把自己依賴進來,然後通過service介面來呼叫,程式碼如下:

@Service
public class HelloServiceImpl implements HelloService {
    @Autowired
    private HelloService helloService;
    
    @Override
    public Object hello(Integer id) {
        System.out.println("執行緒名稱:" + Thread.currentThread().getName());
        helloService.fun1(); // 使用介面方式呼叫,而不是this
        return "service hello";
    }

    @Async
    @Override
    public void fun1() {
        System.out.println("執行緒名稱:" + Thread.currentThread().getName());
    }
}

此種做法首先是Spring中一個典型的迴圈依賴場景:自己依賴自己。本以為能夠像解決事務不生效問題一樣依舊屢試不爽,但沒想到非常的不給面子,啟動即報錯:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'helloServiceImpl': Bean with name 'helloServiceImpl' has been injected into other beans [helloServiceImpl] 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 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:622)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	...

這裡說明一下,為什麼有小夥伴跟我說:我使用@Async即使本類方法呼叫也從來木有遇到這個錯誤啊?難道它不常見? 為此經過我的一番調查,包括看一些同事、小夥伴的程式碼發現:並不是使用@Async沒有啟動報錯,而是他本類呼叫的時候直接呼叫的方法,這樣@Async是不生效的但小夥伴卻全然不知而已。

至於@Async沒生效這種問題為何沒報出來???甚至過了很久很久都沒人發現和關注?? 其實道理很簡單,它和事務不生效不一樣,@Async若沒生效99%情況下都不會影響到業務的正常進行,因為它不會影響資料正確性,只會影響到效能(無非就是非同步變同步唄,這是相容的)。 但是呢,我期望的是作為一個技術人,還是能夠有一定的技術敏感性。能夠迅速幫助自己或者你身邊同事定位到這個問題,這或許是你可以出彩的資本吧~


我們知道事務不生效@Async不生效的根本原因都是同一個:直接呼叫了本類方法而非介面方法/代理物件方法。 解決這類不生效問題的方案一般我們都有兩種:

  1. 自己注入自己,然後再呼叫介面方法(當然此處的一個變種是使用程式設計方式形如:AInterface a = applicationContext.getBean(AInterface.class);這樣子手動獲取也是可行的~~~本文不討論這種比較直接簡單的方式)
  2. 使用AopContext.currentProxy();方式

本文就講解採取方式一自己注入自己的方案解決帶來了更多問題,使用AopContext.currentProxy();方式會在緊鄰的下篇博文裡詳解~

注意:自己注入自己是能夠完美解決事務不生效問題。如題,本文旨在講解解決@Async的問題~~~

有的小夥伴肯定會說:讓不呼叫本類的@Async方法不就可以了;讓不產生迴圈依賴不就可以了;這都是解決方案啊~ 其實你說的沒毛病,但我我想說:理想的設計當然是不建議迴圈依賴的。但在真實的業務開發中迴圈依賴是100%避免不了的,同樣本類方法的互調也同樣是避免不了的~

關於@Async的使用和原理,有興趣的可以先補補課: 【小家Spring】Spring非同步處理@Async的使用以及原理、原始碼分析(@EnableAsync)

自己依賴自己方案帶來的問題分析

說明:所有示例,都預設@EnableAsync已經開啟~ 所以示例程式碼中不再特別標註

自己依賴自己這種方式是一種典型的使用迴圈依賴方式來解決問題,大多數情況下它是一個非常好的解決方案。 比如本例若要解決@Async本類呼叫問題,我們的程式碼會這麼來寫:

@Service
public class HelloServiceImpl implements HelloService {

    @Autowired
    private HelloService helloService;

    @Transactional
    @Override
    public Object hello(Integer id) {
        System.out.println("執行緒名稱:" + Thread.currentThread().getName());
        // fun1(); // 這樣書寫@Async肯定不生效~
        helloService.fun1(); //呼叫介面方法
        return "service hello";
    }

    @Async
    @Override
    public void fun1() {
        System.out.println("執行緒名稱:" + Thread.currentThread().getName());
    }
}

本以為像解決事務問題一樣,像這樣寫是肯定完美解決問題的。但奈何帶來了新問題,啟動即報錯:

報錯資訊如上~~~

BeanCurrentlyInCreationException這個異常型別小夥伴們應該並不陌生,在迴圈依賴那篇文章中(請參閱相關閱讀)有講述到:文章裡有提醒小夥伴們關注報錯的日誌,有朝一日肯定會碰面,沒想到來得這麼快~

對如上異常資訊,我大致翻譯如下:

建立名為“helloServiceImpl”的bean時出錯:名為“helloServiceImpl”的bean已作為迴圈引用的一部分注入到其原始版本中的其他bean[helloServiceImpl]中,
**但最終已被包裝**。這意味著其他bean不使用bean的最終版本。

問題定位

本著先定位問題才能解決問題的原則,找到問題的根本原因成為了我現在最需要做的事。從報錯資訊的描述可以看出,根本原因是helloServiceImpl最終被包裝(代理),所以被使用的bean並不是最終的版本,所以Spring的自檢機制報錯了~~~

說明:Spring管理的Bean都是單例的,所以Spring預設需要保證所有使用此Bean的地方都指向的是同一個地址,也就是最終版本的Bean,否則可能就亂套了,Spring也提供了這樣的自檢機制~

上面文字敘述有點蒼白,相信小夥伴們看著也是一臉懵逼、二臉繼續懵逼吧。下面通過示例程式碼分析看看結果。

為了更好的說明問題,此處不用自己依賴自己來表述(因為名字相同容易混淆不方便說明問題),而以下面A、B兩個類的形式說明:

@Service
public class A implements AInterface {
    @Autowired
    private BInterface b;
    @Async
    @Override
    public void funA() {
    }
}

@Service
public class B implements BInterface {
    @Autowired
    private AInterface a;
    @Override
    public void funB() {
        a.funA();
    }
}

如上示例程式碼啟動時會報錯:(示例程式碼模仿成功)

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Bean with name 'a' has been injected into other beans [b] 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 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:622)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
	...

下面是重點,來跟蹤一下原始碼,定位此問題:

protected Object doCreateBean( ... ){
	...
	boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
	if (earlySingletonExposure) {
		addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
	}
	...

	// populateBean這一句特別的關鍵,它需要給A的屬性賦值,所以此處會去例項化B~~
	// 而B我們從上可以看到它就是個普通的Bean(並不需要建立代理物件),例項化完成之後,繼續給他的屬性A賦值,而此時它會去拿到A的早期引用
	// 也就在此處在給B的屬性a賦值的時候,會執行到上面放進去的Bean A流程中的getEarlyBeanReference()方法  從而拿到A的早期引用~~
	// 執行A的getEarlyBeanReference()方法的時候,會執行自動代理建立器,但是由於A沒有標註事務,所以最終不會建立代理,so B合格屬性引用會是A的**原始物件**
	// 需要注意的是:@Async的代理物件不是在getEarlyBeanReference()中建立的,是在postProcessAfterInitialization建立的代理
	// 從這我們也可以看出@Async的代理它預設並不支援你去迴圈引用,因為它並沒有把代理物件的早期引用提供出來~~~(注意這點和自動代理建立器的區別~)

	// 結論:此處給A的依賴屬性欄位B賦值為了B的例項(因為B不需要建立代理,所以就是原始物件)
	// 而此處例項B裡面依賴的A注入的仍舊為Bean A的普通例項物件(注意  是原始物件非代理物件)  注:此時exposedObject也依舊為原始物件
	populateBean(beanName, mbd, instanceWrapper);
	
	// 標註有@Async的Bean的代理物件在此處會被生成~~~ 參照類:AsyncAnnotationBeanPostProcessor
	// 所以此句執行完成後  exposedObject就會是個代理物件而非原始物件了
	exposedObject = initializeBean(beanName, exposedObject, mbd);
	
	...
	// 這裡是報錯的重點~~~
	if (earlySingletonExposure) {
		// 上面說了A被B迴圈依賴進去了,所以此時A是被放進了二級快取的,所以此處earlySingletonReference 是A的原始物件的引用
		// (這也就解釋了為何我說:如果A沒有被迴圈依賴,是不會報錯不會有問題的   因為若沒有迴圈依賴earlySingletonReference =null後面就直接return了)
		Object earlySingletonReference = getSingleton(beanName, false);
		if (earlySingletonReference != null) {
			// 上面分析了exposedObject 是被@Aysnc代理過的物件, 而bean是原始物件 所以此處不相等  走else邏輯
			if (exposedObject == bean) {
				exposedObject = earlySingletonReference;
			}
			// allowRawInjectionDespiteWrapping 標註是否允許此Bean的原始型別被注入到其它Bean裡面,即使自己最終會被包裝(代理)
			// 預設是false表示不允許,如果改為true表示允許,就不會報錯啦。這是我們後面講的決方案的其中一個方案~~~
			// 另外dependentBeanMap記錄著每個Bean它所依賴的Bean的Map~~~~
			else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
				// 我們的Bean A依賴於B,so此處值為["b"]
				String[] dependentBeans = getDependentBeans(beanName);
				Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);

				// 對所有的依賴進行一一檢查~	比如此處B就會有問題
				// “b”它經過removeSingletonIfCreatedForTypeCheckOnly最終返返回false  因為alreadyCreated裡面已經有它了表示B已經完全建立完成了~~~
				// 而b都完成了,所以屬性a也賦值完成兒聊 但是B裡面引用的a和主流程我這個A竟然不相等,那肯定就有問題(說明不是最終的)~~~
				// so最終會被加入到actualDependentBeans裡面去,表示A真正的依賴~~~
				for (String dependentBean : dependentBeans) {
					if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
						actualDependentBeans.add(dependentBean);
					}
				}
	
				// 若存在這種真正的依賴,那就報錯了~~~  則個異常就是上面看到的異常資訊
				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 " +
							"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
				}
			}
		}
	}
	...
}

這裡知識點避開不@Aysnc註解標註的Bean的建立代理的時機。 @EnableAsync開啟時它會向容器內注入AsyncAnnotationBeanPostProcessor,它是一個BeanPostProcessor,實現了postProcessAfterInitialization方法。此處我們看程式碼,建立代理的動作在抽象父類AbstractAdvisingBeanPostProcessor上:

// @since 3.2   注意:@EnableAsync在Spring3.1後出現
// 繼承自ProxyProcessorSupport,所以具有動態代理相關屬性~ 方便建立代理物件
public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSupport implements BeanPostProcessor {

	// 這裡會快取所有被處理的Bean~~~  eligible:合適的
	private final Map<Class<?>, Boolean> eligibleBeans = new ConcurrentHashMap<>(256);

	//postProcessBeforeInitialization方法什麼不做~
	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) {
		return bean;
	}

	// 關鍵是這裡。當Bean初始化完成後這裡會執行,這裡會決策看看要不要對此Bean建立代理物件再返回~~~
	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) {
		if (this.advisor == null || bean instanceof AopInfrastructureBean) {
			// Ignore AOP infrastructure such as scoped proxies.
			return bean;
		}

		// 如果此Bean已經被代理了(比如已經被事務那邊給代理了~~)
		if (bean instanceof Advised) {
			Advised advised = (Advised) bean;
		
			// 此處拿的是AopUtils.getTargetClass(bean)目標物件,做最終的判斷
			// isEligible()是否合適的判斷方法  是本文最重要的一個方法,下文解釋~
			// 此處還有個小細節:isFrozen為false也就是還沒被凍結的時候,就只向裡面新增一個切面介面   並不要自己再建立代理物件了  省事
			if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {
				// Add our local Advisor to the existing proxy's Advisor chain...
				// beforeExistingAdvisors決定這該advisor最先執行還是最後執行
				// 此處的advisor為:AsyncAnnotationAdvisor  它切入Class和Method標註有@Aysnc註解的地方~~~
				if (this.beforeExistingAdvisors) {
					advised.addAdvisor(0, this.advisor);
				} else {
					advised.addAdvisor(this.advisor);
				}
				return bean;
			}
		}

		// 若不是代理物件,此處就要下手了~~~~isEligible() 這個方法特別重要
		if (isEligible(bean, beanName)) {
			// copy屬性  proxyFactory.copyFrom(this); 生成一個新的ProxyFactory 
			ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
			// 如果沒有強制採用CGLIB 去探測它的介面~
			if (!proxyFactory.isProxyTargetClass()) {
				evaluateProxyInterfaces(bean.getClass(), proxyFactory);
			}
			// 新增進此切面~~ 最終為它建立一個getProxy 代理物件
			proxyFactory.addAdvisor(this.advisor);
			//customize交給子類複寫(實際子類目前都沒有複寫~)
			customizeProxyFactory(proxyFactory);
			return proxyFactory.getProxy(getProxyClassLoader());
		}

		// No proxy needed.
		return bean;
	}
	
	// 我們發現BeanName最終其實是沒有用到的~~~
	// 但是子類AbstractBeanFactoryAwareAdvisingPostProcessor是用到了的  沒有做什麼 可以忽略~~~
	protected boolean isEligible(Object bean, String beanName) {
		return isEligible(bean.getClass());
	}
	protected boolean isEligible(Class<?> targetClass) {
		// 首次進來eligible的值肯定為null~~~
		Boolean eligible = this.eligibleBeans.get(targetClass);
		if (eligible != null) {
			return eligible;
		}
		// 如果根本就沒有配置advisor  也就不用看了~
		if (this.advisor == null) {
			return false;
		}
		
		// 最關鍵的就是canApply這個方法,如果AsyncAnnotationAdvisor  能切進它  那這裡就是true
		// 本例中方法標註有@Aysnc註解,所以鐵定是能被切入的  返回true繼續上面方法體的內容
		eligible = AopUtils.canApply(this.advisor, targetClass);
		this.eligibleBeans.put(targetClass, eligible);
		return eligible;
	}
	...
}

經此一役,根本原理是隻要能被切面AsyncAnnotationAdvisor切入(即只需要類/方法有標註@Async註解即可)的Bean最終都會生成一個代理物件(若已經是代理物件裡,只需要加入該切面即可了~)賦值給上面的exposedObject作為返回最終add進Spring容器內~


針對上面的步驟,為了輔助理解,我嘗試總結文字描述如下:

  1. context.getBean(A)開始建立A,A例項化完成後給A的依賴屬性b開始賦值~
  2. context.getBean(B)開始建立B,B例項化完成後給B的依賴屬性a開始賦值~
  3. 重點:此時因為A支援迴圈依賴,所以會執行A的getEarlyBeanReference方法得到它的早期引用。而執行getEarlyBeanReference()的時候因為@Async根本還沒執行,所以最終返回的仍舊是原始物件的地址
  4. B完成初始化、完成屬性的賦值,此時屬性field持有的是Bean A原始型別的引用~
  5. 完成了A的屬性的賦值(此時已持有B的例項的引用),繼續執行初始化方法initializeBean(...),在此處會解析@Aysnc註解,從而生成一個代理物件,所以最終exposedObject是一個代理物件(而非原始物件)最終加入到容器裡~
  6. 尷尬場面出現了:B引用的屬性A是個原始物件,而此處準備return的例項A竟然是個代理物件,也就是說B引用的並非是最終物件(不是最終放進容器裡的物件)
  7. 執行自檢程式:由於allowRawInjectionDespiteWrapping預設值是false,表示不允許上面不一致的情況發生,so最終就拋錯了~

此步驟是由我個人即興總結,希望能幫助到小夥伴們理解。若有不對的地方,還請指出讓幫忙我斧正

解決方案

通過上面分析,知道了問題的根本原因,現總結出解決上述新問題的解決方案,可分為下面三種方案:

  • allowRawInjectionDespiteWrapping設定為true
  • 使用@Lazy或者@ComponentScan(lazyInit = true)解決
  • 不要讓@Async的Bean參與迴圈依賴

1、把allowRawInjectionDespiteWrapping設定為true:

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        ((AbstractAutowireCapableBeanFactory) beanFactory).setAllowRawInjectionDespiteWrapping(true);
    }
}

這樣配置後,容器啟動將不再報錯了,但是但是但是:Bean A的@Aysnc方法將不起作用了,因為Bean B裡面依賴的a是個原始物件,所以它最終沒法執行非同步操作(即使容器內的a是個代理物件):

 

需要注意的是:但此時候Spring容器裡面的Bean A是Proxy代理物件的~~~

 

但是此種情況若是正常依賴(非迴圈依賴)的a,注入的是代理物件,@Async非同步依舊是會生效的哦~

這種解決方式一方面沒有達到真正的目的(畢竟Bean A上的@Aysnc沒有生效)。

由於它只對迴圈依賴內的Bean受影響,所以影響範圍並不是全域性,因此當找不到更好辦法的時候,此種這樣也不失是一個不錯的方案,所以我個人對此方案的態度是不建議,也不反對

2、使用@Lazy或者@ComponentScan(lazyInit = true)解決

本處以使用@Lazy為例:(強烈不建議使用@ComponentScan(lazyInit = true)作用範圍太廣了,容易產生誤傷)

@Service
public class B implements BInterface {
    @Lazy
    @Autowired
    private AInterface a;

    @Override
    public void funB() {
        System.out.println("執行緒名稱:" + Thread.currentThread().getName());
        a.funA();
    }
}

注意此@Lazy註解加的位置,因為a最終會是@Async的代理物件,所以在@Autowired它的地方加 另外,若不存在迴圈依賴而是直接引用a,是不用加@Lazy

只需要在Bean b的依賴屬性上加上@Lazy即可。(因為是B希望依賴進來的是最終的代理物件進來,所以B加上即可,A上並不需要加)

最終的結果讓人滿意:啟動正常,並且@Async非同步效果也生效了,因此本方案我是推薦的

但是需要稍微注意的是:此種情況下B裡持有A的引用和Spring容器裡的A並不是同一個,如下圖:

 

 

兩處例項a的地址值是不一樣的,容器內的是$Proxy@6914,B持有的是$Proxy@5899

關於@Autowired@Lazy的聯合使用為何是此現象,其實@Lazy的代理物件是由ContextAnnotationAutowireCandidateResolver生成的,具體參考博文:【小家Spring】Spring依賴注入(DI)核心介面AutowireCandidateResolver深度分析,解析@Lazy、@Qualifier註解的原理

3、不要讓@Async的Bean參與迴圈依賴 顯然如果方案3如果能夠解決它肯定是最優的方案。奈何它卻是現實情況中最為難達到的方案。 因為在實際業務開發中像迴圈依賴、類內方法呼叫等情況並不能避免,除非重新設計、按規範改變程式碼結構,因此此種方案就見仁見智吧~


為何@Transactional即使迴圈依賴也沒有問題呢?

最後回答小夥伴給我提問的這個問題:同為建立動態代理物件,同為一個註解標註在類上 / 方法上,為何@Transactional就不會出現這種啟動報錯呢?

其實這個問題的答案在上篇文章的後半拉已經解釋了,詳見 【小家Spring】一文告訴你Spring是如何利用"三級快取"巧妙解決Bean的迴圈依賴問題的

雖說他倆的原理都是產生代理物件,且註解的使用方式幾乎無異。so區別Spring對它哥倆的解析不同,也就是他們代理的建立的方式不同:

  • @Transactional使用的是自動代理建立器AbstractAutoProxyCreator,上篇文章詳細描述了,它實現了getEarlyBeanReference()方法從而很好的對迴圈依賴提供了支援
  • @Async的代理建立使用的是AsyncAnnotationBeanPostProcessor單獨的後置處理器實現的,它只在一處postProcessAfterInitialization()實現了對代理物件的建立,因此若出現它被迴圈依賴了,就會報錯如上~~~

so,雖然從表象上看這兩個註解的實現方式一樣,但細咬其實現過程細節上,兩者差異性還是非常明顯的。瞭解了實現方式上的差異後,自然就不難理解為何有報錯和有不報錯了~



最後,在理解原理的基礎上還需要注意如下這個case(加深理解),若是下面這種情況,其實是啟動不報錯且可以正常work的:

和上面示例相比:唯一區別是把@Async寫在bean B上而A沒有寫(上面是寫在bean A上而B中沒有寫)

@Service
public class A implements AInterface{
    @Autowired
    private BInterface b;
    @Override
    public void funA() {
    }
}

@Service
public class B implements BInterface {
    @Autowired
    private AInterface a;
    @Async // 寫在B的方法上  這樣B最終會被建立代理物件
    @Override
    public void funB() {
        a.funA();
    }
}

備註:若按照正常Spring容器會先初始化A,啟動就肯定是不會報錯的,這也就是我上面說的結論:這種情況下預設是可以work的

通過猜測也能夠猜到,A和B不是對等的關係,處理結果和Bean的初始化順序有關。

至於Spring對Bean的例項化、初始化順序,若沒有特別干預的情況下,它和類名字母排序有關~

為了說明問題,此處我人工干預先讓Spring容器初始化B(此處方案為使用@DependsOn("b")):

@DependsOn("b")
@Service
public class A implements AInterface { ... }

這樣干預能夠保證B肯定在A之前初始化,然後啟動也就會報同樣錯誤:(當然此處報錯資訊是bean b):

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'b': Bean with name 'b' has been injected into other beans [a] 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 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:622)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
	...

若技術敏感點的小夥伴發現,此處能夠給我們一個解決自己依賴自己問題的另外一個思路,是否可以考慮干預一下Bean的初始化順序來達到正常啟動的目的呢? 理論上是可行的,但是在實操過程中個人不太建議這麼去幹(如果有更好的方案的話)~



總結

雖然Spring官方也不推薦迴圈依賴,但是一個是理想情況,一個現實情況,它倆是有差距和差異的。 現實使用中,特別是業務開發中迴圈依賴可以說是幾乎避免不了的,因此知其然而知其所以然後,才能徹底的大徹大悟,遇到問題不再蒙圈。

使用AopContext.currentProxy();方式解決同類方法呼叫的方案,由於這種方式也是一個較大的話題,限於篇幅,且聽緊鄰的下文分解~

相關文章