死磕Spring之IoC篇 - 單例 Bean 的迴圈依賴處理
阿新 • • 發佈:2021-03-04
> 該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 [Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀
>
> Spring 版本:5.1.14.RELEASE
>
> 開始閱讀這一系列文章之前,建議先檢視[**《深入瞭解 Spring IoC(面試題)》**](https://www.cnblogs.com/lifullmoon/p/14422101.html)這一篇文章
>
> 該系列其他文章請檢視:[**《死磕 Spring 之 IoC 篇 - 文章導讀》**](https://www.cnblogs.com/lifullmoon/p/14436372.html)
## 單例 Bean 的迴圈依賴處理
我們先回到[**《Bean 的建立過程》**](https://www.cnblogs.com/lifullmoon/p/14452842.html)中的**“從快取中獲取單例 Bean”**小節,當載入一個 Bean 時,會嘗試從**快取**(三個 Map)中獲取物件,如果未命中則進入後面建立 Bean 的過程。再來看到[**《Bean 的建立過程》**](https://www.cnblogs.com/lifullmoon/p/14452842.html)中的**“提前暴露當前 Bean”**小節,當獲取到一個例項物件(還未設定屬性和初始化)後,會將這個“早期物件”放入前面的快取中(第三個 Map),這裡暴露的物件實際是一個 ObjectFactory,可以通過它獲取“早期物件”。這樣一來,在後面設定屬性的過程中,如果需要依賴注入其他 Bean,且存在迴圈依賴,那麼上面的**快取**就避免了這個問題。接下來,將會分析 Spring 處理迴圈依賴的相關過程。
### 這裡的迴圈依賴是什麼?
迴圈依賴,其實就是迴圈引用,就是兩個或者兩個以上的 Bean 互相引用對方,最終形成一個閉環,如 A 依賴 B,B 依賴 C,C 依賴 A。
例如定義下面兩個物件:
學生類
```java
public class Student {
private Long id;
private String name;
@Autowired
private ClassRoom classRoom;
// 省略 getter、setter
}
```
教室類
```java
public class ClassRoom {
private String name;
@Autowired
private Collection students;
// 省略 getter、setter
}
```
當載入 Student 這個物件時,需要注入一個 ClassRoom 物件,就需要去載入 ClassRoom 這個物件,此時又要去依賴注入所有的 Student 物件,這裡的 Student 和 ClassRoom 就存在迴圈依賴,那麼一直這樣迴圈下去,除非有**終結條件**。
Spring 只處理**單例** Bean 的迴圈依賴,**原型**模式的 Bean 如果存在迴圈依賴直接**丟擲異常**,**單例** Bean 的迴圈依賴的**場景**有兩種:
- 構造器注入出現迴圈依賴
- 欄位(或 Setter)注入出現迴圈依賴
對於構造器注入出現快取依賴,Spring 是無法解決的,因為當前 Bean 還未例項化,無法提前暴露物件,所以只能丟擲異常,**接下來我們分析的都是欄位(或 Setter)注入出現迴圈依賴的處理**
### 迴圈依賴的處理
#### 1. 嘗試從快取中獲取單例 Bean
可以先回到[**《Bean 的建立過程》**](https://www.cnblogs.com/lifullmoon/p/14452842.html)中的**“從快取中獲取單例 Bean”**小節,在獲取一個 Bean 過程中,首先會從快取中嘗試獲取物件,對應程式碼段:
```java
// AbstractBeanFactory#doGetBean(...) 方法
Object sharedInstance = getSingleton(beanName);
// DefaultSingletonBeanRegistry.java
public Object getSingleton(String beanName) {
return getSingleton(beanName, true);
}
// DefaultSingletonBeanRegistry.java
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// <1> **【一級 Map】**從單例快取 `singletonObjects` 中獲取 beanName 對應的 Bean
Object singletonObject = this.singletonObjects.get(beanName);
// <2> 如果**一級 Map**中不存在,且當前 beanName 正在建立
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// <2.1> 對 `singletonObjects` 加鎖
synchronized (this.singletonObjects) {
// <2.2> **【二級 Map】**從 `earlySingletonObjects` 集合中獲取,裡面會儲存從 **三級 Map** 獲取到的正在初始化的 Bean
singletonObject = this.earlySingletonObjects.get(beanName);
// <2.3> 如果**二級 Map** 中不存在,且允許提前建立
if (singletonObject == null && allowEarlyReference) {
// <2.3.1> **【三級 Map】**從 `singletonFactories` 中獲取對應的 ObjectFactory 實現類
ObjectFactory> singletonFactory = this.singletonFactories.get(beanName);
// 如果從**三級 Map** 中存在對應的物件,則進行下面的處理
if (singletonFactory != null) {
// <2.3.2> 呼叫 ObjectFactory#getOject() 方法,獲取目標 Bean 物件(早期半成品)
singletonObject = singletonFactory.getObject();
// <2.3.3> 將目標物件放入**二級 Map**
this.earlySingletonObjects.put(beanName, singletonObject);
// <2.3.4> 從**三級 Map**移除 `beanName`
this.singletonFactories.remove(beanName);
}
}
}
}
// <3> 返回從快取中獲取的物件
return singletonObject;
}
```
這裡的快取指的就是上面三個 Map 物件:
- `singletonObjects`(一級 Map):裡面儲存了所有已經初始化好的單例 Bean,也就是會儲存 Spring IoC 容器中所有單例的 Spring Bean
- `earlySingletonObjects`(二級 Map),裡面會儲存從 **三級 Map** 獲取到的正在初始化的 Bean
- `singletonFactories`(三級 Map),裡面儲存了正在初始化的 Bean 對應的 ObjectFactory 實現類,呼叫其 getObject() 方法返回正在初始化的 Bean 物件(僅例項化還沒完全初始化好)
過程如下:
1. **【一級 Map】**從單例快取 `singletonObjects` 中獲取 beanName 對應的 Bean
2. 如果**一級 Map**中不存在,且當前 beanName 正在建立
1. 對 `singletonObjects` 加鎖
2. **【二級 Map】**從 `earlySingletonObjects` 集合中獲取,裡面會儲存從 **三級 Map** 獲取到的正在初始化的 Bean
3. 如果**二級 Map** 中不存在,且允許提前建立
1. **【三級 Map】**從 `singletonFactories` 中獲取對應的 ObjectFactory 實現類,如果從**三級 Map** 中存在對應的物件,則進行下面的處理
2. 呼叫 ObjectFactory#getOject() 方法,獲取目標 Bean 物件(早期半成品)
3. 將目標物件放入**二級 Map**
4. 從**三級 Map**移除 beanName
3. 返回從快取中獲取的物件
#### 2. 提前暴露當前 Bean
回到[**《Bean 的建立過程》**](https://www.cnblogs.com/lifullmoon/p/14452842.html)中的**“提前暴露當前 Bean”**小節,在獲取到例項物件後,如果是單例模式,則提前暴露這個例項物件,對應程式碼段:
```java
// AbstractAutowireCapableBeanFactory#doCreateBean(...) 方法
// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
// <3> 提前暴露這個 `bean`,如果可以的話,目的是解決單例模式 Bean 的迴圈依賴注入
// <3.1> 判斷是否可以提前暴露
boolean earlySingletonExposure = (mbd.isSingleton() // 單例模式
&& this.allowCircularReferences // 允許迴圈依賴,預設為 true
&& isSingletonCurrentlyInCreation(beanName)); // 當前單例 bean 正在被建立,在前面已經標記過
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
/**
* <3.2>
* 建立一個 ObjectFactory 實現類,用於返回當前正在被建立的 `bean`,提前暴露,儲存在 `singletonFactories` (**三級 Map**)快取中
*
* 可以回到前面的 {@link AbstractBeanFactory#doGetBean#getSingleton(String)} 方法
* 載入 Bean 的過程會先從快取中獲取單例 Bean,可以避免單例模式 Bean 迴圈依賴注入的問題
*/
addSingletonFactory(beanName,
// ObjectFactory 實現類
() -> getEarlyBeanReference(beanName, mbd, bean));
}
```
如果是**單例模式**、**允許迴圈依賴**(預設為 true)、當前單例 Bean **正在被建立**(前面已經標記過),則提前暴露
這裡會先通過 Lambda 表示式建立一個 ObjectFactory 實現類,如下:
```java
// AbstractAutowireCapableBeanFactory.java
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() // RootBeanDefinition 不是使用者定義的(由 Spring 解析出來的)
&& hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}
```
入參 `bean` 為當前 Bean 的例項物件(未初始化),這個實現類允許通過 SmartInstantiationAwareBeanPostProcessor 對這個提前暴露的物件進行處理,最終會返回這個提前暴露的物件。注意,這裡也可以返回一個代理物件。
有了這個 ObjectFactory 實現類後,就需要往**快取**中存放了,如下:
```java
// DefaultSingletonBeanRegistry.java
protected void addSingletonFactory(String beanName, ObjectFactory> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
```
可以看到會將這個 ObjectFactory 往 `singletonFactories` (三級 Map)中存放,到這裡對於 Spring 對單例 Bean 迴圈依賴的處理是不是就非常清晰了
#### 3. 快取單例 Bean
在完全初始化好一個單例 Bean 後,會快取起來,如下:
```java
// DefaultSingletonBeanRegistry.java
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
```
往 `singletonObjects`(一級 Map)存放當前單例 Bean,同時從 `singletonFactories`(三級 Map)和 `earlySingletonObjects`(二級 Map)中移除
### 總結
Spring 只處理單例 Bean 的欄位(或 Setter)注入出現迴圈依賴,對於構造器注入出現的迴圈依賴會直接丟擲異常。還有就是如果是通過 `denpends-on` 配置的依賴出現了迴圈,也會丟擲異常,所以我覺得這裡的“迴圈依賴”換做“迴圈依賴注入”是不是更合適一點
Spring 處理迴圈依賴的解決方案如下:
- Spring 在建立 Bean 的過程中,獲取到例項物件後會提前暴露出去,生成一個 ObjectFactory 物件,放入 `singletonFactories`(三級 Map)中
- 在後續設定屬性過程中,如果出現迴圈,則可以通過 `singletonFactories`(三級 Map)中對應的 ObjectFactory#getObject() 獲取這個早期物件,避免再次初始化
問題一:為什麼需要上面的 **二級 Map** ?
> 因為通過 **三級 Map**獲取 Bean 會有相關 SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference 的處理,避免重複處理,處理後返回的可能是一個代理物件
>
> 例如在迴圈依賴中一個 Bean 可能被多個 Bean 依賴, A -> B(也依賴 A) -> C -> A,當你獲取 A 這個 Bean 時,後續 B 和 C 都要注入 A,沒有上面的 **二級 Map**的話,**三級 Map** 儲存的 ObjectFactory 實現類會被呼叫兩次,會重複處理,可能出現問題,這樣做在效能上也有所提升
問題二:為什麼不直接呼叫這個 ObjectFactory#getObject() 方法放入 **二級Map**中,而需要 **三級 Map**?
> 對於沒有不涉及到 AOP 的 Bean 確實可以不需要 `singletonFactories`(三級 Map),但是 Spring AOP 就是 Spring 體系中的一員,如果沒有`singletonFactories`(三級 Map),意味著 Bean 在例項化後就要完成 AOP 代理,這樣違背了 Spring 的設計原則。Spring 是通過 `AnnotationAwareAspectJAutoProxyCreator` 這個後置處理器在完全建立好 Bean 後來完成 AOP 代理,而不是在例項化後就立馬進行 AOP 代理。如果出現了迴圈依賴,那沒有辦法,只有給 Bean 先建立代理物件,但是在沒有出現迴圈依賴的情況下,設計之初就是讓 Bean 在完全建立好後才完成 AOP