大家好。我們都知道,Spring可以通過三級快取解決迴圈依賴的問題,這也是面試中很常見的一個面試題,本文就來著重討論一下有關迴圈依賴和三級快取的問題。
一、什麼是迴圈依賴
大家平時在寫業務的時候應該寫過這樣的程式碼。
其實這種型別就是迴圈依賴,就是AService 和BService兩個類相互引用。
二、三級快取可以解決的迴圈依賴場景
如上面所說,大家平時在寫這種程式碼的時候,專案其實是可以起來的,也就是說其實三級快取是可以解決這種迴圈依賴的。
當然除了這種欄位注入,set注入也是可以解決的,程式碼如下。
接下來就來探究三級快取是如何解決這種迴圈依賴的?
三、Spring的Bean是如何建立出來的
本文所說的Bean和物件可以理解為同一個意思。
先說如何解決迴圈依賴之前,先來了解一下一個Bean建立的大致流程。為什麼要說Bean的建立過程,因為迴圈依賴主要是發生在Bean建立的過程中,知道Bean是如何建立的,才能更好的理解三級快取的作用。
其實Spring Bean的生命週期原始碼剖析我也在微信公眾號 三友的java日記 中發過,並且有簡單的提到三級快取,有興趣的同學可以在關注公眾號之後回覆 Bean 即可獲取文章連結,裡面有Bean建立過程更詳細的說明。這裡我簡單畫一張圖來說一下。
其實圖裡的每個階段還可以分為一些小的階段,我這裡就沒畫出來了。
來說一下每個階段幹了什麼事。
- BeanDefinition的讀取階段:我們在往Spring容器注入Bean的時候,一般會通過比如xml方式,@Bean註解的方式,@Component註解的方式,其實不論哪一種,容器啟動的時候都會去解析這些配置,然後為每個Bean生成一個對應的BeanDefinition,這個BeanDefinition包含了這個Bean的建立的資訊,Spring就是根據BeanDefinition去決定如何建立一個符合你要求的Bean
- Bean的例項化階段:這個階段主要是將你配置的Bean根據Class的型別建立一個物件出來
- Bean的屬性賦值階段:這個階段主要是用來處理屬性的賦值,比如@Autowired註解的生效就是在這個階段的
- Bean的初始化階段:這個階段主要是回撥一些方法,比如你的類實現了InitializingBean介面,那麼就會回撥afterPropertiesSet方法,同時動態代理其實也是在這個階段完成的。
其實從這可以看出,一個Spring Bean的生成要分為很多的階段,只有這些事都處理完了,這個Bean才是完完全全建立好的Bean,也就是我們可以使用的Bean。
四、三級快取指的是哪三級快取
這裡直接上原始碼
第一級快取:singletonObjects
存放已經完完全全建立好的Bean,什麼叫完完全全建立好的?就是上面說的是,所有的步驟都處理完了,就是建立好的Bean。一個Bean在產的過程中是需要經歷很多的步驟,在這些步驟中可能要處理@Autowired註解,又或是處理@Transcational註解,當需要處理的都處理完之後的Bean,就是完完全全建立好的Bean,這個Bean是可以用來使用的,我們平時在用的Bean其實就是建立好的。
第二級快取:earlySingletonObjects
早期暴露出去的Bean,其實也就是解決迴圈依賴的Bean。早期的意思就是沒有完完全全建立好,但是由於有迴圈依賴,就需要把這種Bean提前暴露出去。其實 早期暴露出去的Bean 跟 完完全全建立好的Bean 他們是同一個物件,只不過早期Bean裡面的註解可能還沒處理,完完全全的Bean已經處理了完了,但是他們指的還是同一個物件,只不過它們是在Bean建立過程中處於的不同狀態,如果早期暴露出去的Bean跟完完全全建立好的Bean不是同一個物件是會報錯的,專案也就起不來,這個不一樣導致報錯問題,這裡我會結合一個案例再來寫一篇文章,這裡不用太care,就認為是一樣的。
第三級快取:singletonFactories
存的是每個Bean對應的ObjectFactory物件,通過呼叫這個物件的getObject方法,就可以獲取到早期暴露出去的Bean。
注意:這裡有個很重要的細節就是三級快取只會對單例的Bean生效,像多例的是無法利用到三級快取的,通過三級快取所在的類名DefaultSingletonBeanRegistry就可以看出,僅僅是對SingletonBean也就是單例Bean有效果。
五、三級快取在Bean生成的過程中是如何解決迴圈依賴的
這裡我假設專案啟動時先建立了AService的Bean,那麼就會根據Spring Bean建立的過程來建立。
在Bean的例項化階段,就會建立出AService的物件,此時裡面的@Autowired註解是沒有處理的,建立出AService的物件之後就會構建AService對應的一個ObjectFactory物件放到三級快取中,通過這個ObjectFactory物件可以獲取到AService的早期Bean。
然後AService繼續往下走,到了某一個階段,開始處理@Autowired註解,要注入BService物件,如圖
要注入BService物件,肯定要去找BService物件,那麼他就會從三級快取中的第一級快取開始依次查詢有沒有BService對應的Bean,肯定都沒有啊,因為BService還沒建立呢。沒有該怎麼辦呢?其實很好辦,沒有就去建立一個麼,這樣不就有了麼。於是AService的注入BService的過程就算暫停了,因為現在得去建立BService,建立之後才能注入給AService。
於是乎,BService就開始建立了,當然他也是Spring的Bean,所以也按照Bean的建立方式來建立,先例項化一個BService物件,然後快取對應的一個ObjectFactory到第三級快取中,然後就到了需要處理@Autowired註解的時候了,如圖。
@Autowired註解需要注入AService物件。注入AService物件,就需要先去拿到AService物件,此時也會一次從三級快取查有沒有AService。
先從第一級查,有沒有建立好的AService,肯定沒有,因為AService此時正在在建立(因為AService在建立的過程中需要注入BService才去建立BService的,雖然此刻程式碼正在建立BService,但是AService也是在建立的過程中,只不過暫停了,只要BService建立完,AService會繼續往下建立);第一級快取沒有,那麼就去第二級看看,也沒有,沒有早期的AService;然後去第三級快取看看有沒有AService對應的ObjectFactory物件,驚天的發現,竟然有(上面提到過,建立出AService的物件之後,會構建AService對應的一個ObjectFactory物件放到三級快取中),那麼此時就會呼叫AService對應的ObjectFactory物件的getObject方法,拿到早期的AService物件,然後將早期的AService物件放到二級快取,為什麼需要放到二級快取,主要是怕還有其他的迴圈依賴,如果還有的話,直接從二級快取中就能拿到早期的AService物件。
雖然是早期的AService物件,但是我前面說過,僅僅只是早期的AService物件可能有些Bean建立的步驟還沒完成,跟最後完完全全建立好的AService Bean是同一個物件。
於是接下來就把早期的AService物件注入給BService。
此時BService的@Autowired註解注入AService物件就完成了,之後再經過其他階段的處理之後,BService物件就完完全全的建立完了。
BService物件建立完之後,就會將BService放入第一級快取,然後清空BService對應的第三級快取,當然也會去清空第二級快取,只是沒有而已,至於為什麼清空,很簡單,因為BService已經完全建立好了,如果需要BService那就在第一級快取中就能查詢到,不需要在從第二級或者第三級快取中找到早期的BService物件。
BService物件就完完全全的建立完之後,那麼接下來該幹什麼呢?此時當然繼續建立AService物件了,你不要忘了為什麼需要建立BService物件,因為AService物件需要注入一個BService物件,所以才去建立BService的,那麼此時既然BService已經建立完了,那麼是不是就應該注入給AService物件了?所以就會將BService注入給AService物件,這下就明白了,BService在構建的時候,已經注入了AService,雖然是早期的AService,但的確是AService物件,現在又把BService注入給了AService,那麼是不是已經解決了迴圈依賴的問題了,AService和BService都各自注入了對方,如圖。
然後AService就會跟BService一樣,繼續處理其它階段的,完全建立好之後,也會清空二三級快取,放入第一級快取。
到這裡,AService和BService就都建立好了,迴圈依賴也就解決了。
這下你應該明白了三級快取的作用,主要是第二級和第三級用來存早期的物件,這樣在有迴圈依賴的物件,就可以注入另一個物件的早期狀態,從而達到解決迴圈依賴的問題,而早期狀態的物件,在構建完成之後,也就會成為完完全全可用的物件。
六、三級快取無法解決的迴圈依賴場景
1)構造器注入無法解決迴圈依賴
上面的例子是通過@Autowired註解直接注入依賴的物件,但是如果通過構造器注入迴圈依賴的物件,是無法解決的,如程式碼下
構造器注入就是指建立AService物件的時候,就傳入BService物件,而不是用@Autowired註解注入BService物件。
執行結果
啟動時就會報錯,所以通過構造器注入物件就能避免產生迴圈依賴的問題,因為如果有迴圈依賴的話,那麼就會報錯。
至於三級快取為什麼不能解決構造器注入的問題呢?其實很好理解,因為上面說三級快取解決迴圈依賴的時候主要講到,在AService例項化之後,會建立對應的ObjectFactory放到第三級快取,發生迴圈依賴的時候,可以通過ObjectFactory拿到早期的AService物件;而構造器注入,是發生在例項化的時候,此時還沒有AService物件正在建立,還沒完成,壓根就還沒執行到往第三級新增對應的ObjectFactory的步驟,那麼BService在建立的時候,就無法通過三級快取拿到早期的AService物件,拿不到怎麼辦,那就去建立AService物件,但是AService不是正在建立麼,於是會報錯。
2)注入多例的物件無法解決迴圈依賴
啟動引導類
要獲取AService物件,因為多例的Bean在容器啟動的時候是不會去建立的,所以得去獲取,這樣就會建立了。
執行結果
為什麼不能解決,上面在說三級快取的時候已經說過了,三級快取只能對單例Bean生效,那麼多例是不會起作用的,並且在建立Bean的時候有這麼一個判斷,那就是如果出現迴圈依賴並且是依賴的是多例的Bean,那麼直接拋異常,原始碼如下
註釋其實說的很明白,推測出現了迴圈依賴,拋異常。
所以上面提到的兩種迴圈依賴的場景,之所以無法通過三級快取來解決,是因為壓根這兩種場景就無法使用三級快取,所以三級快取肯定解決不掉。
七、不用三級快取,用二級快取能不能解決迴圈依賴
遇到這種面試題,你就跟面試官說,如果行的話,Spring的作者為什麼不這麼寫呢?
哈哈,開個玩笑,接下來說說到底為什麼不行。
這裡我先說一下前面沒提到的細節,那就是通過ObjectFactory獲取的Bean可能是兩種型別,第一種就是例項化階段建立出來的物件,還是一種就是例項化階段建立出來的物件的代理物件。至於是不是代理物件,取決於你的配置,如果新增了事務註解又或是自定義aop切面,那就需要代理。這裡你不用擔心,如果這裡獲取的是代理物件,那麼最後完全建立好的物件也是代理物件,ObjectFactory獲取的物件和最終完全建立好的還是同一個,不是同一個肯定會報錯,所以上面的理論依然符合,這裡只是更加的細節化。
有了這個知識點之後,我們就來談一下為什麼要三級快取。
第一級快取,也就是快取完全建立好的Bean的快取,這個快取肯定是需要的,因為單例的Bean只能建立一次,那麼肯定需要第一級快取儲存這些物件,如果有需要,直接從第一級快取返回。那麼如果只能有二級快取的話,就只能捨棄第二級或者第三級快取。
假設捨棄第三級快取
捨棄第三級快取,也就是沒有ObjectFactory,那麼就需要往第二快取放入早期的Bean,那麼是放沒有代理的Bean還是被代理的Bean呢?
1)如果直接往二級快取新增沒有被代理的Bean,那麼可能注入給其它物件的Bean跟最後最後完全生成的Bean是不一樣的,因為最後生成的是代理物件,這肯定是不允許的;
2)那麼如果直接往二級快取新增一個代理Bean呢?
- 假設沒有迴圈依賴,提前暴露了代理物件,那麼如果跟最後建立好的不一樣,那麼專案啟動就會報錯,
- 假設沒有迴圈依賴,使用了ObjectFactory,那麼就不會提前暴露了代理物件,到最後生成的物件是什麼就是什麼,就不會報錯,
- 如果有迴圈依賴,不論怎樣都會提前暴露代理物件,那麼如果跟最後建立好的不一樣,那麼專案啟動就會報錯
通過上面分析,如果沒有迴圈依賴,使用ObjectFactory,就減少了提前暴露代理物件的可能性,從而減少報錯的可能。
假設捨棄第二級快取
假設捨棄第二級快取,也就是沒有存放早期的Bean的快取,其實肯定也不行。上面說過,ObjectFactory其實獲取的物件可能是代理的物件,那麼如果每次都通過ObjectFactory獲取代理物件,那麼每次都重新建立一個代理物件,這肯定也是不允許的。
從上面分析,知道為什麼不能使用二級快取了吧,第三級快取就是為了避免過早地建立代理物件,從而避免沒有迴圈依賴過早暴露代理物件產生的問題,而第二級快取就是防止多次建立代理物件,導致物件不同。
本文完。
如果覺得這篇文章對你有所幫助,還請幫忙點贊、在看、轉發給更多的人,碼字不易,非常感謝!
歡迎關注公眾號 三友的java日記,更多技術乾貨及時獲得。
往期熱門文章推薦