1、Spring Ioc容器實現原理
Spring Ioc容器實現分爲三個步驟:分別是Resource的定位,由ResourceLoader通過輸出Reource接口來完成;BeanDefinition的載入,這個過程是把用戶定義好的bean載入到IOC的內部數據結構表示形式,這個過程就是將你定義的關於bean的信息(例如類名,是否單例,依賴關係,是否懶加載等等信息)存儲到一個POJO的過程;向IOC容器註冊這些BeanDefinition,它的完成是通過BeanDefinitionRegistry來完成的。
IOC容器接口類設計圖如圖1:
圖1、SPRING IOC容器UML圖
從代碼層面上分析Spring IOC容器以BeanFactory和ApplicationContext爲主線演變發展,BeanFactory是一個簡單工廠的設計接口,所有的Bean的產生都是從BeanFactory得來的。所以可以初步看做BeanFacotey是spring IOC容器的一個規範接口類,它提供了一個Bean獲取最基本的操作接口,在它的基礎上擴展有各類BeanFactory的實現,其中ListableBeanFacotry的一個實現爲DefaultListableBeanFacotry就是一個spring容器的基本實現。在這個基本容器的實現基礎上演變出了一個更爲高級的容器接口就是ApplicationContext,從繼承關係可以看出ApplicationContext是可以從雙親獲取bean的一個容器,同時他還擴展了messageResource可以作爲國際化的資源獲取,以及對資源的多方式定位及加載,最後還有一個事件發佈監聽功能。
1.1、Resource的定位
Spring IOC的資源定位方式有很多,但是最終都是通過ResourceLoader來定位的。同時讀取配置資源都是通過BeanDefinitionReader來讀取的,通過一個編程式的方式來大體展示下資源定位的過程。
通過以上編程式闡述,可以對照類比一下,第一步ClassPathResource的獲取過程就是ResourceLoader所負責的工作,而第二步之前已經講到他就是IOC的Bean容器;第三步和第四步代碼就是一個reader去讀取這個資源文件,這個工作就是BeanDefinitionReader去完成的。有了以上大體輪廓影響之後就可以進入代碼對其實現過程進行詳細分析。以FileSystemXmlApplicationContext爲例解析。
首先看下FileSystemXmlApplicationContext的類繼承關係,如圖2所示:
圖2、FileSystemXmlApplicationContext的類繼承關係
接下來看下這個類的構造函數,如下:
public FileSystemXmlApplicationContext(String[]configLocations, boolean refresh, ApplicationContext parent) throwsBeansException {
super(parent);
setConfigLocations(configLocations);
if(refresh) {
refresh();
}
}
首先設置雙親的資源,這個就是繼承了HierarchicalBeanFactory的特性,設置資源的路徑,之後進入IOC容器初始化的主要過程就是refresh().這個refresh過程中關於資源定位部分的時序圖如下圖3所示:
圖3、spring IOC資源定位時序圖
從時序圖可以看出資源的加載和解析都是如編程式方式一樣的步驟。下面可以看下資源定位ResourceLoader是如何工作的?
public Resource getResource(Stringlocation) {
Assert.notNull(location,"Location must not be null");
if(location.startsWith(CLASSPATH_URL_PREFIX)) {
return
newClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()),getClassLoader());
}
else{
try{
//Try to parse the location as a URL...
URLurl = new URL(location);
returnnew UrlResource(url);
}
catch(MalformedURLException ex) {
//No URL -> resolve as resource path.
returngetResourceByPath(location);
}
}
}
代碼對資源定位並解析爲reource過程,首先判斷是否是我們熟悉的classpath:開頭的資源,如果是直接使用ClassPathResource加載完成,如果不是就首先嚐試使用URL建立resource資源,如果解析不正確就調用一個模板方法getResourceByPath(location);這樣就完成了整個資源的定位。接下來就是將一個Resouce讀取出來並進行加載和註冊,這個是後面要講的內容。
1.2、Resource的定位的設計模式
從1.1最後得知getResourceByPath(location);使用的就是一個模板方法,首先了解下模板方法的設計理念:
圖4、模板方法模式的設計類圖
在getResourceByPath中代替AbstractClass的就是DefaultResourceLoader,而分別擴展了它的類ConcreteClassB或A就如下圖所示:
圖5、DefaultResourceLoader的擴展類
這些擴展類分別實現getResourceByPath的具體形式,例如FileSystemXmlApplicationContext所使用到的reourceLoader就是FileSystemResourceLoader,它具體實現的getResourceByPath如下所示:
@Override
protectedResource getResourceByPath(String path) {
if(path != null && path.startsWith("/")) {
path= path.substring(1);
}
returnnew FileSystemContextResource(path);
}
模板方法模式的總結如下:
l 優點
1) 模板方法模式通過把不變的行爲搬移到超類,去除了子類中的重複代碼。
2) 子類實現算法的某些細節,有助於算法的擴展。
3) 通過一個父類調用子類實現的操作,通過子類擴展增加新的行爲,符合“開放-封閉原則”。
l 缺點
1) 每個不同的實現都需要定義一個子類,這會導致類的個數的增加,設計更加抽象。
l 適用場景
1)有相同代碼邏輯的地方抽象出來,並構成一個基本邏輯流程
1.3、Resource的載入
Resource的載入過程首先是拿到reource之後讀入成爲一個document對象,進而採用xml解析並且存入到beandefinition的過程,具體的時序圖如下:
圖6、IOC容器解析加載註冊時序圖
首先得到reource之後需要解析爲Document對象,具體代碼如下:
protected intdoLoadBeanDefinitions(InputSource inputSource, Resource resource)
throwsBeanDefinitionStoreException {
try{
intvalidationMode = getValidationModeForResource(resource);
Documentdoc = this.documentLoader.loadDocument(
inputSource,getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware());
returnregisterBeanDefinitions(doc, resource);
}
而Document是接下來進行資源解析的前提,其解析是委託一個BeanDefinitionParserDelegate來完成的,首先解析的是DefaultElement,這些關鍵字我們在xml定義裏面已經很熟悉:
privatevoid parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if(delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
elseif (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
elseif (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele,delegate);
}
elseif (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
//recurse
doRegisterBeanDefinitions(ele);
}
}
其中importBeanDefinitionResource是觸發對引入資源的重新加載,重要的是processBeanDefinition,從這裏可以看到這個解析是BeanDefinitionParserDelegate完成的。
protected voidprocessBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
BeanDefinitionHolderbdHolder = delegate.parseBeanDefinitionElement(ele);
if(bdHolder != null) {
bdHolder= delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try{
//Register the final decorated instance.
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder,getReaderContext().getRegistry());
}
catch(BeanDefinitionStoreException ex) {
getReaderContext().error("Failedto register bean definition with name '" +
bdHolder.getBeanName()+ "'", ele, ex);
}
//Send registration event.
getReaderContext().fireComponentRegistered(newBeanComponentDefinition(bdHolder));
}
}
Delegate解析得到的是一個BeanDefinitionHolder,這個BeanDefinitionHolder其實是一個BeanDefinition的封裝,其中還包含beanName以及別名alias,對於Element對象被BeanDefinitionParserDelegate解析過程可以詳細查看其實現代碼。解析完成之後調用registerBeanDefinition進行註冊,在BeanDefinition完成註冊之後向監聽者發送註冊完成消息,這是一個訂閱者模式的設計。同時decorateBeanDefinitionIfRequired在處理過程中有裝飾者模式的設計。
1.2、Resource的載入的設計模式
裝飾者模式的設計理念如下:
圖7、裝飾者模式
在這裏裝飾者是BeanDefinitionParserDelegate,而裝飾的對象就是BeanDefinitionHodler,對於BeanDefinitionHodler進行動態數據形式的擴展或刪除。
裝飾者模式總結:
適用性:
1. 需要擴展一個類的功能,或給一個類添加附加職責。
2. 需要動態的給一個對象添加功能,這些功能可以再動態的撤銷。
3. 需要增加由一些基本功能的排列組合而產生的非常大量的功能,從而使繼承關係變的不現實。
4. 當不能採用生成子類的方法進行擴充時。一種情況是,可能有大量獨立的擴展,爲支持每一種組合將產生大量的子類,使得子類數目呈爆炸性增長。另一種情況可能是因爲類定義被隱藏,或類定義不能用於生成子類。
優點:
1. Decorator模式與繼承關係的目的都是要擴展對象的功能,但是Decorator可以提供比繼承更多的靈活性。
2. 通過使用不同的具體裝飾類以及這些裝飾類的排列組合,設計師可以創造出很多不同行爲的組合。
缺點:
1. 這種比繼承更加靈活機動的特性,也同時意味着更加多的複雜性。
2. 裝飾模式會導致設計中出現許多小類,如果過度使用,會使程序變得很複雜。
3.裝飾模式是針對抽象組件(Component)類型編程。但是,如果你要針對具體組件編程時,就應該重新思考你的應用架構,以及裝飾者是否合適。當然也可以改變Component接口,增加新的公開的行爲,實現“半透明”的裝飾者模式。在實際項目中要做出最佳選擇。
觀察者模式設計理念如下:
圖8、觀察者模式
在這段代碼中
getReaderContext().fireComponentRegistered(newBeanComponentDefinition(bdHolder));
Subject是抽象主題,對應的就是ReaderContext,具體主題實現也就是ConcreteSubject就是XmlReaderContext,而對應於抽象主題的方法就是fireComponentRegistered。而抽象觀察者也就是Obsever就是ReaderEventListener,具體的觀察者就是EmptyReaderEventListener,他們接受對應update的方法就是訂閱者就是componentRegistered。而在主題的具體實現類XmlReaderContext中存儲了觀察者具體實現,如下代碼:
public ReaderContext(Resource resource,ProblemReporter problemReporter,
ReaderEventListenereventListener, SourceExtractor sourceExtractor) {
this.resource= resource;
this.problemReporter= problemReporter;
this.eventListener= eventListener;
this.sourceExtractor= sourceExtractor;
}
eventListener在ReaderContext初始化的過程中就已經設置進去,當然這裏的主題他不是一個抽象類或是一個接口,它也是一個具體的class而已。在觀察者的實現中,他們都共同需要實現方法componentRegistered(),完成bean加載完成之後的各種操作。
觀察者模式總結
優點
1)觀察者模式解除了主題和具體觀察者的耦合,讓耦合的雙方都依賴於抽象,而不是依賴具體。從而使得各自的變化都不會影響另一邊的變化。
缺點
1)依賴關係並未完全解除,抽象通知者依舊依賴抽象的觀察者。
適用場景
1)當一個對象的改變需要給變其它對象時,而且它不知道具體有多少個對象有待改變時。
2)一個抽象某型有兩個方面,當其中一個方面依賴於另一個方面,這時用觀察者模式可以將這兩者封裝在獨立的對象中使它們各自獨立地改變和複用。
spring容器初始化就到此結束,下一節我們講解Spring容器依賴注入,敬請關注。