Spring Ioc原始碼分析系列--自動注入迴圈依賴的處理

2022-06-07 21:01:58

Spring Ioc原始碼分析系列--自動注入迴圈依賴的處理

前言

前面的文章Spring Ioc原始碼分析系列--Bean範例化過程(二)在講解到Spring建立bean出現迴圈依賴的時候並沒有深入去分析了,而是留到了這一篇去分析。為什麼要另起一篇,首先回圈依賴是個很經典的問題,也是面試屢屢被問到的問題,就這一點,就值得再起一篇。其次,迴圈依賴相對來說較為複雜,如果想要完全理解Spring解決迴圈依賴的設計思想需要對Spring有比較整體的認知,這裡要理清思路最好是新開一篇去寫。

Spring迴圈依賴相信都已經聽過很多次了,那麼Spring怎麼處理迴圈依賴的知道嗎?知道?不知道?亦或是一知半解知道個三級快取,但是再往深了去就不知道了?

先說迴圈依賴是個什麼,這個簡單,就是不同的bean出現了迴圈參照。如下圖所示,CycleACycleB出現了相互參照的情況,那麼這個時候就會出現了迴圈依賴。那麼這個時候在建立的時候就會陷入迴圈,除非有終止條件,不然會一直建立下去,直到資源耗盡。

看到這裡先不要急著往下看,如果是你,你會怎麼處理?思考一下。

那麼Spring是怎麼處理的呢?Spring是通過提前暴露bean參照來解決的,這相當於是破壞了迴圈等待這個條件,先前的迴圈依賴是依賴於一個完整的bean,提前暴露半成品bean參照可以完成迴圈依賴的處理。那麼提前暴露是怎麼完成的呢?Spring是通過三級快取來完成提前暴露的

那這篇文章就來仔細剖析一下Spring是怎麼處理迴圈依賴的。

構造例子

工欲善其事,必先利其器。概念鋪了很多,不搞個例子看看怎麼對得起觀眾。

這裡先指明現在網路上關於Spring迴圈依賴的一個誤區,現在大部分的文章都說構造器迴圈依賴是沒法處理的,只有setter方式的迴圈依賴能夠被處理,其實這是不對的,構造器迴圈依賴也是能被處理的,只是大家用的姿勢不對或者是不會用,文章後面會進行分析。

新建CycleA類,其中有屬性CycleB依賴於CycleB類,然後有個report()方法列印這兩個類的參照。

/**
 * @author Codegitz
 * @date 2022/6/7
 **/
@Component
public class CycleA {

	@Autowired
	private CycleB cycleB;

	public void report(){
		System.out.println("cycleA: " + this + " reference cycleB: " + cycleB);
	}
}

CycleB類的邏輯類似。

/**
 * @author Codegitz
 * @date 2022/6/7
 **/
@Component
public class CycleB {

	@Autowired
	private CycleA cycleA;

	public void report(){
		System.out.println("cycleB: " + this + " reference cycleA: " + cycleA);
	}
}

新建啟動類CycleApplication跑一跑。

/**
 * @author Codegitz
 * @date 2022/6/7
 **/
public class CycleApplication {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("io.codegitz.cycle");
		CycleB cycleB = (CycleB) applicationContext.getBean("cycleB");
		CycleA cycleA = (CycleA) applicationContext.getBean("cycleA");
		cycleA.report();
		cycleB.report();
	}
}

啟動完成後,發現迴圈參照是能夠被正常處理的。

原始碼分析

直接進入到核心邏輯裡,這裡的實現在AbstractAutowireCapableBeanFactory#doCreateBean()方法裡,核心部分如下圖所示,在該方法核心部分如圖所示,會先進行參照的提前暴露,然後再進行屬性填充,在進行屬性填充的時候,會再去建立cycleB,然後建立cycleB的時候會再來建立cycleA,這個時候由於前面已經提前暴露了cycleA的參照,這裡不會再進行cycleA的範例化,而是直接從快取中獲取半成品的cycleA

這就是最簡單情況下的迴圈依賴的處理,下面來分析一下細節。

半成品bean的範例化我們就略過了,如果想知道細節,可以參考上一篇文章Spring Ioc原始碼分析系列--範例化Bean的幾種方法裡面有詳細的分析。

跟進addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean))方法。可以看到這裡直接判斷一下一級快取裡面是否已存在該bean,如果不存在,則放入到三級快取裡。

這裡介紹一下三級快取分別是什麼以及裡面快取了什麼內容:

  • singletonObjects:單例物件的快取,也就是常說的一級快取,key-value為 bean 名稱到 bean 範例,這裡的範例是完整的bean。
  • earlySingletonObjects:早期單例物件的快取,也就是常說的二級快取,key-value為 bean 名稱到 bean 範例,這裡的範例是半成品的bean。
  • singletonFactories:單例工廠的快取,也就是常說的三級快取,key-value為 bean 名稱到 建立該bean的ObjectFactory

可以看到,具體在addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean))方法上,放入的就是一段建立bean範例的lambada表示式。

我們暫且先不管為什麼要呼叫() -> getEarlyBeanReference(beanName, mbd, bean)來返回bean,只需要記住通過這個ObjectFactory能獲取到一個bean就行。

後續我會再來解釋getEarlyBeanReference(beanName, mbd, bean)的作用,這裡也可以提前說一下,getEarlyBeanReference(beanName, mbd, bean)就是給建立代理提供了機會,例如一些出現了迴圈依賴的動態代理都是在這裡完成了動態代理的建立,然後返回動態代理bean。

到這裡cycleA已經完成了範例化和參照提前暴露,接下來就開始填充屬性,這時候就會去獲取屬性cycleB

cycleB會在哪裡進行獲取呢?由於這裡的迴圈依賴是使用@Autowired註解注入的,關於@Autowired的注入邏輯可以參考之前的文章Spring Ioc原始碼分析系列--@Autowired註解的實現原理,這裡不再贅述。

找到了注入點之後,就會呼叫DefaultListableBeanFactory#resolveDependency()來解析依賴,直接斷點到這裡。

跳過了一些繁雜的邏輯,老套路,回到了AbstractAutowireCapableBeanFactory#doCreateBean()方法裡,這裡進行cycleB的建立,隨後也會提前暴露cycleB的參照。

進入到提前暴露參照的addSingletonFactory()方法裡,可以看到這裡已經有了先前加入的cycleA的提前參照。這裡會把cycleB的參照加入,隨後進行cycleB的屬性填充階段,這個階段又會進行cycleA的建立,由於已經能獲取到cycleA的參照,所以不會再進行cycleA範例化。

cycleB的屬性填充就不再貼過程了,與上文類似。這裡貼一下呼叫鏈吧,可以跟著呼叫鏈回憶一下上篇文章的過程。

populateBean:1626, AbstractAutowireCapableBeanFactory -> 這是cycleB的屬性填充
postProcessProperties:406, AutowiredAnnotationBeanPostProcessor ->
inject:118, InjectionMetadata ->
inject:671, AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement ->
resolveDependency:1268, DefaultListableBeanFactory -> 解析cycleA依賴
doResolveDependency:1382, DefaultListableBeanFactory ->
resolveCandidate:283, DependencyDescriptor ->
getBean:204, AbstractBeanFactory -> 獲取cycleA
doGetBean:259, AbstractBeanFactory ->
getSingleton:175, DefaultSingletonBeanRegistry ->
getSingleton:193, DefaultSingletonBeanRegistry -> 這裡回到快取獲取cycleA

我們直接來到DefaultSingletonBeanRegistry#getSingleton()方法上,結合debug圖片一看,是不是就豁然開朗。

到了這裡,我們就可以來看下為什麼是要呼叫getEarlyBeanReference()方法了,我們跟程序式碼檢視。

	/**
	 * Obtain a reference for early access to the specified bean,
	 * typically for the purpose of resolving a circular reference.
	 *
	 * 獲取對指定 bean 的早期存取的參照,通常用於解析迴圈參照。
	 *
	 * @param beanName the name of the bean (for error handling purposes)
	 * @param mbd the merged bean definition for the bean
	 * @param bean the raw bean instance
	 * @return the object to expose as bean reference
	 */
	protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
		Object exposedObject = bean;
		if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
			for (BeanPostProcessor bp : getBeanPostProcessors()) {
				if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
					SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
					// 對 bean 再一次依賴參照
					// 主要應用 SmartInstantiationAwareBeanPostProcessor,
					// 其中我們熟知的 AOP 就是在這裡將 advice 動態織入 bean 中, 若沒有則直接返回 bean ,不做任何處理
					exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
				}
			}
		}
		return exposedObject;
	}

首先來看這個方法名的命名getEarlyBeanReference(),翻譯一下就是獲取提前的 Bean 參照。那麼思考一下提前是什麼意思,是相對於什麼提前了?

我個人的理解是建立代理的時機提前了,我們可以看到AbstractAutoProxyCreator#getEarlyBeanReference()方法的實現,該實現會呼叫wrapIfNecessary()方法進行代理的建立,而wrapIfNecessary()另一個呼叫點在postProcessAfterInitialization()方法裡,該方法會在物件範例化後、初始化完成後再進行呼叫。這裡是在屬性注入的時候就已經建立,所以相對而言,建立代理的時機提前了。當然這是我個人的理解,如果錯誤還請指正。

繼續偵錯,我們這裡並沒有代理的邏輯,所以返回的就是範例化後半成品的bean,該bean會被放入二級快取,三級快取會被清除。

這時候cycleB已經獲得了cycleA的參照,可以返回繼續cycleB的屬性填充,隨後cycleB已經完成填充,會被放入一級快取。隨後回到AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement#inject()方法裡,完成了cycleA的注入。此時cycleB是一個正常的bean了,回到cycleA的依賴解析。

可以看到cycleA的依賴解析也能獲取到cycleB了。

至此,cycleA也完成了注入,到這裡迴圈依賴完美解決。

再談構造器迴圈依賴

經過上面的分析,你覺得事情就完了嗎?是不是上面的解析是不是有點簡單了,感覺還意猶未盡。還記得開頭提到的一個觀點嗎?就是構造器迴圈依賴也是可以處理的,那麼我們來建立一個構造器迴圈依賴的例子。

簡單改造一下上面的例子,將@Autowired注入改為構造器注入。

CycleA類如下

@Component
public class CycleA {

	private CycleB cycleB;
	
	public CycleA(CycleB cycleB){
		this.cycleB = cycleB;
	}

	public void report(){
		System.out.println("cycleA: " + this + " reference cycleB: " + cycleB);
	}
}

CycleB類如下

@Component
public class CycleB {

	private CycleA cycleA;

	public CycleB(CycleA cycleA){
		this.cycleA = cycleA;
	}

	public void report(){
		System.out.println("cycleB: " + this + " reference cycleA: " + cycleA);
	}
}

其他的不變,啟動例子,會發現啟動報錯。

那麼為什麼構造器注入就不行了呢?很簡單,因為構造器注入的話,Spring沒辦法獲得一個半成品的bean,從而無法提早暴露參照,就會陷入死迴圈。

為什麼構造器注入沒法獲得半成品bean?因為建構函式需要的引數獲取不到,建構函式沒法呼叫,所以永遠沒法範例化一個半成品bean出來。

那就沒有辦法了嗎?方法是有的,按照剛剛說的條件,我們是因為建構函式的引數獲取不到,所以沒法執行建構函式,那有沒有辦法欺騙一下建構函式,給它返回一個偽引數,先讓它完成建構函式的呼叫,後續我們再去保證這個引數的正確性。

按照這個思路,怎麼樣能實現這個想法呢?很簡單,只需要在建構函式上面加上一個@Lazy註解,即可完成建構函式的呼叫。

	public CycleB(@Lazy CycleA cycleA){
		this.cycleA = cycleA;
	}

再啟動,會發現沒有報錯,這究竟是怎麼處理的呢?

關鍵在DefaultListableBeanFactory#resolveDependency()方法裡,這個方法是解析依賴的,這個方法的呼叫時機在前面的文章也有多次提到了,這裡不再贅述,貼一下程式碼。

可以看到這裡分了五種情況來處理:

  • Optional型別
  • ObjectFactory、ObjectProvider型別
  • javax.inject.Provider型別
  • @Lazy型別
  • 正常Bean型別

很顯然,構造器的引數加了@Lazy是屬於第四種型別,那麼接下來分析一個它是怎麼處理的。

	public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
			@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

		// descriptor代表當前需要注入的那個欄位,或者方法的引數,也就是注入點
		// ParameterNameDiscovery用於解析方法引數名稱
		descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
		// 1. Optional<T>
		if (Optional.class == descriptor.getDependencyType()) {
			return createOptionalDependency(descriptor, requestingBeanName);
		}
		// 2. ObjectFactory<T>、ObjectProvider<T>
		else if (ObjectFactory.class == descriptor.getDependencyType() ||
				ObjectProvider.class == descriptor.getDependencyType()) {
			//ObjectFactory和ObjectProvider類的特殊注入處理
			return new DependencyObjectProvider(descriptor, requestingBeanName);
		}
		// 3. javax.inject.Provider<T>
		else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
			return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);
		}
		else {
			// 4. @Lazy
			Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
					descriptor, requestingBeanName);
			if (result == null) {
				//通用處理邏輯
				// 5. 正常情況
				result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
			}
			return result;
		}
	}

直接把斷點打到這裡。我是在CycleB的建構函式上加了註解。

進入getLazyResolutionProxyIfNecessary()方法,這裡的實現在ContextAnnotationAutowireCandidateResolver類裡。

可以看到最終會呼叫buildLazyResolutionProxy()方法構建一個代理類返回,從而讓建構函式得以正常地走下去,破壞了迴圈等待的條件。

那這個代理是怎麼跟真正的cycleA產生聯絡的呢?答案在buildLazyResolutionProxy()方法的TargetSource裡,截圖上TargetSource的程式碼被我收起來了,展開程式碼檢視,豁然開朗,原來它會再去容器裡獲取依賴,只不過這時候容器已經存在該bean,@Lazy也如字面意思一樣做到了延遲載入該bean。

		TargetSource ts = new TargetSource() {
			@Override
			public Class<?> getTargetClass() {
				return descriptor.getDependencyType();
			}
			@Override
			public boolean isStatic() {
				return false;
			}
			@Override
			public Object getTarget() {
                // 這裡會重新解析依賴
				Object target = beanFactory.doResolveDependency(descriptor, beanName, null, null);
				if (target == null) {
					Class<?> type = getTargetClass();
					if (Map.class == type) {
						return Collections.emptyMap();
					}
					else if (List.class == type) {
						return Collections.emptyList();
					}
					else if (Set.class == type || Collection.class == type) {
						return Collections.emptySet();
					}
					throw new NoSuchBeanDefinitionException(descriptor.getResolvableType(),
							"Optional dependency not present for lazy injection point");
				}
				return target;
			}
			@Override
			public void releaseTarget(Object target) {
			}
		};

到這裡,構造器依賴我們也處理完了。細心的朋友會說,上面可不止@Lazy可以處理,別的型別能不能處理呢?答案是能的,也是類似的延遲載入策略,感興趣的可以繼續去手動操作一下。

總結

這一篇簡單介紹了一些迴圈依賴是什麼,然後通過@Autowired註解構造了迴圈依賴例子,隨後通過該例子進行了原始碼分析。但是這樣看起來似乎是太簡單了,所以我們又分析了構造器依賴的場景,普通的場景下的構造器依賴注入是會報錯的,但是我們有巧妙的方法能夠提供一個偽引數,讓建構函式能夠正常的執行,從而完成構造器的迴圈依賴注入。隨後我們分析了可以達到這個效果的原因何在,這一點是網路上很多文章都沒有考慮的誤區,除了@Lazy可以完成這個操作外,還有其他的幾種型別引數也能完成類似的功能,使用的都是延遲載入策略。

個人水平有限,如有錯誤,還請指出。

如果有人看到這裡,那在這裡老話重提。與君共勉,路漫漫其修遠兮,吾將上下而求索。