前言
在 Spring
框架中,大家耳熟能詳的無非就是 IOC
,DI
,Spring MVC
,AOP
,這些是 Spring
中最基礎的核心功能,再高階點的功能就還有資料資料訪問模組(JDBC
,ORM
,事務等)。Spring
本身的擴充套件性也做得非常好,原始碼當中也是運用了大量設計模式來實現,瞭解 Spring
原始碼對於一個 Java
開發人員來說是非常有必要的,從原始碼中我們也可以學習到很多優秀的設計理念,現在就讓我們從 Spring IOC
開啟 Spring
原始碼之旅吧。
IOC
只是一個 Map
集合
提到 IOC
,初次接觸的人可能會覺得非常高大上,覺得是一種很高深的技術,然而事實呢?事實是 IOC
其實僅僅只是一個 Map
集合而已,並不是什麼高深的新技術,請各位大佬們坐下喝杯茶聽我細細道來。
IOC
全稱為:Inversion of Control。控制反轉的基本概念是:不用建立物件,但是需要描述建立物件的方式。
簡單的說我們本來在程式碼中建立一個物件是通過 new
關鍵字,而使用了 Spring
之後,我們不在需要自己去 new
一個物件了,而是直接通過容器裡面去取出來,再將其自動注入到我們需要的物件之中,即:依賴注入。
也就說建立物件的控制權不在我們程式設計師手上了,全部交由 Spring
進行管理,程式要只需要注入就可以了,所以才稱之為控制反轉。
實際上,IOC
也被稱之為 IOC
容器,那麼既然是一個容器,肯定是要用來放東西的,那麼 IOC
容器用來儲存什麼呢?如果大家對 Spring
有所瞭解的話,那就知道在 Spring
裡面可以說是一切面向 Bean
程式設計,而 Bean
指的就是我們交給 Spring
管理的物件,今天我們要學習的 IOC
容器就是用來儲存所有 Bean
的一個容器。
IOC 三大核心介面
Spring
作為一款優秀的框架,對於 Bean
的來源也支援很多種,那麼為了統一標準,自然需要定義一個配置檔案介面,這就是 BeanDefinition
;有了配置標準,那就要定義相關的類來將不同的配置檔案進行轉換,所以就有了 BeanDefinitionReader
;最終將 Bean
解析完成之後,那麼還需要對 Bean
進行操作,於是又有了 BeanFactory
。這三個介面就構成了 IOC
的核心:
- BeanDefinition:定義了一個
Bean
相關配置檔案的各種資訊,比如當前Bean
的構造器引數,屬性,以及其他一些資訊,這個介面同樣也會衍生出其他一些實現類,如 - BeanDefinitionReader:定義了一些讀取配置檔案的方法,支援使用
Resource
和String
位置引數指定載入方法,具體的時候可以擴充套件自己的特有方法。該類只是提供了一個建議標準,不要求所有的解析都實現這個介面。 - BeanFactory:訪問
Bean
容器的頂層介面,我們最常用的ApplicationContext
介面也實現了BeanFactory
。
IOC 初始化三大步驟
上面我們大致知道了 IOC
容器是什麼,也知道了 IOC
容器用來儲存什麼,同時也對 IOC
的核心三大介面混了個眼熟,那麼接下來我們就該瞭解下 Bean
到底是怎麼來的,存到 IOC
容器的又只是 Bean
本身還是做了進一步封裝呢?
帶著這兩個問題就讓我們來細細分析一下 IOC
的整個初始化流程。
IOC
的整個初始化流程可以概要的分為三大步驟:定位,載入,註冊。
- 定位:尋找需要初始化哪些
Bean
。 - 載入:將尋找到需要初始化的
Bean
進行解析封裝。 - 註冊:這一步就是將第二步載入後的
Bean
放入IOC
容器,也就是放入Map
集合之中。
定位
我們最常用的 Bean
一般來源於 xml
配置或者註解,那麼這些配置檔案又儲存在哪裡呢? 在 Spring
中配置檔案支援以下六種來源:
- classpath
- network
- filesystem
- servletContext
- annotation
接下來我們以我們最常用的一種方式作為入口來分析一下定位的流程(ApplicationContext
實現的頂層介面之一就是 BeanFactory
,所以其具有 BeanFactory
的操作 Bean
的能力):
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
applicationContext.getBean("myBean");
applicationContext.getBean(MyBean.class);
在以前使用傳統 Spring
的時候,我們就是通過上面這種方式來獲取 Bean
,定位的入口我們就從 ClassPathXmlApplicationContext
的入口開始吧。
這裡的邏輯非常簡單,先呼叫 setConfigLocations
方法設定配置檔案,然後核心就在 refresh
方法,refresh
是其父類實現的,而父類中的 refresh
方法的主幹就是在 522
行獲取一個 beanFactory
,後面的所有操作都是圍繞 beanFactory
做一些擴充套件操作。
其實看 522
行的註釋也可以知道,最終其還是會呼叫回子類也就是 AbstractRefreshableApplicationContext
來執行載入 bean
操作:
這裡面需要說明的是,核心邏輯是在 623
行,而 624
行實際上是從全域性變數內獲取 beanFactory
:
而這裡的全域性變數 beanFactory
就是 BeanFactory
的一個預設實現 DefaultListableBeanFactory
。瞭解了這個之後,我們繼續回到上面的 refreshBeanFactory
:
這個方法其實也很簡單,就是建立了一個預設的 DefaultListableBeanFactory
,然後就開始呼叫其子類 AbstractXmlApplicationContext
(同時其是 ClassPathXmlApplicationContext
父類)的 loadBeanDefinitions
方法:
載入
執行到上面的方法中,我們可以發現到一個 BeanDefinitionReader
物件 XmlBeanDefinitionReader
被建立了,這就說明到這裡差不多要開始載入配置檔案了,所以接下來要找主幹其實只要跟著這個 BeanDefinitionReader
物件就可以了,我們繼續進入 loadBeanDefinitions
方法:
這裡面分為了兩種情況,一種是根據 Resource
型別,一種是根據 String
型別,我們這裡因為傳的是一個 String
型別的路徑,所以會執行下面的邏輯,但是雖然執行的是下面的邏輯,但是最終還是會將我們傳入的 spring.xml
轉化成 Resource
,從而呼叫上面的解析方法。
接下來還會經過幾次“繞路”,然後還是會進入 XmlBeanDefinitionReader
物件的 loadBeanDefinitions
方法:
在這裡我們終於看到了一個令我們驚喜的方法 doLoadBeanDefinitions
,因為在 Spring
當中,基本上以 do
開頭的方法就是真正的核心處理邏輯方法:
這裡面就是呼叫了兩個方法,第一個就是把 resource
轉化成 document
物件,然後呼叫另一個方法準備註冊 bean
,當然怎麼解析我們的 xml
配置檔案,我們在這裡不做分析,繼續看主幹註冊 bean
的邏輯。
註冊
上面呼叫註冊方法之後,最終會由其子類 DefaultBeanDefinitionDocumentReader
來執行:
到這裡我們又開到了以 do
開頭的方法,說明這裡要開始註冊了。
這裡建立了一個委派者 delegate
,進入這個委派者我們可以發現,這裡面定義了 xml
檔案中的所有節點:
建立好委派者之後,接下來就可以開始呼叫 parseBeanDefinitions
來進行解析了:
到這裡又分成了三種情況,是否預設名稱空間以及是否預設節點,但是不管是什麼情況,最終都是會把節點資訊解析出來轉換成一個 bean
進行註冊,我們進入 parseDefaultElement
解析預設節點方法:
在這裡又分為了不同情況去解析 import
,alias
,bean
節點,也包括了巢狀節點的遞迴處理方式,我們繼續進入 processBeanDefinition
方法:
到這裡基本上就要結束註冊流程了,呼叫了 BeanDefinitionReaderUtils
工具類中的一個方法來進行註冊:
在這裡做了三件事:
- 獲取到
beanName
。 - 回到最開始的
DefaultListableBeanFactory
,呼叫registerBeanDefinition
方法 - 存在別名的話註冊一下別名。
在這裡最關鍵的是第二步,我們發現繞了一大圈最終回到了我們前面載入步驟中的 DefaultListableBeanFactory
類(下面這個方法我為了方便截圖,刪除了部分的異常判斷):
這個方法就是註冊 bean
的最後邏輯,首先會判斷當前 bean
是否已經被註冊,有的話會判斷是否允許覆蓋之類的一些設定,如果最終都能符合條件,那麼就會直接覆蓋(795
行),如果當前 bean
是首次建立,那麼還需要判斷當前整個 ioc
容器是否已經有建立好的 bean
,但是最終其實就是 this.beanDefinitionMap.put(beanName, beanDefinition);
這行程式碼完成了註冊,而 beanDefinitionMap
其實就是一個 ConcurrentHashMap
集合。
到這裡我們整個 ioc
載入主流程就分析結束了,其實整個邏輯非常簡單,而我們之所以會覺得 Spring
複雜難懂,其實是因為 Spring
為了擴充套件性,可讀性,經過了精心設計,整個框架中使用了非常多的設計模式和設計原則,致使我們看原始碼的時候覺得非常繞,但是隻要抓住核心主幹,讀懂原始碼也並不是難事。
總結
本文主要講述了 ioc
的初始化流程,整個過程其實是非常繞非常複雜的,第一次看的話非常容易繞迷路,所以我們需要抓住主流程,理解 ioc
的核心就是三個步驟:定位(找配置檔案),載入(解析配置檔案),註冊(將 bean
新增到 ioc
容器)非常關鍵,只要抓住這三個步驟,我們就能抓住重點一步步往下跟。所以如果我們把獲取 bean
的方式換成註解實現,無非就是把解析 xml
配置檔案的過程改為解析註解的過程,核心的後續流程其實還是一樣。