畫圖帶你徹底弄懂三級快取和迴圈依賴的問題

三友的java日記發表於2022-05-23
畫圖帶你徹底弄懂三級快取和迴圈依賴的問題

大家好。我們都知道,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日記,更多技術乾貨及時獲得。

往期熱門文章推薦

相關文章