高頻面試題:Spring 如何解決循環依賴?

2020-12-14 酷扯兒

本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫

在關於Spring的面試中,我們經常會被問到一個問題:Spring是如何解決循環依賴的問題的。

這個問題算是關於Spring的一個高頻面試題,因為如果不刻意研讀,相信即使讀過源碼,面試者也不一定能夠一下子思考出箇中奧秘。

本文主要針對這個問題,從源碼的角度對其實現原理進行講解。

1. 過程演示

關於Spring bean的創建,其本質上還是一個對象的創建,既然是對象,讀者朋友一定要明白一點就是,一個完整的對象包含兩部分:當前對象實例化和對象屬性的實例化。

在Spring中,對象的實例化是通過反射實現的,而對象的屬性則是在對象實例化之後通過一定的方式設置的。

這個過程可以按照如下方式進行理解:

理解這一個點之後,對於循環依賴的理解就已經幫助一大步了,我們這裡以兩個類A和B為例進行講解,如下是A和B的聲明:

@Componentpublic class A {private B b; public void setB(B b) { this.b = b; }}

@Componentpublic class B {private A a; public void setA(A a) { this.a = a; }}

可以看到,這裡A和B中各自都以對方為自己的全局屬性。這裡首先需要說明的一點,Spring實例化bean是通過ApplicationContext.getBean()方法來進行的。

如果要獲取的對象依賴了另一個對象,那麼其首先會創建當前對象,然後通過遞歸的調用ApplicationContext.getBean()方法來獲取所依賴的對象,最後將獲取到的對象注入到當前對象中。

這裡我們以上面的首先初始化A對象實例為例進行講解。

首先Spring嘗試通過ApplicationContext.getBean()方法獲取A對象的實例,由於Spring容器中還沒有A對象實例,因而其會創建一個A對象

然後發現其依賴了B對象,因而會嘗試遞歸的通過ApplicationContext.getBean()方法獲取B對象的實例

但是Spring容器中此時也沒有B對象的實例,因而其還是會先創建一個B對象的實例。

讀者需要注意這個時間點,此時A對象和B對象都已經創建了,並且保存在Spring容器中了,只不過A對象的屬性b和B對象的屬性a都還沒有設置進去。

在前面Spring創建B對象之後,Spring發現B對象依賴了屬性A,因而還是會嘗試遞歸的調用ApplicationContext.getBean()方法獲取A對象的實例

因為Spring中已經有一個A對象的實例,雖然只是半成品(其屬性b還未初始化),但其也還是目標bean,因而會將該A對象的實例返回。

此時,B對象的屬性a就設置進去了,然後還是ApplicationContext.getBean()方法遞歸的返回,也就是將B對象的實例返回,此時就會將該實例設置到A對象的屬性b中。

這個時候,注意A對象的屬性b和B對象的屬性a都已經設置了目標對象的實例了

讀者朋友可能會比較疑惑的是,前面在為對象B設置屬性a的時候,這個A類型屬性還是個半成品。但是需要注意的是,這個A是一個引用,其本質上還是最開始就實例化的A對象。

而在上面這個遞歸過程的最後,Spring將獲取到的B對象實例設置到了A對象的屬性b中了

這裡的A對象其實和前面設置到實例B中的半成品A對象是同一個對象,其引用地址是同一個,這裡為A對象的b屬性設置了值,其實也就是為那個半成品的a屬性設置了值。

下面我們通過一個流程圖來對這個過程進行講解:

圖中getBean()表示調用Spring的ApplicationContext.getBean()方法,而該方法中的參數,則表示我們要嘗試獲取的目標對象。

圖中的黑色箭頭表示一開始的方法調用走向,走到最後,返回了Spring中緩存的A對象之後,表示遞歸調用返回了,此時使用綠色的箭頭表示。

從圖中我們可以很清楚的看到,B對象的a屬性是在第三步中注入的半成品A對象,而A對象的b屬性是在第二步中注入的成品B對象,此時半成品的A對象也就變成了成品的A對象,因為其屬性已經設置完成了。

2. 源碼講解

對於Spring處理循環依賴問題的方式,我們這裡通過上面的流程圖其實很容易就可以理解

需要注意的一個點,Spring是如何標記開始生成的A對象是一個半成品,並且是如何保存A對象的。

這裡的標記工作Spring是使用ApplicationContext的屬性SetsingletonsCurrentlyInCreation來保存的,而半成品的A對象則是通過MapsingletonFactories來保存的

這裡的ObjectFactory是一個工廠對象,可通過調用其getObject()方法來獲取目標對象。在AbstractBeanFactory.doGetBean()方法中獲取對象的方法如下:

protected T doGetBean(final String name, @Nullable final Class requiredType,@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { // 嘗試通過bean名稱獲取目標bean對象,比如這裡的A對象 Object sharedInstance = getSingleton(beanName); // 我們這裡的目標對象都是單例的 if (mbd.isSingleton()) { // 這裡就嘗試創建目標對象,第二個參數傳的就是一個ObjectFactory類型的對象,這裡是使用Java8的lamada // 表達式書寫的,只要上面的getSingleton()方法返回值為空,則會調用這裡的getSingleton()方法來創建 // 目標對象 sharedInstance = getSingleton(beanName, () -> { try { // 嘗試創建目標對象 return createBean(beanName, mbd, args); } catch (BeansException ex) { throw ex; } }); } return (T) bean;}

這裡的doGetBean()方法是非常關鍵的一個方法(中間省略了其他代碼),上面也主要有兩個步驟

第一個步驟的getSingleton()方法的作用是嘗試從緩存中獲取目標對象,如果沒有獲取到,則嘗試獲取半成品的目標對象;如果第一個步驟沒有獲取到目標對象的實例,那麼就進入第二個步驟

第二個步驟的getSingleton()方法的作用是嘗試創建目標對象,並且為該對象注入其所依賴的屬性。

這裡其實就是主幹邏輯,我們前面圖中已經標明,在整個過程中會調用三次doGetBean()方法

第一次調用的時候會嘗試獲取A對象實例,此時走的是第一個getSingleton()方法,由於沒有已經創建的A對象的成品或半成品,因而這裡得到的是null

然後就會調用第二個getSingleton()方法,創建A對象的實例,然後遞歸的調用doGetBean()方法,嘗試獲取B對象的實例以注入到A對象中

此時由於Spring容器中也沒有B對象的成品或半成品,因而還是會走到第二個getSingleton()方法,在該方法中創建B對象的實例

創建完成之後,嘗試獲取其所依賴的A的實例作為其屬性,因而還是會遞歸的調用doGetBean()方法

此時需要注意的是,在前面由於已經有了一個半成品的A對象的實例,因而這個時候,再嘗試獲取A對象的實例的時候,會走第一個getSingleton()方法

在該方法中會得到一個半成品的A對象的實例,然後將該實例返回,並且將其注入到B對象的屬性a中,此時B對象實例化完成。

然後,將實例化完成的B對象遞歸的返回,此時就會將該實例注入到A對象中,這樣就得到了一個成品的A對象。

我們這裡可以閱讀上面的第一個getSingleton()方法:

@Nullableprotected Object getSingleton(String beanName, boolean allowEarlyReference) {// 嘗試從緩存中獲取成品的目標對象,如果存在,則直接返回 Object singletonObject = this.singletonObjects.get(beanName); // 如果緩存中不存在目標對象,則判斷當前對象是否已經處於創建過程中,在前面的講解中,第一次嘗試獲取A對象 // 的實例之後,就會將A對象標記為正在創建中,因而最後再嘗試獲取A對象的時候,這裡的if判斷就會為true if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { // 這裡的singletonFactories是一個Map,其key是bean的名稱,而值是一個ObjectFactory類型的 // 對象,這裡對於A和B而言,調用圖其getObject()方法返回的就是A和B對象的實例,無論是否是半成品 ObjectFactory singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // 獲取目標對象的實例 singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject;}

這裡我們會存在一個問題就是A的半成品實例是如何實例化的,然後是如何將其封裝為一個ObjectFactory類型的對象,並且將其放到上面的singletonFactories屬性中的。

這主要是在前面的第二個getSingleton()方法中,其最終會通過其傳入的第二個參數,從而調用createBean()方法,該方法的最終調用是委託給了另一個doCreateBean()方法進行的

這裡面有如下一段代碼:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)throws BeanCreationException { // 實例化當前嘗試獲取的bean對象,比如A對象和B對象都是在這裡實例化的 BeanWrapper instanceWrapper = null; if (mbd.isSingleton()) { instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); } if (instanceWrapper == null) { instanceWrapper = createBeanInstance(beanName, mbd, args); } // 判斷Spring是否配置了支持提前暴露目標bean,也就是是否支持提前暴露半成品的bean boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { // 如果支持,這裡就會將當前生成的半成品的bean放到singletonFactories中,這個singletonFactories // 就是前面第一個getSingleton()方法中所使用到的singletonFactories屬性,也就是說,這裡就是 // 封裝半成品的bean的地方。而這裡的getEarlyBeanReference()本質上是直接將放入的第三個參數,也就是 // 目標bean直接返回 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } try { // 在初始化實例之後,這裡就是判斷當前bean是否依賴了其他的bean,如果依賴了, // 就會遞歸的調用getBean()方法嘗試獲取目標bean populateBean(beanName, mbd, instanceWrapper); } catch (Throwable ex) { // 省略... } return exposedObject;}

到這裡,Spring整個解決循環依賴問題的實現思路已經比較清楚了。對於整體過程,讀者朋友只要理解兩點:

Spring是通過遞歸的方式獲取目標bean及其所依賴的bean的;Spring實例化一個bean的時候,是分兩步進行的,首先實例化目標bean,然後為其注入屬性。結合這兩點,也就是說,Spring在實例化一個bean的時候,是首先遞歸的實例化其所依賴的所有bean,直到某個bean沒有依賴其他bean,此時就會將該實例返回,然後反遞歸的將獲取到的bean設置為各個上層bean的屬性的。

End

相關焦點

  • 15個近期JavaScript高頻手寫面試題
    作為前端開發工程師,JavaScript是我們技術能力中最重要的一環,最近結束了面試的高峰期,來給大家總結了一下最近比較高頻出現的15道JavaScript手寫面試題,一起來肝吧!
  • 面試刷題:Spring Bean的生命周期?
    spring是Java軟體開發的事實標準。今天的問題是:springBean的生命周期是怎樣的?答:spring最基礎的能力是IOC(依賴注入),AOP(面向切面編程),ioc改善了模塊之間的耦合問題,依賴注入的方式:set方法,構造方法,成員變量+ @Autowire ;Bean的管理是IOC的主要功能。
  • 繼「劉強東」之後京東的第二位程式設計師「呂科」spring面試題講解
    ,但是具體面試內容是什麼,2020年7月1日,京東的第二位程式設計師「呂科」講解最新面試信息:一面:也就是基礎面試二面:資料庫基礎面試三面:綜合面試四面:HR面試今天講解的是京東spring面試問題答案及spring常見面試問題答案1、什麼是Spring框架?
  • Java經典面試題Spring是什麼 Spring框架入門詳解
    接下來我們看一下spring另一個特性,DI依賴注入是什麼。上面Java代碼中的username,userage都是由我手動設置的,但是這樣實在太過於麻煩,於是Spring提供了為對象屬性注入的功能。那麼spring是否能夠完成我們自定義java對象的注入呢?
  • 面試刷題容易被忽視的點:Spring系列+Mybatis+ZK+ES+MQ
    有效的準備面試,無疑是獲得高薪水的關鍵。無論你是近期打算跳槽,還是金九銀十準備跳槽,我想此刻開始準備面試,無疑是最明智的選擇,所以小編整理匯總了大量的乾貨面試題,下面一起來看吧:Spring面試題什麼是Spring框架?Spring框架有哪些主要模塊?
  • Spring面試題:SpringBoot開發自定義starter
    應用程式只需要在maven中引入starter依賴,SpringBoot就能自動掃描到要加載的信息並啟動相應的默認配置。用一句話描述,就是springboot的場景啟動器。下面是Spring官方提供的部分starter,全部的請參考官網:
  • 2020年6月最新BAT一線大廠JAVA崗高頻面試題:阿里+華為+字節跳動
    前言近期根據網友分享大廠面試題目,今天我將網友面試的BAT等大廠JAVA崗面試題目整理出來,希望能夠幫助大家!13.知道spring AOP是如何實現的麼,動態代理和CGlib分別是如何實現的14.了解Dubbo框架不,看過源碼沒,了解實現原理嗎?15.給定一個整數數組和一個整數,返回兩個數組的索引,這兩個索引指向的數字的加和等於指定的整數。
  • GitHub上訪問下載破百萬的神仙文檔《Java面試神技》看完我呆了!
    這份文檔包含了:JavaOoP面試題,Java集合/泛型面試題,Javs異常面試題,Java中的I0與NI0面試題,Java反射面試題,Java序列化面試題,Javs註解面試題,多線程並發麵試題,JVM面試題,Mysq1面試題,Redi s面試題,Meme ached面試題,MongoDB面試題,Spring面試題,Spring Boot
  • 挑戰全網Java最新面試匯總:Redis+ JVM+ Spring+消息中間+微服務
    不多逼逼,上才藝:消息中間件面試題(RocketMq+ActiveMQ+RocketMq)什麼是 ActiveMQ?ActiveMQ 伺服器宕機怎麼辦?Dubbo 在安全機制方面是如何解決?等.........Java多線程面試題什麼是線程安全和線程不安全?
  • 面試題:SpringBoot的啟動流程
    面試題:SpringBoot的啟動流程不管是用springboot開發還是面試,都需要對SpringBoot的啟動流程所了解。SpringApplication的構造方法,其中做了幾件事情推斷WebApplicationType,主要思想就是在當前的classpath下搜索特定的類搜索META-INF\spring.factories文件配置的ApplicationContextInitializer
  • 阿里面試總結:69道必問的spring面試題(附加答案)
    什麼是spring?2. 使用Spring框架的好處是什麼?3. Spring由哪些模塊組成?4. 核心容器(應用上下文) 模塊。5. BeanFactory – BeanFactory 實現舉例。什麼是Spring的依賴注入?19. 有哪些不同類型的IOC(依賴注入)方式?20. 哪種依賴注入方式你建議使用,構造器注入,還是 Setter方法注入?21.什麼是Spring beans?22.
  • 高頻面試題:什麼是零拷貝?在哪些地方使用了?
    這是一道高頻的面試題,而且在很多技術中都使用到了,比如javaNIO、kafka、Netty、Linux等等。作為一個非常重要的知識點,而且又是高頻面試題,有必要從零開始好好地認識一下。即使你是剛入門的同行,相信也能看的懂。OK,開始今天的文章。一、什麼是零拷貝?
  • 騰訊產品面試題:如何把剃鬚刀賣給張飛?
    「如何把剃鬚刀賣給張飛」這道題很像銷售常考察的題目,不過它可是2018年被稱為「騰訊淘汰率最高」的騰訊產品面試題,那麼產品經理求職者們該如何解答這道題呢?筆者將從產品思維和業務思維展開分析。這是2018年被求職者評為「騰訊淘汰率最高」的騰訊產品面試題,最近有用戶在後臺留言「這道題又重出江湖了」,但思來想去都覺得是個考察銷售能力的題,希望給出產品角度的思考。如果這道題目是你第一次見,可能覺得要不就是銷售題要不就是腦洞題,沒有研究的必要。
  • 公司來了一位前阿里大神,分享8面阿里面經(Java崗面試題集錦)
    下面我說一下自己面試的流程:剛開始的時候面試官會讓自我介紹,閒聊一小會(主要是為了緩解緊張的氣氛),下面就進入了正題(以下面試題都是涉及到的,沒有一一都記清楚,記了個大概):基礎篇(面試完後期又整理了一下)
  • 面試官:你了解spring嗎?spring的兩大核心是什麼?
    IOC(DI) - 控制反轉(依賴注入)所謂的IOC稱之為控制反轉,簡單來說就是將對象的創建的權利及對象的生命周期的管理過程交由Spring框架來處理,從此在開發過程中不再需要關注對象的創建和生命周期的管理,而是在需要時由Spring框架提供,這個由spring框架管理對象創建和生命周期的機制稱之為控制反轉。
  • 2021年山東省公務員面試技巧:漫畫題如何做答
    2021年山東省公務員面試技巧:漫畫題如何做答 山東公務員面試:本頻道提供山東公務員結構化面試、面試技巧>、面試熱點等山東省考面試資料。
  • 2020Web前端開發常見面試題匯總-開課吧
    以下是小編為大家整理的web前端面試題及答案,供各位參考。Web前端面試題:異步請求適合在哪個生命周期調?解析:官實例的異步請求是在mounted命周期中調的,實際上也可以在created命周期中調。Web前端面試題:各個生命周期的作用是什麼?
  • 2020山東事業單位結構化面試:計劃組織題如何表述自己的想法
    2020山東事業單位結構化面試:計劃組織題如何表述自己的想法 2020-12-22 15:14:38| 中公事業單位考試題庫 2020山東事業單位面試陸續進行,為了方便大家備考山東事業單位面試
  • 大廠面試題解析:如何設計一款老年人專用的ATM機?
    產品經理面試中,我們常常會遇到一些看起來很簡單、甚至一看就能脫口而出給答案的問題。當時這種題往往又帶著陷阱——越是憑藉直覺、或者是思維停留在消費者級認知層面,認識產品理解產品,越暴露自己缺乏產品思維。今天是美圖的面試題~之所以選擇今天說這個選題是因為老年人這個群體我們看似熟悉其實卻很陌生,為他們專門設計一款產品我想對我而言是一種挑戰。
  • 如何應對公務員考試中的面試題
    對於應對公務員考試中的面試題,我感覺,是一個需要系統認識的問題。非要一個明確的答案的話,就是:從容應對。那麼要做到從容應對,應該做到以下幾點:第一點,要從容應對公務員考試中的面試題,首先要做到知彼。第二點,要從容應對,公務員考試中的面試題,必須要做到知己。要通過面試的學習和訓練了解到,自己最擅長什麼樣的題型,自己的短板是什麼樣的?自己的表達習慣和靈敏度集中反映在哪些方面上?這樣才能夠揚長補短。第三點,要從容應對公務員考試中的面試題,要努力做到熟能生巧。