Spring的三級緩存是繞不過去的一個坎兒。面試也經常被問到。而網文大多都在講Spring三級緩存的用途,而分析的很好的很少。
接下來整篇文章分析下:Spring為什麼要使用三級緩存解決循環依賴,而不是二級緩存或是一級緩存
導學
先來明白一下Spring執行個體化一個Bean的過程中幾個重要概念
getBean
通過getBean方法擷取單例Bean,每個bean的建立都是從該方法開始的。
getSingleton
Spring中最最重要的核心邏輯,沒有之一。該方法依次從一級緩存、二級緩存、三級緩存中擷取單例bean對象。
注意:如果從三級緩存中擷取到對象之後,就會被立即移動到二級緩存。下面是源碼
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// Consistent creation of early reference within full singleton lock
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
三個級别緩存作用
一級緩存:存放最終的單例Bean,裡面所有的Bean都是直接能使用的。(這個大家一定都明白就不多說了)
三級緩存:存放一個工廠對象,這個工廠對象有一個getObject方法。工廠一般由lambda表達式組成,在工廠中主要完成了對Aop的代理。執行一些Bean的擴充邏輯。
二級緩存:沒錯,先介紹三級緩存是有目的的。二級緩存隻有在getSingleton(前文提到)方法中,才會把三級緩存獲得的對象存入二級緩存。并且删除三級緩存中的工廠對象。至于為什麼總結裡會說。
循環依賴示例
廢話不多說,先給兩個類Aoo和Boo,這兩個類形成循環依賴,代碼簡單如下。
@Component
public class Aoo {
@Autowired
private Boo boo;
}
@Component
public class Boo {
@Autowired
private Aoo aoo;
}
循環依賴執行流程
首先Spring會掃描指定的包,把所有标注@Component注解的類順序的執行個體化。Spring啟動時,容器中沒有任何bean(當然是有一些内部bean的,但是這樣說便于我們了解)
下面來了解下Spring執行個體化Bean的順序,
- 啟動時
- 檢測到Aoo開始執行個體化Aoo對象
- 調用doGetBean方法執行個體化Aoo
- 調用getSingleton方法,試圖從三級緩存中依次擷取bean。但是第一次肯定都為空
Object sharedInstance = getSingleton(beanName);
5.因為緩存為空,是以程式繼續往下走
sharedInstance = getSingleton(beanName, () -> {
return createBean(beanName, mbd, args);
});
程式調用getSingleton方法建立Aoo對象,并且該方法傳入一個lambda表達式,這個表達式後面是會被放入三級緩存的。
6.我們來看下getSingleton方法
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
try {
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
if (newSingleton) {
addSingleton(beanName, singletonObject);
}
return singletonObject;
}
可以看到該方法中試圖通過第二個參數,也就是上一步的lambda表達式建立Aoo對象,并且建立完成後把Aoo對象存入一級緩存。 此時一級緩存何時加入的我們清楚了。
7.接下來我們來看下這個lambda表達式,也即是createBean方法。而它又調用了doCreateBean方法。
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args){
// 省略無關代碼
return doCreateBean(beanName, mbdToUse, args);
}
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
// 建立一個Aoo的包裝對象。此時Aoo是一個空殼對象其中的所有屬性均為空
BeanWrapper instanceWrapper = null;
Object bean = instanceWrapper.getWrappedInstance();
// 判斷是否提前暴露(其實就是循環依賴了)
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
// 把Aoo對象加入三級緩存!!!
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
Object exposedObject = bean;
try {
// 給Aoo對象注入屬性
populateBean(beanName, mbd, instanceWrapper);
// 初始化Aoo(一些擴充點,與循環依賴關系不大)
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
if (earlySingletonExposure) {
// 如果提前暴露對象,則嘗試從緩存中擷取。
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
exposedObject = earlySingletonReference;
}
}
return exposedObject;
}
第一步:建立一個Aoo的空殼對象,此時Aoo其中的屬性還沒有值。
第二步:把工廠存入三級緩存,緩存的key就是對象的名稱aoo,而value是一個工廠對象的lambda表達式,這個工廠對象會傳回Aoo對象。這個工廠會執行getEarlyBeanReference方法,該方法中會完成AOP動态代理。需要重點說明一下,從三級緩存中取出的對象每次都是不一樣的。因為它是每次代理生成的。
第三步:執行populateBean方法。為Aoo注入屬性,Aoo隻有一個屬性Boo。Spring在注入Boo屬性的時候發現容器沒有Boo對象。
第四步:從這一步就循環到文章的最開始了,接下來我用小寫數字表示的步驟表示上文提到的步驟。回到3調用doGetBean方法執行個體化Boo。
第五步:執行4調用getSingleton方法,試圖從三級緩存中依次擷取bean。前面說過第一次肯定都為空,這次是第一次擷取Boo對象肯定也還是空的。
第六步:依次執行5,6,7,在7中會把Boo對象存入三級緩存中。 沒錯,任何一個Bean都會先放到三級緩存中。此時三級緩存中有兩個Bean了,分别是Aoo和Boo
第七步:在7中的populateBean方法開始給Boo注入屬性了,Boo隻有一個屬性Aoo,Spring在注入Aoo屬性是會從容器中擷取,也就是調用getBean方法,此時發現三級緩存中有Aoo。就會從三級緩存中擷取Aoo對象并給Boo的這個屬性指派。同時也會把Aoo對象從三級緩存移動到二級緩存中。此時一級緩存為空、二級緩存中有Aoo對象、三級緩存中有Boo對象。此時Aoo、Boo的狀态還都是建立的過程中。
第八步:Boo的屬性已經完成,回到上面的6它會把Boo對象添加到一級緩存,并從三級緩存中移除(這兒沒二級緩存啥事兒嘿嘿嘿)。 此時一級緩存中有一個對象Boo、二級緩存中有Aoo對象、三級緩存為空。
第九步:既然Boo都已經在一級緩存當中了,那麼接着第三步來說,此時Aoo的屬性Boo也完成了指派。此時Aoo也是一個完整對象了。但它此刻還在二級緩存當中。
第十步:在7執行完畢之後,回歸到6的代碼,執行addSingleton方法把Aoo從二級緩存移動到一級緩存當中。至此,依賴注入完畢。一級緩存中有Aoo對象和Boo對象。二級、三級緩存為空。
思考:為什麼需要三個級别的緩存來解決循環依賴
現在來思考一下為什麼一定要是三個級别的緩存呢?我們來删除二級緩存後看這個問題。下面我們就使用隻有一級緩存和三級緩存這2個緩存來看下循環依賴的問題能不能解決。
還是Aoo和Boo兩個類循環依賴
- Spring啟動
- 從一級緩存和三級緩存中擷取Aoo,緩存中沒有,則建立
- 建立Aoo的空殼對象,并把它和工廠對象放入三級緩存中。
- 對Aoo進行屬性注入,發現Boo即不在一級緩存,也不在三級緩存。隻能建立了
- 建立Boo對象
- 對Boo進行屬性注入,發現三級緩存中有Aoo對象,直接從三級緩存中擷取。
- Boo對象屬性裝配完成,把它從三級緩存移到一級緩存。
- Aoo對象屬性裝配完成,此時從三級緩存中移到一級緩存。
乍一看沒啥問題是不是,其實不是的。問題出在第6步,和第8步。通過前面的講解,一定要了解到,三級緩存中每次傳回的對象都不一樣。是以第6步和第8步如果都從三級緩存中擷取Aoo對象, 這兩步中的Aoo對象不是同一個,Spring中的Aoo對象和Boo對象就會使這個樣子
總結
首先不是說非要三級緩存機制才能解決循環依賴,一級緩存同樣可以解決,把三級緩存代碼平鋪化就好了嘛,或者使用JVM指令,位元組碼等技術完成循環依賴,但你想一下,那樣的話代碼的可讀性必然很低。是以第一個原因就是使用三級緩存解決循環依賴使得代碼可讀性非常好。
第二個原因是三級緩存中的工廠,每次getObject方法傳回的執行個體不是同一個對象,是以需要二級緩存來緩存一下三級緩存生成的bean,這樣就保證了兩個類的屬性是環形依賴,不會破壞循環依賴。
作者:念念清晰
連結:https://juejin.cn/post/7200366809407651877
來源:稀土掘金