小白都能看懂的Spring原始碼揭祕之IOC容器原始碼分析

雙子孤狼發表於2021-11-05

前言

Spring 框架中,大家耳熟能詳的無非就是 IOCDISpring MVCAOP,這些是 Spring 中最基礎的核心功能,再高階點的功能就還有資料資料訪問模組(JDBCORM,事務等)。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:定義了一些讀取配置檔案的方法,支援使用 ResourceString 位置引數指定載入方法,具體的時候可以擴充套件自己的特有方法。該類只是提供了一個建議標準,不要求所有的解析都實現這個介面。
  • BeanFactory:訪問 Bean 容器的頂層介面,我們最常用的 ApplicationContext 介面也實現了 BeanFactory

IOC 初始化三大步驟

上面我們大致知道了 IOC 容器是什麼,也知道了 IOC 容器用來儲存什麼,同時也對 IOC 的核心三大介面混了個眼熟,那麼接下來我們就該瞭解下 Bean 到底是怎麼來的,存到 IOC 容器的又只是 Bean 本身還是做了進一步封裝呢?

帶著這兩個問題就讓我們來細細分析一下 IOC 的整個初始化流程。

IOC 的整個初始化流程可以概要的分為三大步驟:定位載入註冊

  1. 定位:尋找需要初始化哪些 Bean
  2. 載入:將尋找到需要初始化的 Bean 進行解析封裝。
  3. 註冊:這一步就是將第二步載入後的 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 解析預設節點方法:

在這裡又分為了不同情況去解析 importaliasbean 節點,也包括了巢狀節點的遞迴處理方式,我們繼續進入 processBeanDefinition 方法:

到這裡基本上就要結束註冊流程了,呼叫了 BeanDefinitionReaderUtils 工具類中的一個方法來進行註冊:

在這裡做了三件事:

  1. 獲取到 beanName
  2. 回到最開始的 DefaultListableBeanFactory,呼叫 registerBeanDefinition 方法
  3. 存在別名的話註冊一下別名。

在這裡最關鍵的是第二步,我們發現繞了一大圈最終回到了我們前面載入步驟中的 DefaultListableBeanFactory 類(下面這個方法我為了方便截圖,刪除了部分的異常判斷):

這個方法就是註冊 bean 的最後邏輯,首先會判斷當前 bean 是否已經被註冊,有的話會判斷是否允許覆蓋之類的一些設定,如果最終都能符合條件,那麼就會直接覆蓋(795 行),如果當前 bean 是首次建立,那麼還需要判斷當前整個 ioc 容器是否已經有建立好的 bean,但是最終其實就是 this.beanDefinitionMap.put(beanName, beanDefinition); 這行程式碼完成了註冊,而 beanDefinitionMap 其實就是一個 ConcurrentHashMap 集合。

到這裡我們整個 ioc 載入主流程就分析結束了,其實整個邏輯非常簡單,而我們之所以會覺得 Spring 複雜難懂,其實是因為 Spring 為了擴充套件性,可讀性,經過了精心設計,整個框架中使用了非常多的設計模式和設計原則,致使我們看原始碼的時候覺得非常繞,但是隻要抓住核心主幹,讀懂原始碼也並不是難事。

總結

本文主要講述了 ioc 的初始化流程,整個過程其實是非常繞非常複雜的,第一次看的話非常容易繞迷路,所以我們需要抓住主流程,理解 ioc 的核心就是三個步驟:定位(找配置檔案),載入(解析配置檔案),註冊(將 bean 新增到 ioc 容器)非常關鍵,只要抓住這三個步驟,我們就能抓住重點一步步往下跟。所以如果我們把獲取 bean 的方式換成註解實現,無非就是把解析 xml 配置檔案的過程改為解析註解的過程,核心的後續流程其實還是一樣。

相關文章