前面學習了 IoC 模式的核心概念,使用場景,以及 Spring 對 IoC
具體實現的兩種系列:BeanFactory 和 ApplicationContext
通過兩種系列的具體 IoC 容器來幫助我們瞭解了兩個不同的特點,以及面向不同的場景。有利有弊,在開發中需要根據具體需求選擇合適的 IoC 具體實現。
其中也通過對 Spring IoC 的具體實現的簡單分析,對 IoC 設計的有了初步的瞭解和想法。那麼現在就來開始瞭解 IoC 容器初始化的過程。
前面在學習 FileSystemXmlApplicationContext 的時候,構造方法中通過此類呼叫了 refresh()
方法。IoC 容器的初始化實際上就是通過這個方法來啟動的,標誌著 IoC 容器正式啟動。
IoC 容器的啟動包括以下三個基本過程:
期間需要注意的是這是一個順序過程,同時指的是 IoC 容器的初始化,而 Bean 的依賴注入的實現,一般不包括其中,但是 BeanDefiniton 有一個 lazyinit
的屬性,使用者可以通過這個屬性改變 Bean 的依賴注入過程,eg:一般情況下 Bean 的注入需要在容器初始化之後,第一次呼叫 getBean()
時才會觸發,而通過 lazyinit 屬性可以讓 Bean 在 IoC 容器初始化時就預先完成了依賴注入。
根據前面的學習,我們這一過程的表層應該不難知曉,就是通過定義一個 Resuorce 去定位容器使用的 BeanDefinition。eg:ClassPathResource()
這個類就是在專案中的類路徑中尋找以檔案形式存在的 BeanDefinition。
應該注意的是,不能把 Resource 的定位 BeanDefinition 資源和 BD 的載入弄混淆了。只是定位資源而已,此時 IoC 容器還不能直接使用這些資訊,這些資訊是交由 BeanDefinitionReader
來對這些資訊進行 BD 的載入處理。
相對於 DefaultListableBeanFactory
容器需要手動設定好特定的 Resource 讀取器,ApplicationContext
容器就準備好了一系列的讀取器。
但是使用 DefaultListableBeanFactory
這種底層容器可以根據業務客製化 IoC 容器的靈活性,有利有弊。
還是通過 FileSystemXmlApplicationContext
這一具體容器來分析是如何完成 Resource 的定位過程。
繼承體系:
主要兩個功能的原始碼:
上圖表明瞭 getResourceByPath()
是實現 Resource 定位的方法。但是並不是使用者呼叫的,檢視該方法的呼叫鏈:
上圖示注了該方法最初是由 refresh()
方法觸發的,而 refresh()
是在構造器中呼叫的。
我們需要通過這個方法來了解過程。
構造器呼叫的是超類 AbstractApplicationContext
中的 refresh()
,檢視原始碼:
public void refresh() throws BeansException, IllegalStateException {
synchronized(this.startupShutdownMonitor) {
this.prepareRefresh();
// 建立beanFactory以及掃描bean資訊(beanDefinition),並通過BeanDefinitionRegistry 註冊到容器中。
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
// 省略...
}
}
obtainFreshBeanFactory()
原始碼:
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
this.refreshBeanFactory();
// 省略...
}
refreshBeanFactory()
是一個抽象方法,有兩個具體的實現類:
在這裡我們的 FSXAC 繼承了 AbstractRefreshableApplicationContext
,所以我們看在這個類中 refreshBeanFactory()
的實現:
protected final void refreshBeanFactory() throws BeansException {
//...
try {
DefaultListableBeanFactory beanFactory = this.createBeanFactory(); // 1
beanFactory.setSerializationId(this.getId());
this.customizeBeanFactory(beanFactory);
this.loadBeanDefinitions(beanFactory);// 2
///...
}
}
我們抽出這兩行程式碼進行分析:
我們到目前位置並沒有看到與之相關的 Resource 定位資訊,只看到 BD 的載入啟動,所以針對 loadBeanDefinitions()
進行進一步分析。該方法呼叫的是本類的一個抽象方法loadBeanDefinitions(DefaultListableBeanFactory var1)
,此方法是模板方法,由子類具體實現:
而 FSXAC 就是 AbstractXmlApplicationContext
的子類,所以進而分析這個類的具體實現。
可以看到:
建立了 XmlBeanDefinitionReader 類,用於將 XML 檔案中的 Bean 讀取出來並載入。
呼叫 XBDR 的 loadBeanDefinitions,開始啟動 BeanDefinition 的載入。
在具體的實現中,分別傳入不同的引數,但是在此方法中走判斷時,呼叫了 this.getConfigResources()
這個方法在此類中是返回的 Resource[]
是 null,所以走第二個判斷,獲取以字串陣列,因為之前在 FSXAC 中就設定好了。
將 String[]
傳入呼叫的是 XmlBeanDefinitionReader
的基礎類別 AbstractBeanDefinitionReader
的方法:
這裡就是將 String 陣列中的字串,一個一個傳入呼叫本類過載方法,並且對其進行計數。
public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException {
return this.loadBeanDefinitions(location, (Set)null);
}
public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {
// 取得 ResourceLoader,使用的是 DeaultResourceLoader
ResourceLoader resourceLoader = this.getResourceLoader(); // 關鍵程式碼1
if (resourceLoader == null) {
//...略
} else {
int loadCount;
// 呼叫 DefaultResourceLoader 的 getResource 完成具體的 Resource 定位
if (!(resourceLoader instanceof ResourcePatternResolver)) { // 關鍵程式碼2
Resource resource = resourceLoader.getResource(location);
loadCount = this.loadBeanDefinitions((Resource)resource);
//... 略
} else {
// 呼叫 DefaultResourceLoader 的 getResources 完成具體的 Resource 定位
try {
Resource[] resources = ((ResourcePatternResolver)resourceLoader).getResources(location);
loadCount = this.loadBeanDefinitions(resources);
//... 略
}
//... 略
}
}
}
可以看到最終呼叫的是兩個引數的方法:(String location, Set<Resource> actualResources)
,通過上面程式碼的簡要分析,我們提取出兩個重要的資訊:
首先第一個,Spring 將資源的定義和載入區分開來,這裡需要注意的是資源的載入也就是 Resource 的載入,而不是 BeanDefinition 的載入。Resource 定義了統一的資源(抽象並統一各種資源來源),ResourceLoader 定義了這些資源的統一載入。所以 BeanDefinition 資源的定位過程應該是:將不同 BD 資源獲取途徑經過 Spring 統一封裝為 Resource,再由 ResourceLoader 進行資源載入,獲取這些 Resource,給 BeanDefinition 的載入做準備。
而在這個 FSXAC 的例子中,這個 ResourceLoader 就是 DefaultResourceLoader,來看看是怎麼具體實現 getResource()
.
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
Iterator var2 = this.protocolResolvers.iterator();
Resource resource;
do {
if (!var2.hasNext()) {
if (location.startsWith("/")) {
// 處理以 / 標識的 Resource 定位
return this.getResourceByPath(location);
}
// 處理帶有 classpath 表示的 Resource
if (location.startsWith("classpath:")) {
return new ClassPathResource(location.substring("classpath:".length()), this.getClassLoader());
}
try {
// 處理 URL 表示的 Resource 定位
URL url = new URL(location);
return new UrlResource(url);
} catch (MalformedURLException var5) {
// 處理既不是 classpath 也不是 URL 標識的 Resource 定位
// 則將 getResource 的責任交給 getResourceByPath(),這個方法時 protected,預設實現是得到一個 ClassPathContextResource 物件,通常會由子類實現該方法。
return this.getResourceByPath(location);
}
}
ProtocolResolver protocolResolver = (ProtocolResolver)var2.next();
resource = protocolResolver.resolve(location, this);
} while(resource == null);
return resource;
}
通過上述分析,找到了熟悉的方法名: protected Resource getResourceByPath(String path){}
這個方法由子類 FSXAC 實現,這個方法返回的是:FileSystemResource
物件,通過這個物件,Spring 就可以進行相關的 I/O 操作,完成 BeanDefinition 定位。
實際上這麼多過程和細節,都是為了實現一個功能,對 path 進行解析,然後生成一個 FileSystemResource 物件,並返回,給 BeanDefinition 載入過程做準備。
實際上 Spring 針對不同型別的 Resource 都準備了對應的實現類,方便我們針對不同場景進行合適的使用,不同的 ApplicationContext 會對應生成其他的 Resource:ClassPathResource、ServletContextResource 等,而且 Resource 介面本身就是繼承了 InputStreamSource (這個抽象類唯一的方法是返回一個 InputStream)
,定義了很多的 I/O 相關的操作,其實現類也主要是針對不同的資源型別做出合適的實現。
通過 FileSystemXmlApplicationContext
這個 AC 實現原理為例子,初步的瞭解了 Resource 定位的解決方案,就是通過呼叫 getResourceByPath()
方法,重寫了父類別 DefaultResourceLoader
的方法,最後得到了 FileSystemResource
這個型別的 Resource 的定位實現。那麼此時這個 Resource 的定位過程已經完成,為 BeanDefinition 的載入創造了 I/O 操作的條件,但是具體的資料還沒開始讀入。讀入就是 BeanDefinition 的載入和解析過程了。
其實 Resource 就是統一了資源的定義,各種 BeanDefinition 定義的資源(File,URL,XML...)都統一抽象成 Resource,所有實現類都需要實現相關的 I/O 操作。
而 ResourceLoader 就是根據某種匹配方式來建立匹配的 Resource,並返回。
將其過程多捋幾遍,初步理解其 BeanDefinition 的資源定位過程。下一步就是 BeanDefinition 的載入和解析過程。