前言
在 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
配置文件的過程改為解析注解的過程,核心的后續流程其實還是一樣。