1. 程式人生 > >Spring是如何解決迴圈依賴的

Spring是如何解決迴圈依賴的

## 前言 在面試的時候這兩年有一個非常高頻的關於spring的問題,那就是spring是如何解決迴圈依賴的。這個問題聽著就是輕描淡寫的一句話,其實考察的內容還是非常多的,主要還是考察的應聘者有沒有研究過spring的原始碼。但是說實話,spring的原始碼其實非常複雜的,研究起來並不是個簡單的事情,所以我們此篇文章只是為了解釋清楚Spring是如何解決迴圈依賴的這個問題。 ### 什麼樣的依賴算是迴圈依賴? 用過Spring框架的人都對**依賴注入**這個詞不陌生,一個Java類A中存在一個屬性是類B的一個物件,那麼我們就說類A的物件依賴類B,而在Spring中是依靠的IOC來實現的物件注入,也就是說建立物件的過程是IOC容器來實現的,並不需要自己在使用的時候通過new關鍵字來建立物件。 那麼當類A中依賴類B的物件,而類B中又依賴類C的物件,最後類C中又依賴類A的物件的時候,這種情況最終的依賴關係會形成一個環,這就是迴圈依賴。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200823170033101.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmlNb2Vy,size_25,color_c8cae6,t_70#pic_center) ### 迴圈依賴的型別 根據注入的時機可以分為兩種: * **構造器迴圈依賴** 依賴的物件是通過構造方法傳入的,在例項化bean的時候發生。 * **賦值屬性迴圈依賴** 依賴的物件是通過setter方法傳入的,物件已經例項化,在屬性賦值和依賴注入的時候發生。 **構造器迴圈依賴,本質上是無解的**,例項化A的時候呼叫A的構造器,發現依賴了B,又去例項化B,然後呼叫B的構造器,發現又依賴的C,然後呼叫C的構造器去例項化,結果發起C的構造器裡依賴了A,這就是個死迴圈無解。所以Spring也是不支援構造器迴圈依賴的,當發現存在構造器迴圈依賴時,會直接丟擲`BeanCurrentlyInCreationException` 異常。 **賦值屬性迴圈依賴,Spring只支援bean在單例模式下的迴圈依賴**,其他模式下的迴圈依賴Spring也是會丟擲`BeanCurrentlyInCreationException` 異常的。==Spring通過對還在建立過程中的單例bean,進行快取並提前暴露該單例,使得其他例項可以提前引用到該單例bean。== ### Spring為什麼只支援單例模式下的bean的賦值情況下的迴圈依賴 在prototype的模式下的bean,使用了一個ThreadLocal變數`prototypesCurrentlyInCreation`來記錄當前執行緒正在建立中的bean,這個變數在`AbtractBeanFactory`類裡。在建立前用beanName記錄bean,在建立完成後刪除bean。在`prototypesCurrentlyInCreation`裡採用了一個Set物件來儲存正在建立中的bean。我們都知道Set是不允許存在重複物件的,這樣就能保證同一個bean在一個執行緒中只能有一個正在建立。 下面是`prototypesCurrentlyInCreation`變數在刪除bean時的操作,在`AbtractBeanFactory`的`beforePrototypeCreation`操作裡。 ```java protected void afterPrototypeCreation(String beanName) { Object curVal = this.prototypesCurrentlyInCreation.get(); if (curVal instanceof String) { this.prototypesCurrentlyInCreation.remove(); } else if (curVal instanceof Set) { Set beanNameSet = (Set) curVal; beanNameSet.remove(beanName); if (beanNameSet.isEmpty()) { this.prototypesCurrentlyInCreation.remove(); } } } ``` 從上面的程式碼中看出,當變數為一個的時候採用了一個String物件來儲存,節省了一些記憶體空間。 在`AbstractBeanFactory`類的`doGetBean`方法裡先判斷是否為單例物件,不是單例物件,則直接判斷當前執行緒是否已經存在了正在建立的bean。存在的話直接丟擲異常。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200824225946260.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmlNb2Vy,size_60,color_c8cae6,t_70#pic_center) 這個`isPrototypeCurrentlyInCreation()`方法的實現程式碼如下: ```java protected boolean isPrototypeCurrentlyInCreation(String beanName) { Object curVal = this.prototypesCurrentlyInCreation.get(); return curVal != null && (curVal.equals(beanName) || curVal instanceof Set && ((Set)curVal).contains(beanName)); } ``` 因為有了這個機制,spring在原型模式下是解決不了bean的迴圈依賴的,當發現有迴圈依賴的時候會直接丟擲`BeanCurrentlyInCreationException`異常的。 ### 那麼為什麼spring在單例模式下的構造賦值也不支援迴圈依賴呢? 其實原理和原型模式下的情況類似,在單例模式下,bean也會用一個Set集合來儲存正在建立中的bean,在建立前儲存,建立完成後刪除。 這個物件在`DefaultSingletonBeanRegistry`類下變數名為:`singletonsCurrentlyInCreation` ```java public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry { private final Set singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap(16)); } ``` 判定程式碼在`DefaultSingletonBeanRegistry`類的`beforeSingletonCreation`方法下。 ```java protected void beforeSingletonCreation(String beanName) { if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } } ``` 在上面這個方法中,判定`singletonsCurrentlyInCreation`是否能成功的儲存一個單例bean。如果不能成功儲存,那麼就會直接丟擲`BeanCurrentlyInCreationException`異常。 ### 單例模式下的Setter賦值迴圈依賴 終於到了我們的重點,Spring是如何解決單例模式下的Setter賦值的迴圈依賴了。 其實主要的就是靠**提前暴露建立中的單例例項**。 那麼具體是一個怎樣的過程呢? 例如:上面那個圖的例子,A依賴B,B依賴C,C又依賴B。 過程如下: * **`建立A,呼叫構造方法,完成構造,進行屬性賦值注入,發現依賴B,去例項化B`**。 * **`建立B,呼叫構造方法,完成構造,進行屬性賦值注入,發現依賴C,去例項化C`**。 * 建立C,呼叫構造方法,完成構造,進行屬性賦值注入,發現依賴A。 **==這個時候就是解決迴圈依賴的關鍵了,因為A已經通過構造方法已經構造完成了,也就是說已經將Bean的在堆中分配好了記憶體,這樣即使A再填充屬性值也不會更改記憶體地址了,所以此時可以提前拿出來A的引用,來完成C的例項化。==** 這樣上面建立C過程就會變成了: * **`建立C,呼叫構造方法,完成構造,進行屬性賦值注入,發現依賴A,A已經構造完成,直接引用,完成C的例項化`**。 * **`C完成例項化後,注入B,B也完成了例項化,然後B注入A,A也完成了例項化`**。 為了能獲取到建立中單例bean,spring提供了三級快取來將正在建立中的bean提前暴露。 在類`DefaultSingletonBeanRegistry`下,即下圖紅框中的三個Map物件。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200825222038424.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmlNb2Vy,size_40,color_c8cae6,t_70#pic_center) 這三個快取Map的作用如下: * 一級快取,`singletonObjects` 單例快取,儲存已經例項化的單例bean。 * 二級快取,`earlySingletonObjects` 提前暴露的單例快取,這裡儲存的bean是剛剛構造完成,但還會通過屬性注入bean。 * 三級快取,`singletonFactories` 生產單例的工廠快取,儲存工廠。 首先在建立bean的時候會先建立一個和bean同名的單例工廠,並將bean先放入到單例工廠中。程式碼在`AbstractAutowireCapableBeanFactory`類的`doCreateBean`方法中。 ```java protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, Object[] args) throws BeanCreationException { ...... this.addSingletonFactory(beanName, new ObjectFactory() { public Object getObject() throws BeansException { return AbstractAutowireCapableBeanFactory.this.getEarlyBeanReference(beanName, mbd, bean); } }); ..... } ``` 而上面的程式碼中的`addSingletonFactory`方法的程式碼如下: ```java protected void addSingletonFactory(String beanName, ObjectFactory singletonFactory) { Assert.notNull(singletonFactory, "Singleton factory must not be null"); Map var3 = this.singletonObjects; synchronized(this.singletonObjects) { if (!this.singletonObjects.containsKey(beanName)) { this.singletonFactories.put(beanName, singletonFactory); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } } ``` `addSingletonFactory`方法的作用通過程式碼就可以看到是將存在了正在建立中的bean的單例工廠,放在三級快取裡,這樣保證了在迴圈依賴查詢的時候是可以找到bean的引用的。 具體讀取快取獲取bean的過程在類`DefaultSingletonBeanRegistry`的`getSingleton`方法裡。 如下原始碼: ```java protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) { Map var4 = this.singletonObjects; synchronized(this.singletonObjects) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { ObjectFactory singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject != NULL_OBJECT ? singletonObject : null; } ``` 通過上面的原始碼我們可以看到,在獲取單例Bean的時候,會先從一級快取`singletonObjects`裡獲取,如果沒有獲取到(說明不存在或沒有例項化完成),會去第二級快取`earlySingletonObjects`中去找,如果還是沒有找到的話,就會三級快取中獲取單例工廠`singletonFactory`,通過從`singletonFactory`中獲取正在建立中的引用,將`singletonFactory`儲存在`earlySingletonObjects` 二級快取中,這樣就將建立中的單例引用從三級快取中升級到了二級快取中,二級快取`earlySingletonObjects`,是會提前暴露已完成構造,還可以執行屬性注入的單例bean的。 這個時候如何還有其他的bean也是需要屬性注入,那麼就可以直接從`earlySingletonObjects`中獲取了。 上面的例子中的過程中的A,在注入C的時候,其實並沒有真正的初始化完成,等到順利的注入了B才算是真正的初始化完成。 整個過程如下圖: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200825233756793.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmlNb2Vy,size_60,color_FFFFFF,t_70#pic_