曹工說Spring Boot原始碼(29)-- Spring 解決迴圈依賴為什麼使用三級快取,而不是二級快取
阿新 • • 發佈:2020-06-03
# 寫在前面的話
相關背景及資源:
[曹工說Spring Boot原始碼(1)-- Bean Definition到底是什麼,附spring思維導圖分享](https://www.cnblogs.com/grey-wolf/p/12044199.html)
[曹工說Spring Boot原始碼(2)-- Bean Definition到底是什麼,咱們對著介面,逐個方法講解](https://www.cnblogs.com/grey-wolf/p/12051957.html )
[曹工說Spring Boot原始碼(3)-- 手動註冊Bean Definition不比遊戲好玩嗎,我們來試一下](https://www.cnblogs.com/grey-wolf/p/12070377.html)
[曹工說Spring Boot原始碼(4)-- 我是怎麼自定義ApplicationContext,從json檔案讀取bean definition的?](https://www.cnblogs.com/grey-wolf/p/12078673.html)
[曹工說Spring Boot原始碼(5)-- 怎麼從properties檔案讀取bean](https://www.cnblogs.com/grey-wolf/p/12093929.html)
[曹工說Spring Boot原始碼(6)-- Spring怎麼從xml檔案裡解析bean的](https://www.cnblogs.com/grey-wolf/p/12114604.html )
[曹工說Spring Boot原始碼(7)-- Spring解析xml檔案,到底從中得到了什麼(上)](https://www.cnblogs.com/grey-wolf/p/12151809.html)
[曹工說Spring Boot原始碼(8)-- Spring解析xml檔案,到底從中得到了什麼(util名稱空間)](https://www.cnblogs.com/grey-wolf/p/12158935.html)
[曹工說Spring Boot原始碼(9)-- Spring解析xml檔案,到底從中得到了什麼(context名稱空間上)](https://www.cnblogs.com/grey-wolf/p/12189842.html)
[曹工說Spring Boot原始碼(10)-- Spring解析xml檔案,到底從中得到了什麼(context:annotation-config 解析)](https://www.cnblogs.com/grey-wolf/p/12199334.html)
[曹工說Spring Boot原始碼(11)-- context:component-scan,你真的會用嗎(這次來說說它的奇技淫巧)](https://www.cnblogs.com/grey-wolf/p/12203743.html)
[曹工說Spring Boot原始碼(12)-- Spring解析xml檔案,到底從中得到了什麼(context:component-scan完整解析)](https://www.cnblogs.com/grey-wolf/p/12214408.html)
[曹工說Spring Boot原始碼(13)-- AspectJ的執行時織入(Load-Time-Weaving),基本內容是講清楚了(附原始碼)](https://www.cnblogs.com/grey-wolf/p/12228958.html)
[曹工說Spring Boot原始碼(14)-- AspectJ的Load-Time-Weaving的兩種實現方式細細講解,以及怎麼和Spring Instrumentation整合](https://www.cnblogs.com/grey-wolf/p/12283544.html)
[曹工說Spring Boot原始碼(15)-- Spring從xml檔案裡到底得到了什麼(context:load-time-weaver 完整解析)](https://www.cnblogs.com/grey-wolf/p/12288391.html)
[曹工說Spring Boot原始碼(16)-- Spring從xml檔案裡到底得到了什麼(aop:config完整解析【上】)](https://www.cnblogs.com/grey-wolf/p/12314954.html)
[曹工說Spring Boot原始碼(17)-- Spring從xml檔案裡到底得到了什麼(aop:config完整解析【中】)](https://www.cnblogs.com/grey-wolf/p/12317612.html)
[曹工說Spring Boot原始碼(18)-- Spring AOP原始碼分析三部曲,終於快講完了 (aop:config完整解析【下】)](https://www.cnblogs.com/grey-wolf/p/12322587.html)
[曹工說Spring Boot原始碼(19)-- Spring 帶給我們的工具利器,建立代理不用愁(ProxyFactory)](https://www.cnblogs.com/grey-wolf/p/12359963.html)
[曹工說Spring Boot原始碼(20)-- 碼網恢恢,疏而不漏,如何記錄Spring RedisTemplate每次操作日誌](https://www.cnblogs.com/grey-wolf/p/12375656.html)
[曹工說Spring Boot原始碼(21)-- 為了讓大家理解Spring Aop利器ProxyFactory,我已經拼了](https://www.cnblogs.com/grey-wolf/p/12384356.html)
[曹工說Spring Boot原始碼(22)-- 你說我Spring Aop依賴AspectJ,我依賴它什麼了](https://www.cnblogs.com/grey-wolf/p/12418425.html)
[曹工說Spring Boot原始碼(23)-- ASM又立功了,Spring原來是這麼遞迴獲取註解的元註解的](https://www.cnblogs.com/grey-wolf/p/12535152.html)
[曹工說Spring Boot原始碼(24)-- Spring註解掃描的瑞士軍刀,asm技術實戰(上)](https://www.cnblogs.com/grey-wolf/p/12571217.html)
[曹工說Spring Boot原始碼(25)-- Spring註解掃描的瑞士軍刀,ASM + Java Instrumentation,順便提提Jar包破解](https://www.cnblogs.com/grey-wolf/p/12584861.html)
[曹工說Spring Boot原始碼(26)-- 學習位元組碼也太難了,實在不能忍受了,寫了個小小的位元組碼執行引擎](https://www.cnblogs.com/grey-wolf/p/12600097.html)
[曹工說Spring Boot原始碼(27)-- Spring的component-scan,光是include-filter屬性的各種配置方式,就夠玩半天了](https://www.cnblogs.com/grey-wolf/p/12601823.html)
[曹工說Spring Boot原始碼(28)-- Spring的component-scan機制,讓你自己來進行簡單實現,怎麼辦](https://www.cnblogs.com/grey-wolf/p/12632419.html)
[工程程式碼地址](https://gitee.com/ckl111/spring-boot-first-version-learn ) [思維導圖地址](https://www.processon.com/view/link/5deeefdee4b0e2c298aa5596)
工程結構圖:
![](https://img2018.cnblogs.com/blog/519126/201912/519126-20191215144930717-1919774390.png)
# 什麼是三級快取
在獲取單例bean的時候,會進入以下方法:
```java
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 2
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 3
ObjectFactory> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 4
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
```
這裡面涉及到了該類中的三個field。
```java
/** 1級快取 Cache of singleton objects: bean name to bean instance. */
private final Map singletonObjects = new ConcurrentHashMap<>(256);
/** 2級快取 Cache of early singleton objects: bean name to bean instance. */
private final Map earlySingletonObjects = new HashMap<>(16);
/** 3級快取 Cache of singleton factories: bean name to ObjectFactory. */
private final Map> singletonFactories = new HashMap<>(16);
```
接著說前面的程式碼。
* 1處,在最上層的快取`singletonObjects`中,獲取單例bean,這裡面拿到的bean,直接可以使用;如果沒取到,則進入2處
* 2處,在2級快取`earlySingletonObjects`中,查詢bean;
* 3處,如果在2級快取中,還是沒找到,則在3級快取中查詢對應的工廠物件,利用拿到的工廠物件(工廠物件中,有3個field,一個是beanName,一個是RootBeanDefinition,一個是已經建立好的,但還沒有注入屬性的bean),去獲取包裝後的bean,或者說,代理後的bean。
什麼是已經建立好的,但沒有注入屬性的bean?
比如一個bean,有10個欄位,你new了之後,物件已經有了,記憶體空間已經開闢了,堆裡已經分配了該物件的空間了,只是此時的10個field還是null。
# ioc容器,普通迴圈依賴,一級快取夠用嗎
說實話,如果簡單寫寫的話,一級快取都沒問題。給大家看一個我以前寫的渣渣ioc容器:
[曹工說Tomcat4:利用 Digester 手擼一個輕量的 Spring IOC容器](https://www.cnblogs.com/grey-wolf/p/11146727.html)
```java
@Data
public class BeanDefinitionRegistry {
/**
* map:儲存 bean的class-》bean例項
*/
private Map beanMapByClass = new ConcurrentHashMap<>();
/**
* 根據bean 定義獲取bean
* 1、先查bean容器,查到則返回
* 2、生成bean,放進容器(此時,依賴還沒注入,主要是解決迴圈依賴問題)
* 3、注入依賴
*
* @param beanDefiniton
* @return
*/
private Object getBean(MyBeanDefiniton beanDefiniton) {
Class> beanClazz = beanDefiniton.getBeanClazz();
Object bean = beanMapByClass.get(beanClazz);
if (bean != null) {
return bean;
}
// 0
bean = generateBeanInstance(beanClazz);
// 1 先行暴露,解決迴圈依賴問題
beanMapByClass.put(beanClazz, bean);
beanMapByName.put(beanDefiniton.getBeanName(), bean);
// 2 查詢依賴
List dependencysByField = beanDefiniton.getDependencysByField();
if (dependencysByField == null) {
return bean;
}
// 3
for (Field field : dependencysByField) {
try {
autowireField(beanClazz, bean, field);
} catch (Exception e) {
throw new RuntimeException(beanClazz.getName() + " 建立失敗",e);
}
}
return bean;
}
}
```
大家看上面的程式碼,我只定義了一個field,就是一個map,存放bean的class-》bean。
```java
/**
* map:儲存 bean的class-》bean例項
*/
private Map beanMapByClass = new ConcurrentHashMap<>();
```
* 0處,生成bean,直接就是new
* 1處,先把這個不完整的bean,放進map
* 2處,獲取需要注入的屬性集合
* 3處,進行自動注入,就是根據field的Class,去map裡查詢對應的bean,設定到field裡。
上面這個程式碼,有啥問題沒?spring為啥整整三級?
# ioc,一級快取有什麼問題
一級快取的問題在於,就1個map,裡面既有完整的已經ready的bean,也有不完整的,尚未設定field的bean。
如果這時候,有其他執行緒去這個map裡獲取bean來用怎麼辦?拿到的bean,不完整,怎麼辦呢?屬性都是null,直接空指標了。
所以,我們就要加一個map,這個map,用來存放那種不完整的bean。這裡,還是拿spring舉例。我們可以只用下面這兩層:
```java
/** 1級快取 Cache of singleton objects: bean name to bean instance. */
private final Map singletonObjects = new ConcurrentHashMap<>(256);
/** 2級快取 Cache of early singleton objects: bean name to bean instance. */
private final Map earlySingletonObjects = new HashMap<>(16);
```
因為spring程式碼裡是三級快取,所以我們對原始碼做一點修改。
#修改spring原始碼,只使用二級快取
##修改建立bean的程式碼,不放入第三級快取,只放入第二級快取
建立了bean之後,屬性注入之前,將創建出來的不完整bean,放到`earlySingletonObjects`
這個程式碼,在`org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean`,我這邊只有4.0版本的spring原始碼工程,不過這套邏輯,算是spring核心邏輯,和5.x版本差別不大。
```java
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) {
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
// 1
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
Class beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);
...
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
// 2
earlySingletonObjects.put(beanName,bean);
registeredSingletonObjects.add(beanName);
// 3
// addSingletonFactory(beanName, new ObjectFactory() {
// public Object getObject() throws BeansException {
// return getEarlyBeanReference(beanName, mbd, bean);
// }
// });
}
```
* 1處,就是建立物件,就是new
* 2處,這是我加的程式碼,放入二級快取
* 3處,本來這就是增加三級快取的位置,被我註釋了。現在,就不會往三級快取放東西了
## 修改獲取bean的程式碼,只從第一、第二級快取獲取,不從第三級獲取
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)
之前的程式碼是文章開頭那樣的,我這裡修改為:
```java
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
return singletonObject;
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
```
這樣,就是隻用兩級快取了。
# 兩級快取,有啥問題?
ioc迴圈依賴,一點問題都沒有,完全夠用了。
我這邊一個簡單的例子,
```java
public class Chick{
private Egg egg;
public Egg getEgg() {
return egg;
}
public void setEgg(Egg egg) {
this.egg = egg;
}
}
```
```java
public class Egg {
private Chick chick;
public Chick getChick() {
return chick;
}
public void setChick(Chick chick) {
this.chick = chick;
}
```
```xml
```
```java
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
"context-namespace-test-aop.xml");
Egg egg = (Egg) ctx.getBean(Egg.class);
```
結論:
![](https://img2020.cnblogs.com/blog/519126/202006/519126-20200602215958047-2035448637.png)
所以,一級快取都能解決的問題,二級當然更沒問題。
但是,如果我這裡給上面的Egg類,加個切面(aop的邏輯,意思就是最終會生成Egg的一個動態代理物件),那還有問題沒?
```xml
```
注意這裡的切點:
```java
execution(public * foo.Egg.*(..))
```
就是切Egg類的方法。
加了這個邏輯後,我們繼續執行,在` Egg egg = (Egg) ctx.getBean(Egg.class);`行,會丟擲如下異常:
![](https://img2020.cnblogs.com/blog/519126/202006/519126-20200602220852323-537278707.png)
我塗掉了一部分,因為那是官方對這個異常的推論,因為我們改了程式碼,所以推論不準確,因此乾脆隱去。
這個異常是說:
> 兄弟啊,bean egg已經被注入到了其他bean:chick中。(因為我們迴圈依賴了),但是,注入到chick中的,是Egg型別。但是,我們這裡最後對egg這個bean,進行了後置處理,生成了代理物件。那其他bean裡,用原始的bean,是不是不太對啊?
所以,spring給我們拋錯了。
怎麼理解呢? 以io流舉例,我們一開始都是用的原始位元組流,然後給別人用的也是位元組流,但是,最後,我感覺不方便,我自己悄悄弄了個快取字元流(類比代理物件),我是方便了,但是,別人用的,還是原始的位元組流啊。
你bean不是單例嗎?不能這麼玩吧?
所以,這就是二級快取,不能解決的問題。
什麼問題?aop情形下,注入到其他bean的,不是最終的代理物件。
# 三級快取,怎麼解決這個問題
要解決這個問題,必須在其他bean(chick),來查詢我們(以上面例子為例,我們是egg)的時候,查詢到最終形態的egg,即代理後的egg。
怎麼做到這點呢?
加個三級快取,裡面不存具體的bean,裡面存一個工廠物件。通過工廠物件,是可以拿到最終形態的代理後的egg。
ok,我們將前面修改的程式碼還原:
```java
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) {
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
// 1
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
Class beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
// 2
// Map earlySingletonObjects = this.getEarlySingletonObjects();
// earlySingletonObjects.put(beanName,bean);
//
// Set registeredSingletonObjects = this.getRegisteredSingletonObjects();
// registeredSingletonObjects.add(beanName);
// 3
addSingletonFactory(beanName, new ObjectFactory() {
public Object getObject() throws BeansException {
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}
```
* 1處,建立bean,單純new,不注入
* 2處,revert我們的程式碼
* 3處,這裡new了一個ObjectFactory,然後會存入到如下的第三級快取。
```java
/** 3級快取 Cache of singleton factories: bean name to ObjectFactory. */
private final Map> singletonFactories = new HashMap<>(16);
```
注意,new一個匿名內部類(假設這個匿名類叫AA)的物件,其中用到的外部類的變數,都會在AA中隱式生成對應的field。
![](https://img2020.cnblogs.com/blog/519126/202006/519126-20200602222551213-464889980.png)
大家看上圖,裡面的3個欄位,和下面程式碼1處中的,幾個欄位,是一一對應的。
```java
addSingletonFactory(beanName, new ObjectFactory() {
public Object getObject() throws BeansException {
// 1
return getEarlyBeanReference(beanName, mbd, bean);
}
});
```
ok,現在,egg已經把自己存進去了,存在了第三級快取,1級和2級都沒有,那後續chick在使用getSingleton查詢egg的時候,就會進入下面的邏輯了(就是文章開頭的那段程式碼,下面已經把我們的修改還原了):
```java
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Object singletonObject = this.singletonObjects.get(beanName);
// if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// synchronized (this.singletonObjects) {
// singletonObject = this.earlySingletonObjects.get(beanName);
// return singletonObject;
// }
// }
// return (singletonObject != NULL_OBJECT ? singletonObject : null);
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 1
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
```
上面就會進入1處,呼叫`singletonFactory.getObject();`。
而前面我們知道,這個factory的邏輯是:
```java
addSingletonFactory(beanName, new ObjectFactory() {
public Object getObject() throws BeansException {
// 1
return getEarlyBeanReference(beanName, mbd, bean);
}
});
```
1處就是這個工廠方法的邏輯,這裡面,簡單說,就會去呼叫各個beanPostProcessor的getEarlyBeanReference方法。
其中,主要就是aop的主力beanPostProcessor,`AbstractAutoProxyCreator#getEarlyBeanReference`
其實現如下:
```java
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.add(cacheKey);
// 1
return wrapIfNecessary(bean, beanName, cacheKey);
}
```
這裡的1處,就會去對egg這個bean,建立代理,此時,返回的物件,就是個代理物件了,那,注入到chick的,自然也是代理後的egg了。
#關於SmartInstantiationAwareBeanPostProcessor
我們上面說的那個`getEarlyBeanReference`就在這個介面中。
![](https://img2020.cnblogs.com/blog/519126/202006/519126-20200602223603137-439922390.png)
這個介面繼承了`BeanPostProcessor`。
而建立代理物件,目前就是在如下兩個方法中去建立:
```java
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
```
這兩個方法,都是在例項化之後,建立代理。那我們前面建立代理,是在依賴解析過程中:
```java
public interface SmartInstantiationAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessor {
...
Object getEarlyBeanReference(Object bean, String beanName) throws BeansException;
}
```
所以,spring希望我們,在這幾處,要返回同樣的物件,即:既然你這幾處都要返回代理物件,那就不能返回不一樣的代理物件。
![](https://img2020.cnblogs.com/blog/519126/202006/519126-20200602224042718-938402841.png)
# 原始碼
文章用到的aop迴圈依賴的demo,自己寫一個也可以,很簡單:
https://gitee.com/ckl111/spring-boot-first-version-learn/tree/master/all-demo-in-spring-learning/spring-aop-xml-demo-cycle-reference
# 不錯的參考資料
https://blog.csdn.net/f641385712/article/details/92801300
# 總結
如果有問題,歡迎指出;歡迎加群討論;有幫助的話,請點個贊吧