Spring IoC源碼探索(一)

1、探索前:談談我對IoC容器的瞭解

IoC容器主要用於管理Bean的生命週期和對象間的關係,經過依賴注入(DI)對容器中的Bean所須要依賴的其餘對象進行注入。而這一切都是在Ioc容器裏邊進行的,假設A對象依賴B對象,若是IoC容器裏只有A沒有B,那麼將會拋出bean找不到的異常;或者說A對象不在IoC容器,而B對象在IoC容器,那麼將達不到自動注入的效果。java

2、探索源碼:以ClassPathXMLApplicationContext爲例看IoC容器的建立過程——Spring版本爲5.1.7.RELEASE

1)ClassPathXMLApplicationContext的建立

2)進入構造器,發現其調了另外一個更全面(複雜)的構造器

3)很容易從字面上獲得信息,spring的配置文件容許多個,是否要刷新(重載加載容器),還有一個是父級容器(能夠延伸理解springMVC的容器和應用頂層IoC之間的父子關係),固然咱們目前是一個被強轉的null,等於當前容器爲頂級容器(Root)。

4)深刻super能夠看到,在AbstractApplicationContext抽象類中,調用了setParent方法,若是父級容器不爲空,還會進行了一些環境變量的合併操做,這裏就再也不深刻。

5)追蹤setConfigLocations方法,發現全部配置文件路徑都只會保留在當前容器裏,並且在保存配置文件時,還會對路徑字符串進行一個trim的操做,也就是說,在傳入配置文件路徑時,兩端有空格也是不礙事的。

OK,在保存配置文件路徑前,還有一個resolvePath解析配置文件的路徑,具體就不深刻,我猜應該是將環境變量(如:classpath)解析出來吧。web

6)最後跟蹤refresh,由於只傳入路徑的構造器默認傳入的refresh形參爲true,所以是必然會調用refresh()方法的。並且,父容器設置了,配置文件路徑設置了,確定要開始初始化了。

這裏是父級抽象類裏的方法,方法實現自更上一級的ConfigurableApplicationContext接口。spring

能夠看到,方法一上來就是一個sync,用於作鎖的對象是一個new Object();編程

而後會有一個刷新前的準備操做,好比記錄刷新開始時間啦,一些狀態變量啦,打印日誌啦。緩存

這裏仍是看下getEnvironment().validateRequiredProperties();具體是什麼,由於在其它地方都常見到Enviroment這個類,好比解析路徑的時候就有它。session

OK,能夠看到它屬性解析器裏有el表達式的 ${ } 和 : 符號,並且屬性源列表裏有兩個類型的屬性源,打開其中一個,有不少K/V鍵值對,結合各類類、屬性的各類語義,看來我以前的猜測無誤,確實是用於把變量解析出值的。多線程

行,下一個。app

7)告訴子類刷新內部bean工廠(百度翻譯過來的)

1 // Tell the subclass to refresh the internal bean factory.
2 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

跟蹤一下obtainFreshBeanFactory();jvm

OK長話短說吧,若是有BeanFactory,那麼關閉它構建過的單例,再把它關閉掉(置爲null讓GC回收它),而後在建立一個新的BeanFactory,並進行一些初始化操做,用sync上鎖賦值。值得一提的是,建立BeanFactory時,會先嚐試獲取父容器的BeanFactory做爲parentBeanFactory,不過如今是頂級容器,因此註定爲null。函數式編程

8)差點忽略重點,上面講到建立BeanFactory,第133行代碼的位置,loadBeanDefintion這一行相當重要,正是我想了解的地方,接着追蹤查看。

 兜兜轉轉,終於來到了解析xml定義的bean的代碼位置。

 

OK,解讀這一段:

首先,獲取<bean>節點上的id屬性;

接着,獲取<bean>節點上的name屬性;那麼,name屬性呢,會被做爲別名,多個別名能夠容許用英文半角符號下的逗號和分號進行分割(, ;)。

接下來,就是以(,;)分割name屬性上的值,將每一個別名都分割出來,雖然它在內部對分割出來的每一個別名作了trim操做,但我仍是建議不要留有空格。

再接着,肯定beanName,若是id屬性有的話,就用id屬性,沒有的話,就會從別名中抽出第一個別名做爲beanName,我說的抽出,就是它裏邊的aliases.remove(0)。

而後呢,會進行一個對beanName的查重操做,這裏邊能夠看出,beanName和aliase是存放在同一個命名空間的(Set<String>集合),所以,在上一步aliases須要用remove(),不然本身就會拋名稱重複異常。

接下來都差很少,就是一些屬性的解析,子節點的解析。而後都封裝到org.springframework.beans.factory.support.DefaultListableBeanFactory的beanDefinitionMap屬性裏,固然還有不少其它的細節我沒有去深究,水平有限,太細了腦殼會爆的。

9)那麼何時開始實例化單例呢,回到refresh()方法.能夠看到在獲取到beanFactory後,進行了不少準備操做,紅框部分爲真正實例化Bean的部分,在它先後能夠看到onRefresh()和finishRefresh()方法,其中onRefresh是一個空方法,用於擴展實例化Bean前的操做。接下來看紅框部分的finishBeanFactoryInitialization。

進入方法後也能看到,Spring的源碼裏有用到Lambda表達式,以前有遇到過Java8以前版本Lambda報錯,因而這裏我嘗試了把IDEA的Language level改成了7,發現也能正常運行,仔細回想,以前碰到的Lambda報錯是由於IDEA容許寫Lambda,可是因爲選擇的java編譯器是Java8版本前的版本,因而編譯不經過。那麼這麼一想,我現在加入的依賴其實是Spring已經編譯好的jar包,也就是說Lambda表達式已經被編譯成了字節碼文件,即便把運行環境切換回Java8以前的版本,也是能夠運行的。那麼我大膽的猜想,Java8和以後推出的一些語法糖在編譯後,一樣能在低版本的jvm上運行(這個離題了,以後再試,到時候順便去看看官方文檔)。

我們繼續分析上面的代碼,最後一行就是初始化單例的操做了,那麼在它的上一行,還有一個凍結配置的操做,代碼粘貼出來,都能看懂,凍結後具體有什麼用,咱先不看。

繼續,重點來了。

 

循環迭代全部定義的Bean,獲取到Bean在Root容器中的定義 (這塊的合併定義有點東西,有空再看仔細),接着就是判斷Bean的類型是否爲org.springframework.beans.factory.FactoryBean接口的派生類,若是是,還會通過一些系統權限特殊處理。固然,最後都會到達getBean(beanName)。

看到這個getBean(beanName),我突然猜測,RootBeanDefinition是有一個lazyInit屬性的,這個屬性默認爲false,爲的就是讓Bean在須要時,纔會初始化,那麼getBean是否就是爲須要Bean時調用的方法,這樣的話一切邏輯都能說得通,在getBean的時候,若是Bean沒初始化,那麼給Bean初始化再返回Bean,所以,非懶加載的Bean單例直接調用getBean便可完成初始化。Ok,帶着猜測繼續往下看。

10)而getBean的底層全是org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean

11)那繼續深刻,doGetBean,先看下方法下的代碼

 

嗯,方法還挺長的,截了好幾回圖。

簡單理解下:

1.嘗試獲取單例,若是單例已經存在了,那麼作一些驗證後沒有問題直接返回這個單例,裏邊的邏輯還涉及一些工廠Bean和普通Bean的選擇問題,也挺複雜的,就不細說。這其實已經能夠證實了我上面的猜想了。

2.1若是示例並不存在,那麼首先檢查它是否是在構建中,若是已經在構建中了這裏還來構建, 那可能已經進入了一個循環構建的狀態了,這時候就會直接拋出【BeanCurrentlyInCreationException】,後邊我會針對這個異常進行測試。

2.2先獲取父級BeanFactory,若是存在父級的BeanFactory而且自身有沒有該beanName對應的BeanDefinition的話,那用父級的BeanFactory來提供這個Bean,若是一直到Root級的容器也提供不了這個Bean,那就是【NoSuchBeanDefinitionException】了。那麼這裏的話能夠參考上面的第7大點,建立BeanFactory的時候,會將父級容器裏的BeanFactory做爲本身的patentBeanFactory,那咱們這裏的是Root級別的容器,BeanFactory也就是Root級別的,因此只能本身構建了。

2.3判斷下是否要類型檢查,通常都是false,我看來一下,只有一個地方用了true,位置在方法org.springframework.beans.factory.support.AbstractBeanFactory#getTypeForFactoryBean。 咱不深究,就以如今是false的狀態繼續吧,會執行markBeanAsCreated(beanName),這裏在真正構造Bean以前,先記錄一下Bean已經構建了,而後還把mergedBeanDefinitions集合裏的Bean的定義給remove掉了。

2.4分析Bean定義開始,調用的getMergedLocalBeanDefinition(beanName),哇塞,上一步剛吧BeanDefinition從mergedBeanDefinitions集合中remove掉,這一步又給加回去了,感受刪掉那一步有些畫蛇添足,就像是個BUG,不過倒不會引發什麼異常。

接下來注意了注意了,這裏要開始構建了,若是忽略掉多級容器,多線程什麼什麼的,我以爲這個位置算是最核心部分了,在我看來接下來這塊就是IoC和DI的具體實現。

2.5從RootBeanDefinition定義中獲取到構建依賴(dependsOn),這個dependsOn,其實就是在配置<bean>的時候能夠配置一個"depend-on"這麼一個屬性,裏邊只能配置其餘的beanName,若是這個屬性有值,就會先等待依賴的beanName先構建好,再繼續構建自身,若是配置的是本身,或者是依賴自身的其餘類,那麼就會陷入死循環,拋出異常好比:【BeanCreationException】。

2.6若是存在依賴,那麼註冊依賴關係,這依賴關係是雙向維護的,若是A依賴B,那麼A所需依賴裏有B,B的被他依賴裏有A。

2.7而後先把所需依賴給初始化了,也就是A依賴B,那麼先把B給初始化了,這裏就算是爲DI作前置準備了。

2.8接着,就判斷Bean的scope,若是是singleton,getSingleton

若是是prototype,那就從新構建一個Bean的實例。

再else,還有別的特殊處理,好比request、session,是須要webApplicationContext下才生效的這裏就不翻那麼多了。

這裏咱先關注singleton,能夠看到getSingleton()穿了兩個參數,一個是beanName,一個是Lambda表達式,嗯,就是構建了一個匿名內部類的實例做爲參數。嗯,函數式編程槓槓的。

 

2.8.1這裏一樣是,若是單例已經存在了,就直接返回,若是沒有存在,就調用專屬的ObjectFactory#getObject構建一個實例再返回。

2.8.2跟蹤createBean方法。

再跟蹤doCreateBean,

在一連串繁瑣的處理後,終於仍是來到了BeanUtils……

先給構造器設置爲可訪問的,還檢查一下是否爲Kotlin的類,是就用Kotlin的方式構造實例,不是就用調用原生的newInstance。唉,再深刻就是反射的源代碼了,前段時間剛看到一篇文章說反射的對象調用超過必定次數後會被生成class字節碼加載到jvm裏,今天看到了那段代碼,可是讓腦子緩緩吧,改天再研究反射的實現。

最後,對象構建完成後,還有屬性的注入。

在doCreateBean方法內,有這麼一段代碼:

其中populateBean則是對屬性進行賦值的,一直找到org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyPropertyValues

會看到遍歷屬性賦值這一段:

若是有關聯(即ref),則會在valueResolver.resolveValueIfNecessary(pv, originalValue);內進行檢查。

跟蹤查看

 

再跟蹤到resolveReference裏:

能夠看到它會在這裏getBean,若是ref的bean已存在,天然會直接獲得,若是沒有存在,則會先初始化。嗯,而後上面的初始化操做又來一遍。

屬性值獲得後,會通過一系列類型轉換的處理完後,裝好在屬性值映射集合裏,再經過反射調用Setter方法給屬性賦值,固然了,類型轉換的處理主要是基本屬性類型和包裝類,至於複雜屬性類型(引用類型)直接強轉就OK。

備註:

用於類型轉換的類型修改器默認只有overriddenDefaultEditors,共12個,均爲與IO相關的好比URL、InputSteam。

基本類型的修改器在首次查到沒有時,纔會建立,共47個,好比int、long、String,當前Spring版本爲5.1.7.RELEASE。

12)至於註解方式的注入,我下次再探究,但我推測,只須要在掃描包的時候,將註解式修飾的類、方法、屬性等解析成BeanDefinition,同樣能夠進入這個流程。

3、這裏咱們對一些依賴異常進行測試和筆記。

1)實例的屬性依賴自身:

 

① scope="singleton"時,Spring能處理好這個關係,成功容許。

② scope="prototype"時,會拋出異常【BeanCurrentlyInCreationException】。也以前咱們提到的那段檢查的代碼,就是用來檢查scope="prototype"的。

 2)<bean> 的depends-on依賴自身:

 解析成BeanDefinition的時候是不拋異常的,只有在運行到構建dependOn時,纔會拋出異常【BeanCreationException】,不管scope是prototype仍是singleton。

3)構造器中依賴自身:

 一樣,無論scope是prototype仍是singleton,都會拋出異常【BeanCurrentlyInCreationException】。

4)兩個Bean構造器中相互依賴:

 

雙方是 都是scope="prototype"時,拋出異常【BeanCurrentlyInCreationException】。只要有一方是 scope="singleton",則可正常運行。

若是是多個,好比:

 

 那Role必須是單例的狀況下,才能正常運行了。

4、總結:

從容器結構來看,理論上IoC容器能夠達到無限嵌套,在子容器維護着父級容器的關係,父子容器各自定義的Bean的單例都會緩存在各自的BeanFactory的singletonObjects裏,當子容器在singletonObjects中找不到Bean時,會往父容器裏找,或者說子容器中能夠定義新的Bean屏蔽掉父級的Bean,使得切換不一樣的Bean實現能夠更加靈活。可是呢,父容器由於沒有維護與子容器的關係,所以父容器裏是沒法經過getBean獲取到子容器的Bean的。

Bean的定義上,會將Bean的構建條件都解析封裝到BeanDefinition中,纔開始初始化單例。定義時,要規避死循環通常的依賴,也就是在實例化Bean前,避免依賴關係又回當前Bean。

從IoC容器中獲取一個未實例化的Bean A時,會先將它全部的必要依賴(depend-on和constructor-arg)加載到容器,再實例化Bean A,以後將屬性中的依賴(property ref="xxx")從IoC容器中獲取獲得,再將準備好的全部自動裝配的屬性值經過反射調用Setter方法賦值給Bean A。

自個畫了兩張圖:

① 父子容器間的關係圖:

 

② 容器初始化大體流程

 

水平有限,若是有哪裏寫的不對的,歡迎指出,我會及時改正,避免誤導你們。

相關文章
相關標籤/搜索