這次徹底搞懂Spring循環(huán)依賴
今天我們討論 Spring 框架所提供的核心功能之一:依賴注入。
依賴注入可以說是使用 Spring 框架的基本手段,我們通過它獲取所需的各種 Bean。但在使用不同的依賴注入類型時(shí),經(jīng)常會(huì)碰到循環(huán)依賴問題。為了深入分析這一問題的解決方案,我們先從 Spring 依賴注入的類型和循環(huán)依賴的基本概念開始講起。
Spring 依賴注入和循環(huán)依賴
Spring 為開發(fā)人員提供了三種不同的依賴注入類型,分別是字段注入、構(gòu)造器注入和 Setter 方法注入。
Spring 框架的三種依賴注入類型
其中,字段注入是最常用、也是最容易使用的一種。但是,它也是三種注入方式中最應(yīng)該避免使用的。因?yàn)樗赡軐?dǎo)致潛在的循環(huán)依賴。所謂循環(huán)依賴,就是兩個(gè)類之間互相注入,例如這段示例代碼:
public class ClassA {
@Autowired
private ClassB classB;
}
public class ClassB {
@Autowired
private ClassA classA;
}
顯然,這里的 ClassA 和 ClassB 通過@Autowired 注解相互注入,發(fā)生了循環(huán)依賴。在 Spring 中,上述代碼是合法的,容器啟動(dòng)時(shí)并不會(huì)報(bào)任何錯(cuò)誤,只有在使用到具體某個(gè) ClassA 或 ClassB 時(shí),才會(huì)報(bào)錯(cuò)。
事實(shí)上,Spring 官方也不推薦開發(fā)人員使用字段注入這種注入模式,而是推薦構(gòu)造器注入。基于構(gòu)造器注入,前面介紹的 ClassA 和 ClassB 之間的循環(huán)依賴關(guān)系是這樣的:
public class ClassA {
private ClassB classB;
@Autowired
public ClassA(ClassB classB) {
this.classB = classB;
}
}
public class ClassB {
private ClassA classA;
@Autowired
public ClassB(ClassA classA) {
this.classA = classA;
}
}
這時(shí)候,如果啟動(dòng) Spring 容器,就會(huì)拋出一個(gè)循環(huán)依賴異常,提醒你應(yīng)該避免循環(huán)依賴。
其實(shí),Setter 方法注入可以很好地解決循環(huán)依賴問題,如下所示的代碼是可以正確執(zhí)行的:
public class ClassA {
private ClassB classB;
@Autowired
public void setClassB(ClassB classB) {
this.classB = classB;
}
}
public class ClassB {
private ClassA classA;
@Autowired
public void setClassA(ClassA classA) {
this.classA = classA;
}
}
請(qǐng)注意,上述代碼能夠正確執(zhí)行的前提是:ClassA 和 ClassB 的作用域都是“Singleton”,即單例。所謂的單例,指的就是不管對(duì)某一個(gè)類的引用有多少個(gè),容器只會(huì)創(chuàng)建該類的一個(gè)實(shí)例。
講到這里,你可能會(huì)好奇,這就需要剖析 Spring 中對(duì)于單例 Bean 的存儲(chǔ)和獲取方式,讓我們一起來看一下。
Spring 循環(huán)依賴解決方案
對(duì)于單例作用域來說,在 Spring 容器的整個(gè)生命周期內(nèi),有且僅有一個(gè) Bean 對(duì)象,所以很容易想到這個(gè)對(duì)象應(yīng)該位于緩存中。Spring 為了解決單例 Bean 的循環(huán)依賴問題,使用了三級(jí)緩存。這是 Spring 在設(shè)計(jì)和實(shí)現(xiàn)上的一大特色,也是面試過程中經(jīng)常遇到的話題。
三級(jí)緩存結(jié)構(gòu)
所謂的三級(jí)緩存,在 Spring 中表現(xiàn)為三個(gè) Map 對(duì)象,定義在 DefaultSingletonBeanRegistry 類中,如下所示:
/** 單例對(duì)象的緩存: Bean名稱 --> Bean實(shí)例 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 提前暴露的單例對(duì)象的緩存: Bean名稱 --> Bean實(shí)例(屬性未注入)*/
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/** 單例對(duì)象工廠的緩存: Bean名稱 --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
請(qǐng)注意:
這里的 singletonObjects 就是第一級(jí)緩存,用來持有完整的 Bean 實(shí)例。
而 earlySingletonObjects 中存放的是那些提前暴露的對(duì)象,也就是已經(jīng)創(chuàng)建但還沒有完成屬性注入的對(duì)象,屬于第二級(jí)緩存。
最后的 singletonFactories 存放用來創(chuàng)建 earlySingletonObjects 的工廠對(duì)象,屬于第三級(jí)緩存。
三級(jí)緩存與保存的對(duì)象
那么三級(jí)緩存是如何發(fā)揮作用的呢?讓我們來分析獲取 Bean 的代碼流程:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
//首先從一級(jí)緩存singletonObjects中獲取
Object singletonObject = this.singletonObjects.get(beanName);
//如果獲取不到,就從二級(jí)緩存earlySingletonObjects中獲取
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
//如果還是獲取不到,就從三級(jí)緩存singletonFactory中獲取
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
//一旦獲取成功,就把對(duì)象從第三級(jí)緩存移動(dòng)到第二級(jí)緩存中
.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
我們首先從一級(jí)緩存 singletonObjects 中獲取目前對(duì)象,如果獲取不到,則從二級(jí)緩存 earlySingletonObjects 中獲??;如果還是獲取不到,就從三級(jí)緩存 singletonFactory 中通過 ObjectFactory 進(jìn)行獲取。而一旦獲取成功,就會(huì)把目標(biāo)對(duì)象從第三級(jí)緩存移動(dòng)到第二級(jí)緩存中,從而為下一次對(duì)象獲取過程做準(zhǔn)備。
通過這段代碼,我們了解了三級(jí)緩存的依次訪問過程,但可能你還是不理解 Spring 為什么要這樣設(shè)計(jì)。事實(shí)上,解決循環(huán)依賴的關(guān)鍵點(diǎn)還是在 Bean 的生命周期上。
在通過@Autowired 注解注入 Bean 時(shí),真正完成 Bean 的創(chuàng)建是在一個(gè) doCreateBean 方法中,該方法包括三個(gè)核心步驟:
通過 createBeanInstance 方法實(shí)例化 Bean。
通過 populateBean 方法實(shí)現(xiàn)屬性的注入。
最后通過 initializeBean 方法對(duì) Bean 進(jìn)行擴(kuò)展。
單例對(duì)象的初始化步驟示意圖
對(duì)應(yīng)的代碼結(jié)構(gòu)如下所示:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException {
//1. 初始化Bean實(shí)例
instanceWrapper = createBeanInstance(beanName, mbd, args);
//針對(duì)循環(huán)依賴問題暴露單例工廠類
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
1
//2. 對(duì)Bean實(shí)例執(zhí)行屬性注入
populateBean(beanName, mbd, instanceWrapper);
//3. 執(zhí)行初始化Bean實(shí)例的回調(diào)
exposedObject = initializeBean(beanName, exposedObject, mbd);
return exposedObject;
}
上面的第 1 步完成了 Bean 的初始化,而第 2 步才完成 Bean 的完整實(shí)例化。我們看到在第 1 步和第 2 步之間,存在一個(gè) addSingletonFactory 方法,用于初始化這個(gè)第三級(jí)緩存中的數(shù)據(jù)。
Spring 解決循環(huán)依賴的訣竅就在于 singletonFactories 這個(gè)第三級(jí)緩存:
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
//添加Bean到第三級(jí)緩存中
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
請(qǐng)注意,這段代碼的執(zhí)行時(shí)機(jī)是 Bean 已經(jīng)通過構(gòu)造函數(shù)進(jìn)行創(chuàng)建,但還沒有完成 Bean 中完整屬性的注入。換句話說,Bean 已經(jīng)可以被暴露出來進(jìn)行識(shí)別了,但還不能正常使用。接下來我們就來分析一下為什么通過這種機(jī)制就能解決循環(huán)依賴問題。
循環(huán)依賴解決方案
我們繼續(xù)討論 ClassA 和 ClassB 的循環(huán)依賴關(guān)系,基于 Setter 方法注入,整個(gè)流程如下所示,我們用紅色部分表示 ClassA 的創(chuàng)建過程,用黃色部分表示 ClassB 的創(chuàng)建過程。
基于 Setter 注入的循環(huán)依賴解決流程
圖中,假設(shè)我們先初始化 ClassA。ClassA 首先通過 createBeanInstance 方法創(chuàng)建了實(shí)例,并且將這個(gè)實(shí)例提前暴露到第三級(jí)緩存 singletonFactories 中。然后,ClassA 嘗試通過 populateBean 方法注入屬性,發(fā)現(xiàn)自己依賴 ClassB 這個(gè)屬性,就會(huì)嘗試去獲取 ClassB 的實(shí)例。
顯然,這時(shí)候 ClassB 還沒有被創(chuàng)建,所以走創(chuàng)建流程。
ClassB 在初始化第一步的時(shí)候發(fā)現(xiàn)自己依賴了 ClassA,就會(huì)嘗試從第一級(jí)緩存 singletonObjects 去獲取 ClassA 的實(shí)例。因?yàn)?ClassA 這時(shí)候還沒有完全創(chuàng)建完畢,所以第一級(jí)緩存中不存在,同樣第二級(jí)緩存中也不存在。當(dāng)嘗試訪問第三級(jí)緩存時(shí),因?yàn)?ClassA 已經(jīng)提前暴露了,所以 ClassB 能夠通過 singletonFactories 拿到 ClassA 對(duì)象并順利完成所有初始化流程。
ClassB 對(duì)象創(chuàng)建完成之后會(huì)把自己放到第一級(jí)緩存中,這時(shí)候 ClassA 就能從第一級(jí)緩存中獲取 ClassB 的實(shí)例,進(jìn)而完成 ClassA 的所有初始化流程。
講到這里,相信你能理解為什么構(gòu)造器注入無(wú)法解決循環(huán)依賴問題了。這是因?yàn)闃?gòu)造器注入過程是發(fā)生在創(chuàng)建 Bean 的第一個(gè)步驟 createBeanInstance 中,而這個(gè)步驟中還沒有調(diào)用 addSingletonFactory 方法完成第三級(jí)緩存的構(gòu)建,自然也就無(wú)法從該緩存中獲取目標(biāo)對(duì)象。
總結(jié)
今天我們系統(tǒng)分析了 Spring 為開發(fā)人員提供的循環(huán)依賴解決方案。雖然基于 Spring 框架實(shí)現(xiàn) Bean 的依賴注入比較簡(jiǎn)單,但也存在一些最佳實(shí)踐,尤其是在使用 Spring 的過程中,經(jīng)常碰到的循環(huán)依賴問題,需要開發(fā)人員對(duì)框架的底層原理有一定的了解。
于是基于 Spring 提供的三層緩存機(jī)制,我們對(duì)這一主題進(jìn)行了源碼級(jí)的深入分析。從源碼中,我們發(fā)現(xiàn) Spring 中解決循環(huán)依賴的核心思想在于:基于 Bean 的作用域和生命周期,把 Bean 的創(chuàng)建過程拆分成“實(shí)例化”和“屬性填充”這兩個(gè)階段,確保 Bean 對(duì)象能夠在第一個(gè)階段就能夠盡早暴露出來供其他 Bean 進(jìn)行使用。
而在 Spring 所提供的三種依賴注入類型中,也只有 Setter 方法注入能夠解決循環(huán)依賴問題,原因就在于這種注入類型生效的時(shí)機(jī)恰恰就在“實(shí)例化”階段之后、“屬性填充”階段之前。