曹工說Spring Boot源碼(29)– Spring 解決循環依賴為什麼使用三級快取,而不是二級快取
- 2020 年 6 月 2 日
- 筆記
- spring/boot源碼解析
寫在前面的話
相關背景及資源:
曹工說Spring Boot源碼(1)– Bean Definition到底是什麼,附spring思維導圖分享
曹工說Spring Boot源碼(2)– Bean Definition到底是什麼,咱們對著介面,逐個方法講解
曹工說Spring Boot源碼(3)– 手動註冊Bean Definition不比遊戲好玩嗎,我們來試一下
曹工說Spring Boot源碼(4)– 我是怎麼自定義ApplicationContext,從json文件讀取bean definition的?
曹工說Spring Boot源碼(5)– 怎麼從properties文件讀取bean
曹工說Spring Boot源碼(6)– Spring怎麼從xml文件里解析bean的
曹工說Spring Boot源碼(7)– Spring解析xml文件,到底從中得到了什麼(上)
曹工說Spring Boot源碼(8)– Spring解析xml文件,到底從中得到了什麼(util命名空間)
曹工說Spring Boot源碼(9)– Spring解析xml文件,到底從中得到了什麼(context命名空間上)
曹工說Spring Boot源碼(10)– Spring解析xml文件,到底從中得到了什麼(context:annotation-config 解析)
曹工說Spring Boot源碼(11)– context:component-scan,你真的會用嗎(這次來說說它的奇技淫巧)
曹工說Spring Boot源碼(12)– Spring解析xml文件,到底從中得到了什麼(context:component-scan完整解析)
曹工說Spring Boot源碼(13)– AspectJ的運行時織入(Load-Time-Weaving),基本內容是講清楚了(附源碼)
曹工說Spring Boot源碼(14)– AspectJ的Load-Time-Weaving的兩種實現方式細細講解,以及怎麼和Spring Instrumentation集成
曹工說Spring Boot源碼(15)– Spring從xml文件里到底得到了什麼(context:load-time-weaver 完整解析)
曹工說Spring Boot源碼(16)– Spring從xml文件里到底得到了什麼(aop:config完整解析【上】)
曹工說Spring Boot源碼(17)– Spring從xml文件里到底得到了什麼(aop:config完整解析【中】)
曹工說Spring Boot源碼(18)– Spring AOP源碼分析三部曲,終於快講完了 (aop:config完整解析【下】)
曹工說Spring Boot源碼(19)– Spring 帶給我們的工具利器,創建代理不用愁(ProxyFactory)
曹工說Spring Boot源碼(20)– 碼網恢恢,疏而不漏,如何記錄Spring RedisTemplate每次操作日誌
曹工說Spring Boot源碼(21)– 為了讓大家理解Spring Aop利器ProxyFactory,我已經拼了
曹工說Spring Boot源碼(22)– 你說我Spring Aop依賴AspectJ,我依賴它什麼了
曹工說Spring Boot源碼(23)– ASM又立功了,Spring原來是這麼遞歸獲取註解的元註解的
曹工說Spring Boot源碼(24)– Spring註解掃描的瑞士軍刀,asm技術實戰(上)
曹工說Spring Boot源碼(25)– Spring註解掃描的瑞士軍刀,ASM + Java Instrumentation,順便提提Jar包破解
曹工說Spring Boot源碼(26)– 學習位元組碼也太難了,實在不能忍受了,寫了個小小的位元組碼執行引擎
曹工說Spring Boot源碼(27)– Spring的component-scan,光是include-filter屬性的各種配置方式,就夠玩半天了
曹工說Spring Boot源碼(28)– Spring的component-scan機制,讓你自己來進行簡單實現,怎麼辦
工程結構圖:
什麼是三級快取
在獲取單例bean的時候,會進入以下方法:
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。
/** 1級快取 Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 2級快取 Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/** 3級快取 Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> 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容器
@Data
public class BeanDefinitionRegistry {
/**
* map:存儲 bean的class-》bean實例
*/
private Map<Class, Object> 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<Field> 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。
/**
* map:存儲 bean的class-》bean實例
*/
private Map<Class, Object> 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舉例。我們可以只用下面這兩層:
/** 1級快取 Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 2級快取 Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
因為spring程式碼里是三級快取,所以我們對源碼做一點修改。
修改spring源碼,只使用二級快取
修改創建bean的程式碼,不放入第三級快取,只放入第二級快取
創建了bean之後,屬性注入之前,將創建出來的不完整bean,放到earlySingletonObjects
這個程式碼,在org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
,我這邊只有4.0版本的spring源碼工程,不過這套邏輯,算是spring核心邏輯,和5.x版本差別不大。
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)
之前的程式碼是文章開頭那樣的,我這裡修改為:
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循環依賴,一點問題都沒有,完全夠用了。
我這邊一個簡單的例子,
public class Chick{
private Egg egg;
public Egg getEgg() {
return egg;
}
public void setEgg(Egg egg) {
this.egg = egg;
}
}
public class Egg {
private Chick chick;
public Chick getChick() {
return chick;
}
public void setChick(Chick chick) {
this.chick = chick;
}
<bean id="chick" class="foo.Chick" lazy-init="true">
<property name="egg" ref="egg"/>
</bean>
<bean id="egg" class="foo.Egg" lazy-init="true">
<property name="chick" ref="chick"/>
</bean>
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
"context-namespace-test-aop.xml");
Egg egg = (Egg) ctx.getBean(Egg.class);
結論:
所以,一級快取都能解決的問題,二級當然更沒問題。
但是,如果我這裡給上面的Egg類,加個切面(aop的邏輯,意思就是最終會生成Egg的一個動態代理對象),那還有問題沒?
<aop:config>
<aop:pointcut id="mypointcut" expression="execution(public * foo.Egg.*(..))"/>
<aop:aspect id="myAspect" ref="performenceAspect">
<aop:after method="afterIncubate" pointcut-ref="mypointcut"/>
</aop:aspect>
</aop:config>
注意這裡的切點:
execution(public * foo.Egg.*(..))
就是切Egg類的方法。
加了這個邏輯後,我們繼續運行,在 Egg egg = (Egg) ctx.getBean(Egg.class);
行,會拋出如下異常:
我塗掉了一部分,因為那是官方對這個異常的推論,因為我們改了程式碼,所以推論不準確,因此乾脆隱去。
這個異常是說:
兄弟啊,bean egg已經被注入到了其他bean:chick中。(因為我們循環依賴了),但是,注入到chick中的,是Egg類型。但是,我們這裡最後對egg這個bean,進行了後置處理,生成了代理對象。那其他bean里,用原始的bean,是不是不太對啊?
所以,spring給我們拋錯了。
怎麼理解呢? 以io流舉例,我們一開始都是用的原始位元組流,然後給別人用的也是位元組流,但是,最後,我感覺不方便,我自己悄悄弄了個快取字元流(類比代理對象),我是方便了,但是,別人用的,還是原始的位元組流啊。
你bean不是單例嗎?不能這麼玩吧?
所以,這就是二級快取,不能解決的問題。
什麼問題?aop情形下,注入到其他bean的,不是最終的代理對象。
三級快取,怎麼解決這個問題
要解決這個問題,必須在其他bean(chick),來查找我們(以上面例子為例,我們是egg)的時候,查找到最終形態的egg,即代理後的egg。
怎麼做到這點呢?
加個三級快取,裡面不存具體的bean,裡面存一個工廠對象。通過工廠對象,是可以拿到最終形態的代理後的egg。
ok,我們將前面修改的程式碼還原:
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<String, Object> earlySingletonObjects = this.getEarlySingletonObjects();
// earlySingletonObjects.put(beanName,bean);
//
// Set<String> 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,然後會存入到如下的第三級快取。
/** 3級快取 Cache of singleton factories: bean name to ObjectFactory. */ private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
注意,new一個匿名內部類(假設這個匿名類叫AA)的對象,其中用到的外部類的變數,都會在AA中隱式生成對應的field。
大家看上圖,裡面的3個欄位,和下面程式碼1處中的,幾個欄位,是一一對應的。
addSingletonFactory(beanName, new ObjectFactory() { public Object getObject() throws BeansException { // 1 return getEarlyBeanReference(beanName, mbd, bean); } });
ok,現在,egg已經把自己存進去了,存在了第三級快取,1級和2級都沒有,那後續chick在使用getSingleton查找egg的時候,就會進入下面的邏輯了(就是文章開頭的那段程式碼,下面已經把我們的修改還原了):
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的邏輯是:
addSingletonFactory(beanName, new ObjectFactory() {
public Object getObject() throws BeansException {
// 1
return getEarlyBeanReference(beanName, mbd, bean);
}
});
1處就是這個工廠方法的邏輯,這裡面,簡單說,就會去調用各個beanPostProcessor的getEarlyBeanReference方法。
其中,主要就是aop的主力beanPostProcessor,AbstractAutoProxyCreator#getEarlyBeanReference
其實現如下:
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
就在這個介面中。
這個介面繼承了BeanPostProcessor
。
而創建代理對象,目前就是在如下兩個方法中去創建:
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
這兩個方法,都是在實例化之後,創建代理。那我們前面創建代理,是在依賴解析過程中:
public interface SmartInstantiationAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessor {
...
Object getEarlyBeanReference(Object bean, String beanName) throws BeansException;
}
所以,spring希望我們,在這幾處,要返回同樣的對象,即:既然你這幾處都要返回代理對象,那就不能返回不一樣的代理對象。
源碼
文章用到的aop循環依賴的demo,自己寫一個也可以,很簡單:
不錯的參考資料
//blog.csdn.net/f641385712/article/details/92801300
總結
如果有問題,歡迎指出;歡迎加群討論;有幫助的話,請點個贊吧,謝謝