1.1 IOC容器到底是什么
IOC和AOP是Spring框架的核心功能,而IOC又是AOP實現的基礎,因而可以說IOC是整個Spring框架的基石。那么什么是IOC?IOC即控制反轉,通俗的說就是讓Spring框架來幫助我們完成對象的依賴管理和生命周期控制等等工作。從面向對象的角度來說,具有這種行為,完成這種工作的主體就可以形象的稱之為IOC容器。從代碼角度來看,IOC容器不過是Spring中定義的具有IOC基本功能的一些類的統稱,這些類都遵循一些共同的接口規范,所以我們可以說實現某些接口的具體的實現類就是IOC容器。而IOC容器的啟動流程,說白了就是創建並初始化一個該實現類的實例的過程,在這個過程中要進行諸如配置文件的加載解析,核心組件的注冊,bean 實例的創建等一系列繁瑣復雜的操作,因而整個過程顯得相對漫長,邏輯也相對復雜。
1.2 BeanFactory和ApplicationContext的聯系以及區別
前面說到Spring中為容器類定義了一些接口規范,如下圖所示
具體而言,Spring中的容器類可以分為兩大類。
-
一類是由BeanFactory接口定義的核心容器。BeanFactory位於整個容器類體系結構的頂端,其基本實現類為DefaultListableBeanFactory。之所以稱其為核心容器,是因為該類容器實現IOC的核心功能:比如配置文件的加載解析,Bean依賴的注入以及生命周期的管理等。BeanFactory作為Spring框架的基礎設施,面向Spring框架本身,一般不會被用戶直接使用。
-
另一類則是由ApplicationContext接口定義的容器,通常譯為應用上下文,不過稱其為應用容器可能更形象些。它在BeanFactory提供的核心IOC功能之上作了擴展。通常ApplicationContext的實現類內部都持有一個BeanFactory的實例,IOC容器的核心功能會交由它去完成。而ApplicationContext本身,則專注於在應用層對BeanFactory作擴展,比如提供對國際化的支持,支持框架級的事件監聽機制以及增加了很多對應用環境的適配等。ApplicationContext面向的是使用Spring框架的開發者。開發中經常使用的ClassPathXmlApplicationContext就是典型的Spring的應用容器,也是要進行解讀的IOC容器。
1.3 解讀IOC容器啟動流程的意義
-
1.IOC模塊是整個Spring框架的核心,是實現其他模塊的基礎。IOC容器在啟動時會注冊並初始化Spring框架的所有基礎組件,這些組件不僅在IOC模塊中被用到,也會被AOP等模塊使用。因而熟悉IOC容器的啟動流程不僅是掌握IOC模塊的關鍵,也是理解整個Spring框架的前提。
-
2.Spring是個很靈活的框架,允許用戶在原有功能上進行擴展或者進行滿足業務需求的個性化設置,比如對容器和Bean的生命周期過程進行增強,進行事件監聽等等。要更好的使用Spring的這些特性,必須了解其工作原理,而答案就在IOC容器的啟動過程中。
-
3.Spring框架在實現時使用了大量的設計模式,體現了很多優秀的設計思想。其IOC容器的啟動源碼就是供開發者學習這種設計經驗的絕佳樣板。
長求總:為了更好的理解和使用Spring框架並從它優秀的設計和實現經驗中進行學習。
1.4 如何有效的閱讀源碼
Spring框架經過多年的發展,隨着功能特性的增加,其實現也越來越復雜和抽象,要徹底弄清楚框架實現的每一個細節並不是一件簡單的事。因而,對於Spring源碼的解讀,不必死摳每個方法和實現細節,這樣太浪費時間,畢竟對於絕大分開發者而言,閱讀Spring源碼並不是為了成為Spring框架的開發者,而是為了更好的理解和使用Spring框架,或者從更高的角度,學習Spring的設計經驗和思想,並將其運用到自己的項目實踐中。
由於Spring容器的啟動流程十分冗長,內容實在太多,全部放在一篇進行講解實在太臃腫,也十分影響閱讀體驗。因而采取化整為零的策略,將整個IOC容器的啟動流程划分為若干個階段,每篇只對其中一個階段進行詳細講解,因而對於容器啟動源碼的解讀,主要抓住以下兩個要點:
-
1.對容器啟動流程的梳理
容器啟動流程分為哪幾個階段,在每個階段容器做了哪些工作,初始化了哪些組件,執行了哪些用戶自定義的回調函數。 -
2.對設計模式和設計思想的學習
在實現這個功能時采用了哪些設計模式,遵循了哪些設計思想,這么做有哪些好處。
2. 初探IOC容器啟動源碼
本次源碼閱讀的Spring版本為4.3.10.RELEASE。
啟動Spring容器,本質上是創建並初始化一個具體的容器類的過程,以常見的容器類ClassPathXmlApplicationContext為例,啟動一個Spring容器可以用以下代碼表示
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
盡管只有短短的一行代碼,但已經創建並啟動了一個Spring的IOC容器。為了后面更好的理解,先來看下ClassPathXmlApplicationContext的類繼承結構
關鍵的幾個類已經用紅色箭頭標注了出來。
-
AbstractApplicationContext
ApplicationContext接口的抽象實現類,能夠自動檢測並注冊各種后置處理器(PostProcessor)和事件監聽器(Listener),以模板方法模式定義了一些容器的通用方法,比如啟動容器的真正方法refresh()就是在該類中定義的。 -
AbstractRefreshableApplicationContext
繼承AbstractApplicationContext的抽象類。內部持有一個DefaultListableBeanFactory 的實例,使得繼承AbstractRefreshableApplicationContext的Spring的應用容器內部默認有一個Spring的核心容器,那么Spring容器的一些核心功能就可以委托給內部的核心容器去完成。AbstractRefreshableApplicationContext在內部定義了創建,銷毀以及刷新核心容器BeanFactory的方法。 -
ClassPathXmlApplicationContext
最常用的Spring的應用容器之一。在啟動時會加載類路徑下的xml文件作為容器的配置信息。
下面就正式開始容器啟動流程的源碼閱讀
進入ClassPathXmlApplicationContext的構造方法,首先調用了重載構造函數
/**
* Create a new ClassPathXmlApplicationContext, loading the definitions
* from the given XML file and automatically refreshing the context.
* @param configLocation resource location
* @throws BeansException if context creation failed
*/
public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
this(new String[] {configLocation}, true, null);
}
這里有兩點需要注意下:
- 1.創建ClassPathXmlApplicationContext時需要指定xml文件的路徑作為參數,盡管我們在創建時只指定了一個,但其實可以同時指定多個。
- 2.Spring容器有父子容器的概念,通過HierarchicalBeanFactory接口定義了具有層級關系的容器體系。而在抽象實現類AbstractApplicationContext類的內部,有一個表示父容器的成員變量
/** Parent context */
private ApplicationContext parent;
重載函數的第三個參數即表示要創建的ClassPathXmlApplicationContext的父容器,不過這里只需要設置為null。關於Spring的父子容器,還有一些獨特的訪問規則,子容器可以訪問父容器中的Bean,父容器不可以訪問子容器中的Bean。不知道這個規則在使用Spring做web開發時可能會碰到一些匪夷所思的問題。
繼續跟進源碼
//設置父容器
super(parent);
//設置xml文件的路徑參數
setConfigLocations(configLocations);
if (refresh) { //默認為true
//啟動Spring容器
refresh();
}
設置完父容器和xml文件的路徑信息后,終於看到了refresh()方法,正如前面提到的,這是真正啟動Spring容器的方法,想要知道Spring IOC容器的啟動流程,就要知道該方法內部都做了什么。
2.1 啟動容器的真正入口refresh()
refresh()是定義在AbstractApplicationContext類中的模板方法,定義了容器啟動的基本流程,並留下鈎子方法供子類進行擴展。
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
啟動容器的方法之所以用refresh(刷新)來命名,是為了形象的表達容器可以被重啟這層含義。為了防止並發環境下多個線程同時啟動IOC容器,整個過程使用同步代碼塊來進行同步。容器的啟動從方法內容上來看並不復雜,流程也十分清晰,從方法名上大概就可以猜到每一步做了什么。
2.2 容器啟動流程的不同階段
為了更好的進行講解,可以將容器啟動的整個流程划分為以下五個階段
3 容器啟動前的准備工作
容器啟動前的准備工作定義在下面的方法中
prepareRefresh();
進去一探究竟
/**
* Prepare this context for refreshing, setting its startup date and
* active flag as well as performing any initialization of property sources.
*/
protected void prepareRefresh() {
//記錄容器的啟動時間
this.startupDate = System.currentTimeMillis();
//將容器的關閉標志置位false
this.closed.set(false);
//將容器的啟動標記置位true
this.active.set(true);
if (logger.isInfoEnabled()) {
logger.info("Refreshing " + this);
}
// Initialize any placeholder property sources in the context environment
//空實現的鈎子方法,供子類重寫
initPropertySources();
// Validate that all properties marked as required are resolvable
// see ConfigurablePropertyResolver#setRequiredProperties
//對必須的系統環境變量進行校驗,如果不存在將拋出異常
getEnvironment().validateRequiredProperties();
// Allow for the collection of early ApplicationEvents,
// to be published once the multicaster is available...
this.earlyApplicationEvents = new LinkedHashSet<ApplicationEvent>();
}
首先記錄了容器的啟動時間和對容器的狀態進行了標記。之后來到了容器為用戶提供的第一個擴展點:
initPropertySources();
protected void initPropertySources() {
// For subclasses: do nothing by default.
}
這是一個默認空實現的鈎子方法,用戶在自定義IOC容器時可以重寫,完成一些環境變量屬性的初始化工作。
之后會對一些必要的環境變量信息進行校驗
getEnvironment().validateRequiredProperties();
如果必須的環境變量信息不存在,則會拋出異常
@Override
public void validateRequiredProperties() {
MissingRequiredPropertiesException ex = new MissingRequiredPropertiesException(); //異常信息集合
for (String key : this.requiredProperties) {
if (this.getProperty(key) == null) {
ex.addMissingRequiredProperty(key); //加入異常信息
}
}
if (!ex.getMissingRequiredProperties().isEmpty()) {
throw ex; //拋出異常信息集合
}
}
結合前面的鈎子initPropertySources(),用戶在自定義IOC容器時可以完成一些個性化需求,比如要求容器在啟動時必須從環境變量中加載某屬性值,若該屬性值不存在則啟動失敗。重寫initPropertySources()如下
@Override
protected void initPropertySources() {
getEnvironment().setRequiredProperties("XXXX");
}
若環境變量不存在則會拋出以下異常
總結下容器啟動前的准備工作:主要是對容器狀態進行標記,初始化環境變量信息並對必要的環境變量進行校驗。
4. 總結
這篇文章的主要內容
- 1.講解IOC容器的概念和類結構
- 2.找到容器啟動流程的真正入口refresh()方法,將容器啟動流程划分為了5個階段:啟動前的准備階段,初始化核心容器階段,初始化基礎組件階段,創建單實例bean階段以及容器啟動的收尾階段
- 3.對容器啟動前的准備階段進行了源碼解讀
可以看到容器啟動源碼中對模板方法模式的合理運用。容器啟動的流程以模板方法模式定義在了抽象容器類AbstractApplicationContext中,並留下了鈎子函數供子類重寫。用戶實現自定義容器時,可以通過繼承並重寫鈎子函數的方法對原有容器的功能進行擴展,而無需多做其他改動。這樣既為用戶擴展Spring容器開放了接口,又為用戶屏蔽了容器實現的復雜性,很好的實現了Spring容器通用性和擴展性的統一。