Spring中的迴圈依賴解決詳解

迴圈引用警告怎麼取消

原 https://www。cnblogs。com/leeego-123/p/12165278。html

前言

說起Spring中迴圈依賴的解決辦法,相信很多園友們都或多或少的知道一些,但當真的要詳細說明的時候,可能又沒法一下將它講清楚。本文就試著儘自己所能,對此做出一個較詳細的解讀。另,需注意一點,下文中會出現類的例項化跟類的初始化兩個短語,為怕園友迷惑,事先宣告一下,本文的例項化是指剛執行完構造器將一個物件new出來,但還未填充屬性值的狀態,而初始化是指完成了屬性的依賴注入。

一、先說說Spring解決的迴圈依賴是什麼

Java中的迴圈依賴分兩種,一種是構造器的迴圈依賴,另一種是屬性的迴圈依賴。

構造器的迴圈依賴就是在構造器中有屬性迴圈依賴

,如下所示的兩個類就屬於構造器迴圈依賴:

@Servicepublic class Student { @Autowired private Teacher teacher; public Student (Teacher teacher) { System。out。println(“Student init1:” + teacher); } public void learn () { System。out。println(“Student learn”); }}

@Servicepublic class Teacher { @Autowired private Student student; public Teacher (Student student) { System。out。println(“Teacher init1:” + student); } public void teach () { System。out。println(“teach:”); student。learn(); }}

這種迴圈依賴沒有什麼解決辦法,因為JVM虛擬機器在對類進行例項化的時候,需先例項化構造器的引數,而由於迴圈引用這個引數無法提前例項化,故只能丟擲錯誤。

Spring解決的迴圈依賴就是指屬性的迴圈依賴

,如下所示:

@Servicepublic class Teacher { @Autowired private Student student; public Teacher (Student student) { System。out。println(“Teacher init1:” + student); } public void teach () { System。out。println(“teach:”); student。learn(); }}

@Servicepublic class Student { @Autowired private Teacher teacher; public Student () { System。out。println(“Student init:” + teacher); } public void learn () { System。out。println(“Student learn”); }}

測試掃描類:

@ComponentScan(value = “myPackage”)public class ScanConfig {    }

測試啟動類:

public class SpringTest { public static void main(String[] args) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ScanConfig。class);        applicationContext。getBean(Teacher。class)。teach(); }}

測試類執行結果:

1 Student init:null2 Teacher init:null3 teach:4 Student learn

可以看到,在構造器執行的時候未完成屬性的注入,而在呼叫方法的時候已經完成了注入。下面就一起看看Spring內部是在何時完成的屬性注入,又是如何解決的迴圈依賴。

二、迴圈依賴與屬性注入

1、對於非懶載入的類,是在refresh方法中的

finishBeanFactoryInitialization(beanFactory) 方法完成的包掃描以及bean的初始化

,下面就一起追蹤下去。

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {     // 其他程式碼     // Instantiate all remaining (non-lazy-init) singletons。     beanFactory。preInstantiateSingletons();}

可以看到呼叫了beanFactory的一個方法,此處的beanFactory就是指我們最常見的那個DefaultListableBeanFactory,下面看它裡面的這個方法。

2、DefaultListableBeanFactory的preInstantiateSingletons方法

public void preInstantiateSingletons() throws BeansException { List beanNames = new ArrayList<>(this。beanDefinitionNames); // Trigger initialization of all non-lazy singleton beans。。。 for (String beanName : beanNames) { RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); if (!bd。isAbstract() && bd。isSingleton() && !bd。isLazyInit()) { // 判斷為非抽象類、是單例、非懶載入 才給初始化 if (isFactoryBean(beanName)) { // 無關程式碼(針對FactoryBean的處理) } else { // 重要!!!普通bean就是在這裡初始化的 getBean(beanName); } } } // 其他無關程式碼 }

可以看到,就是在此方法中迴圈Spring容器中所有的bean,依次對其進行初始化,初始化的入口就是getBean方法

3、AbstractBeanFactory的getBean跟doGetBean方法

追蹤getBean方法:

public Object getBean(String name) throws BeansException { return doGetBean(name, null, null, false);}

可見引用了過載的doGetBean方法,繼續追蹤之:

protected T doGetBean(final String name, @Nullable final Class requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { final String beanName = transformedBeanName(name); Object bean; // 方法1)從三個map中獲取單例類 Object sharedInstance = getSingleton(beanName); // 省略無關程式碼 } else { // 如果是多例的迴圈引用,則直接報錯 if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } // 省略若干無關程式碼 try { // Create bean instance。 if (mbd。isSingleton()) { // 方法2) 獲取單例物件 sharedInstance = getSingleton(beanName, () -> { try { //方法3) 建立ObjectFactory中getObject方法的返回值 return createBean(beanName, mbd, args); } catch (BeansException ex) { // Explicitly remove instance from singleton cache: It might have been put there // eagerly by the creation process, to allow for circular reference resolution。 // Also remove any beans that received a temporary reference to the bean。 destroySingleton(beanName); throw ex; } }); bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } } // 省略若干無關程式碼 return (T) bean; }

該方法比較長,對於解決迴圈引用來說,上面標出來的3個方法起到了至關重要的作用,下面我們挨個攻克。

3.1) getSingleton(beanName)方法

注意該方法跟方法2)是過載方法,名字一樣內部邏輯卻大相徑庭。

protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject = this。singletonObjects。get(beanName);// 步驟A if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this。singletonObjects) { singletonObject = this。earlySingletonObjects。get(beanName);// 步驟B if (singletonObject == null && allowEarlyReference) { ObjectFactory<?> singletonFactory = this。singletonFactories。get(beanName);// 步驟C if (singletonFactory != null) { singletonObject = singletonFactory。getObject(); this。earlySingletonObjects。put(beanName, singletonObject); this。singletonFactories。remove(beanName); } } } } return singletonObject; }

透過上面的步驟可以看出這三個map的優先順序。其中singletonObjects裡面存放的是初始化之後的單例物件;earlySingletonObjects中存放的是一個已完成例項化未完成初始化的早期單例物件;而singletonFactories中存放的是ObjectFactory物件,此物件的getObject方法返回值即剛完成例項化還未開始初始化的單例物件。

所以先後順序是,單例物件先存在於singletonFactories中,後存在於earlySingletonObjects中,最後初始化完成後放入singletonObjects中

當debug到此處時,以上述Teacher和Student兩個迴圈引用的類為例,如果第一個走到這一步的是Teacher,則從此處這三個map中get到的值都是空,因為還未新增進去。這個方法主要是給迴圈依賴中後來過來的物件用。

3.2)getSingleton(String beanName, ObjectFactory<?> singletonFactory)方法

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { Assert。notNull(beanName, “Bean name must not be null”); synchronized (this。singletonObjects) { Object singletonObject = this。singletonObjects。get(beanName); if (singletonObject == null) { // 省略無關程式碼 beforeSingletonCreation(beanName); // 步驟A boolean newSingleton = false; // 省略無關程式碼 try { singletonObject = singletonFactory。getObject();// 步驟B newSingleton = true; } // 省略無關程式碼 finally { if (recordSuppressedExceptions) { this。suppressedExceptions = null; } afterSingletonCreation(beanName);// 步驟C } if (newSingleton) { addSingleton(beanName, singletonObject);// 步驟D } } return singletonObject; } }

獲取單例物件的主要邏輯就是此方法實現的

,主要分為上面四個步驟,繼續挨個看:

步驟A:

1 protected void beforeSingletonCreation(String beanName) {2 // 判斷,並首次將beanName即teacher放入singletonsCurrentlyInCreation中3 if (!this。inCreationCheckExclusions。contains(beanName) && !this。singletonsCurrentlyInCreation。add(beanName)) {4 throw new BeanCurrentlyInCreationException(beanName);5 }6 }

步驟C:

protected void afterSingletonCreation(String beanName) {         // 得到單例物件後,再講beanName從singletonsCurrentlyInCreation中移除         if (!this。inCreationCheckExclusions。contains(beanName) && !this。singletonsCurrentlyInCreation。remove(beanName)) {             throw new IllegalStateException(“Singleton ‘” + beanName + “’ isn‘t currently in creation”);         }}

步驟D:

protected void addSingleton(String beanName, Object singletonObject) {         synchronized (this。singletonObjects) {             this。singletonObjects。put(beanName, singletonObject);//新增單例物件到map中             this。singletonFactories。remove(beanName);//從早期暴露的工廠中移除,此map在解決迴圈依賴中發揮了關鍵的作用             this。earlySingletonObjects。remove(beanName);//從早期暴露的物件map中移除             this。registeredSingletons。add(beanName);//新增到已註冊的單例名字集合中         }     }

步驟B:

此處呼叫了ObjectFactory的getObject方法,此方法是在哪裡實現的呢?返回的又是什麼?且往回翻,找到3中的方法3,對java8函數語言程式設計有過了解的園友應該能看出來,方法3 【createBean(beanName, mbd, args)】的返回值就是getObject方法的返回值,即方法3返回的就是我們需要的單例物件,下面且追蹤方法3而去。

3.3)

AbstractAutowireCapableBeanFactory#createBean(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.Object[]) 方法

protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { // 省略無關程式碼 try { Object beanInstance = doCreateBean(beanName, mbdToUse, args); return beanInstance; } // 省略無關程式碼 }

去掉無關程式碼之後,關鍵方法只有doCreateBean方法,追蹤之:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException { BeanWrapper instanceWrapper = null; // 省略程式碼 if (instanceWrapper == null) { // 例項化bean instanceWrapper = createBeanInstance(beanName, mbd, args); } boolean earlySingletonExposure = (mbd。isSingleton() && this。allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { // 重點!!!將例項化的物件新增到singletonFactories中 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } // 初始化bean Object exposedObject = bean; try { populateBean(beanName, mbd, instanceWrapper);//也很重要 exposedObject = initializeBean(beanName, exposedObject, mbd); } // 省略無關程式碼 return exposedObject;}

上面註釋中標出的重點是此方法的關鍵。在addSingletonFactory方法中,將第二個引數ObjectFactory存入了singletonFactories供其他物件依賴時呼叫。然後下面的

populateBean方法對剛例項化的bean進行屬性注入(該方法關聯較多,本文暫時不展開追蹤了,有興趣的園友自行檢視即可)

,如果遇到Spring中的物件屬性,則再透過getBean方法獲取該物件。至此,迴圈依賴在Spring中的處理過程已經追溯完畢,下面我們總結一下。

小結

屬性注入主要是在populateBean方法中進行的。對於迴圈依賴,以我們上文中的Teacher中注入了Student、Student中注入了Teacher為例來說明,假定Spring的載入順序為先載入Teacher,再載入Student。

getBean方法觸發Teacher的初始化後:

a。 首先走到3中的方法1),此時map中都為空,獲取不到例項;

b。 然後走到方法2)中,步驟A、步驟C、步驟D為控制map中資料的方法,實現簡單,可暫不關注。其中步驟B的getObject方法觸發對方法3)的呼叫;

c。 在方法3)中,先透過createBeanInstance例項化Teacher物件,又將該例項化的物件透過addSingletonFactory方法放入singletonFactories中,完成Teacher物件早期的暴露;

d。 然後在方法3)中透過populateBean方法對Teacher物件進行屬性的注入,發現它有一個Student屬性,則觸發getBean方法對Student進行初始化

e。 重複a、b、c步驟,只是此時要初始化的是Student物件

f。 走到d的時候,呼叫populateBean對Student物件進行屬性注入,發現它有一個Teacher屬性,則觸發getBean方法對Teacher進行初始化;

g。 對Teacher進行初始化,又來到a,但此時map已經不為空了,因為之前在c步驟中已經將Teacher例項放入了singletonFactories中,a中得到Teacher例項後返回;

h。完成f中對Student的初始化,繼而依次往上回溯完成Teacher的初始化;

完成Teacher的初始化後,Student的初始化就簡單了,因為map中已經存了這個單例。

至此,Spring迴圈依賴的總結分析結束,一句話來概括一下:

Spring透過將例項化後的物件提前暴露給Spring容器中的singletonFactories,解決了迴圈依賴的問題

後臺私信回覆 1024 免費領取 SpringCloud、SpringBoot,微信小程式、Java面試、資料結構、演算法等全套影片資料。