1. 程式人生 > >曹工說Spring Boot原始碼(29)-- Spring 解決迴圈依賴為什麼使用三級快取,而不是二級快取

曹工說Spring Boot原始碼(29)-- Spring 解決迴圈依賴為什麼使用三級快取,而不是二級快取

# 寫在前面的話 相關背景及資源: [曹工說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 # 總結 如果有問題,歡迎指出;歡迎加群討論;有幫助的話,請點個贊吧