1. 程式人生 > >《Spring原始碼深度解析》學習筆記——Spring的整體架構與容器的基本實現

《Spring原始碼深度解析》學習筆記——Spring的整體架構與容器的基本實現

Spring的整體架構

Spring框架是一個分層架構,它包含一系列的功能要素,並被分為大約20個模組,如下圖所示

<img s_001>

這些模組被總結為以下幾個部分:

  • Core Container

    Core Container(核心容器)包含有Core、Beans、Context和Expression Language模組
    Core和Beans模組是框架的基礎部分,提供IoC(轉控制)和依賴注入特性。這裡的基礎概念是BeanFactory,它提供對Factory模式的經典實現來消除對程式性單例模式的需要,並真正地允許你從程式邏輯中分離出依賴關係和配置

    • Core模組主要包含Spring框架基本的核心工具類
    • Beans模組是所有應用都要用到的,它包含訪問配置檔案、建立和管理bean以及進行Inversion of Control/Dependency Injection(Ioc/DI)操作相關的所有類
    • Context模組構建於Core和Beans模組基礎之上,提供了一種類似於JNDI註冊器的框架式的物件訪問方法。Context模組繼承了Beans的特性,為Spring核心提供了大量擴充套件,添加了對國際化(如資源繫結)、事件傳播、資源載入和對Context的透明建立的支援。ApplicationContext介面是Context模組的關鍵
    • Expression Language模組提供了一個強大的表示式語言用於在執行時查詢和操縱物件,該語言支援設定/獲取屬性的值,屬性的分配,方法的呼叫,訪問陣列上下文、容器和索引器、邏輯和算術運算子、命名變數以及從Spring的IoC容器中根據名稱檢索物件
  • Data Access/Integration

    • JDBC模組提供了一個JDBC抽象層,它可以消除冗長的JDBC編碼和解析資料庫廠商特有的錯誤程式碼,這個模組包含了Spring對JDBC資料訪問進行封裝的所有類
    • ORM模組為流行的物件-關係對映API,如JPA、JDO、Hibernate、iBatis等,提供了一個互動層,利用ORM封裝包,可以混合使用所有Spring提供的特性進行O/R對映,如前邊提到的簡單宣告性事務管理
  • Web

    Web上下文模組建立在應用程式上下文模組之上,為基於Web的應用程式提供了上下文,所以Spring框架支援與Jakarta Struts的整合。Web模組還簡化了處理多部分請求以及將請求引數繫結到域物件的工作。Web層包含了Web、Web-Servlet、Web-Struts和Web、Porlet模組

    • Web模組:提供了基礎的面向Web的整合特性,例如,多檔案上傳、使用Servlet listeners初始化IoC容器以及一個面向Web的應用上下文,它還包含了Spring遠端支援中Web的相關部分
    • Web-Servlet模組web.servlet.jar:該模組包含Spring的model-view-controller(MVC)實現,Spring的MVC框架使得模型範圍內的程式碼和web forms之間能夠清楚地分離開來,並與Spring框架的其他特性基礎在一起
    • Web-Struts模組:該模組提供了對Struts的支援,使得類在Spring應用中能夠與一個典型的Struts Web層整合在一起
    • Web-Porlet模組:提供了用於Portlet環境和Web-Servlet模組的MVC的實現
  • AOP

    AOP模組提供了一個符合AOP聯盟標準的面向切面程式設計的實現,它讓你可以定義例如方法攔截器和切點,從而將邏輯程式碼分開,降低它們之間的耦合性,利用source-level的元資料功能,還可以將各種行為資訊合併到你的程式碼中

    Spring AOP模組為基於Spring的應用程式中的物件提供了事務管理服務,通過使用Spring AOP,不用依賴EJB元件,就可以將宣告性事務管理整合到應用程式中

  • Test

    Test模組支援使用Junit和TestNG對Spring元件進行測試

容器的基本實現

Spring的結構組成

beans包的層級結構

beans包中的各個原始碼包的功能如下

  • src/main/java 用於展現Spring的主要邏輯
  • src/main/resources 用於存放系統的配置檔案
  • src/test/java 用於對主要邏輯進行單元測試
  • src/test/resources 用於存放測試用的配置檔案

核心類介紹

1.DefaultListableBeanFactory

XmlBeanFactory繼承自DefaultListableBeanFactory,而DefaultListableBeanFactory是整個bean載入的核心部分,是Spring註冊及載入bean的預設實現,而對於XmlBeanFactory與DefaultListableBeanFactory不同的地方其實是在XmlBeanFactory中使用了自定義的XML讀取器XmlBeanDefinitionReader,實現了個性化的BeanDefinitionReader讀取,DefaultListableBeanFactory繼承了AbstractAutowireCapableBeanFactory並實現了ConfigURableListableBeanFactory以及BeanDefinitionRegistry介面。以下是ConfigURationListableBeanFactory的層次結構圖以下相關類圖

<img s_0002>

容器載入相關類圖

<img s_0003>

類圖中各個類的作用:

  • AliasRegistry:定義對alias的簡單增刪改等操作
  • SimpleAliasRegistry:主要使用map作為alias的快取,並對介面AliasRegistry進行實現
  • SingletonBeanRegistry:定義對單例的註冊及獲取
  • BeanFactory:定義獲取bean及bean的各種屬性
  • DefaultSingletonBeanRegistry:對介面SingletonBeanRegistry各函式的實現
  • HierarchicalBeanFactory:繼承BeanFactory,也就是在BeanFactory定義的功能的基礎上增加了對parentFactory的支援
  • BeanDefinitionRegistry:定義對BeanDefinition的各種增刪改操作
  • FactoryBeanRegistrySupport:在DefaultSingletonBeanRegistry基礎上增加了對FactoryBean的特殊處理功能
  • ConfigurableBeanFactory:提供配置Factory的各種方法
  • ListableBeanFactory:根據各種條件獲取bean的配置清單
  • AbstractBeanFactory:綜合FactoryBeanRegistrySupport和ConfigurationBeanFactory的功能
  • AutowireCapableBeanFactory:提供建立bean、自動注入、初始化以及應用bean的後處理器
  • AbstractAutowireCapableBeanFactory:綜合AbstractBeanFactory並對介面AutowireCapableBeanFactory進行實現
  • ConfigurableListableBeanFactory:BeanFactory配置清單,指定忽略型別及介面等
  • DefaultListableBeanFactory:綜合上面所有功能,主要是對Bean註冊後的處理

XmlBeanFactory對DefaultListableBeanFactory類進行了擴充套件,主要用於從XML文件中讀取BeanDefinition,對於註冊及獲取Bean都是使用從父類DefaultListableBeanFactory繼承的方法去實現,而唯獨與父類不同的個性化實現就是增加了XmlBeanDefinitionReader型別的reader屬性。在XmlBeanFactory中主要使用reader屬性對資原始檔進行讀取和註冊

2.XmlBeanDefinitionReader

XML配置檔案的讀取是Spring中重要的功能,因為Spring的大部分功能都是以配置作為切入點的,可以從XmlBeanDefinitionReader中梳理一下資原始檔讀取、解析及註冊的大致脈絡,首先看看各個類的功能

  • ResourceLoader:定義資源載入器,主要應用於根據給定的資原始檔地址返回對應的Resource
  • BeanDefinitionReader:主要定義資原始檔讀取並轉換為BeanDefinition的各個功能
  • EnvironmentCapable:定義獲取Environment方法
  • DocumentLoader:定義從資原始檔載入到轉換為Document的功能
  • AbstractBeanDefinitionReader:對EnvironmentCapable、BeanDefinitionReader類定義的功能進行實現
  • BeanDefinitionDocumentReader:定義讀取Document並註冊BeanDefinition功能
  • BeanDefinitionParserDelegate:定義解析Element的各種方法

整個XML配置檔案讀取的大致流程,在XmlBeanDefinitionReader中主要包含以下幾步處理

<img s_0004>

(1)通過繼承自AbstractBeanDefinitionReader中的方法,來使用ResourceLoader將資原始檔路徑轉換為對應的Resource檔案

(2)通過DocumentLoader對Resource檔案進行轉換,將Resource檔案轉換為Document檔案

(3)通過實現介面BeanDefinitionDocumentReader的DefaultBeanDefinitionDocumentReader類對Document進行解析,並使用BeanDefinitionParserDelegate對Element進行解析

容器的基礎XmlBeanFactory

BeanFactory bf = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml"));
  • 1

通過XmlBeanFactory初始化時序圖看一看上面程式碼的執行邏輯

<img s_0005>

時序圖從BeanFactoryTest測試類開始,首先呼叫ClassPathResource的建構函式來構造Resource資原始檔的例項物件,這樣後續的資源處理就可以用Resource提供的各種服務來操作了

配置檔案封裝

Spring的配置檔案讀取是通過ClassPathResource進行封裝的,Spring對其內部使用到的資源實現了自己的抽象結構:Resource介面來封裝底層資源

public interface InputStreamSource {
    InputStream getInputStream() throws IOException;
}
public interface Resource extends InputStreamSource {
    boolean exists();

    boolean isReadable();

    boolean isOpen();

    URL getURL() throws IOException;

    URI getURI() throws IOException;

    File getFile() throws IOException;

    long lastModified() throws IOException;

    Resource createRelative(String var1) throws IOException;

    String getFilename();

    String getDescription();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

InputStreamSource封裝任何能返回InputStream的類,比如File、Classpath下的資源和Byte Array等, 它只有一個方法定義:getInputStream(),該方法返回一個新的InputStream物件

Resource介面抽象了所有Spring內部使用到的底層資源:File、URL、Classpath等。首先,它定義了3個判斷當前資源狀態的方法:存在性(exists)、可讀性(isReadable)、是否處於開啟狀態(isOpen)。另外,Resource介面還提供了不同資源到URL、URI、File型別的轉換,以及獲取lastModified屬性、檔名(不帶路徑資訊的檔名,getFilename())的方法,為了便於操作,Resource還提供了基於當前資源建立一個相對資源的方法:createRelative(),還提供了getDescription()方法用於在錯誤處理中的列印資訊

對不同來源的資原始檔都有相應的Resource實現:檔案(FileSystemResource)、Classpath資源(ClassPathResource)、URL資源(UrlResource)、InputStream資源(InputStreamResource)、Byte陣列(ByteArrayResource)等,相關類圖如下所示

<img s_0006>

當通過Resource相關類完成了對配置檔案進行封裝後,配置檔案的讀取工作就全權交給XmlBeanDefinitionReader來處理了

XmlBeanFactory的初始化有若干辦法,Spring提供了很多的建構函式,在這裡分析的是使用Resource例項作為建構函式引數的辦法,程式碼如下

public XmlBeanFactory(Resource resource) throws BeansException {
    //呼叫XmlBeanFactory(Resource,BeanFactory)構造方法
    this(resource, (BeanFactory)null);
}

//parentBeanFactory為父類BeanFactory用於factory合併,可以為空
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
    super(parentBeanFactory);
    this.reader = new XmlBeanDefinitionReader(this);
    this.reader.loadBeanDefinitions(resource);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

上面函式中的程式碼this.reader.loadBeanDefinitions(resource)才是資源載入的真正實現,時序圖中提到的XmlBeanDefinitionReader載入資料就是在這裡完成的,但是在XmlBeanDefinitionReader載入資料前還有一個呼叫父類建構函式初始化的過程:super(parentBeanFactory),跟蹤程式碼到父類AbstractAutowireCapableBeanFactory的建構函式中:

public AbstractAutowireCapableBeanFactory() {
    super();
    this.ignoreDependencyInterface(BeanNameAware.class);
    this.ignoreDependencyInterface(BeanFactoryAware.class);
    this.ignoreDependencyInterface(BeanClassLoaderAware.class);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

ignoreDependencyInterface的主要功能是忽略給定介面的自動裝配功能,目的是:實現了BeanNameAware介面的屬性,不會被Spring自動初始化。自動裝配時忽略給定的依賴介面,典型應用是通過其他方式解析Application上下文註冊依賴,類似於BeanFactory通過BeanFactoryAware進行注入或者ApplicationContext通過ApplicationContextAware進行注入

載入Bean

在XmlBeanFactory建構函式中呼叫了XmlBeanDefinitionReader型別的reader屬性提供的方法this.reader.loadBeanDefinitions(resource),而這句程式碼則是整個資源載入的切入點,這個方法的時序圖如下

<img s_0007>

(1)封裝資原始檔。當進入XmlBeanDefinitionReader後首先對引數Resource使用EncodedResource類進行封裝
(2)獲取輸入流。從Resource中獲取對應的InputStream並構造InputSource
(3)通過構造的InputSource例項和Resource例項繼續呼叫函式doLoadBeanDefinitions,loadBeanDefinitions函式具體的實現過程:

public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
    return this.loadBeanDefinitions(new EncodedResource(resource));
}
  • 1
  • 2
  • 3

EncodedResource的作用是對資原始檔的編碼進行處理的,其中的主要邏輯體現在getReader()方法中,當設定了編碼屬性的時候Spring會使用相應的編碼作為輸入流的編碼

public Reader getReader() throws IOException {
    return this.encoding != null?new InputStreamReader(this.resource.getInputStream(), this.encoding):new InputStreamReader(this.resource.getInputStream());
}
  • 1
  • 2
  • 3

在構造好了encodeResource物件後,再次轉入了可複用方法loadBeanDefinitions(new EncodedResource(resource)),這個方法內部才是真正的資料準備階段

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
    Assert.notNull(encodedResource, "EncodedResource must not be null");
    if(this.logger.isInfoEnabled()) {
        this.logger.info("Loading XML bean definitions from " + encodedResource.getResource());
    }

    //通過屬性來記錄已經載入的資源
    Object currentResources = (Set)this.resourcesCurrentlyBeingLoaded.get();
    if(currentResources == null) {
        currentResources = new HashSet(4);
        this.resourcesCurrentlyBeingLoaded.set(currentResources);
    }

    if(!((Set)currentResources).add(encodedResource)) {
        throw new BeanDefinitionStoreException("Detected cyclic loading of " + encodedResource + " - check your import definitions!");
    } else {
        int var6;
        try {
            //從encodedResource中獲取已經封裝的Resource物件並再次從Resource中獲取其中的InputStream
            InputStream ex = encodedResource.getResource().getInputStream();

            try {
                InputSource inputSource = new InputSource(ex);
                if(encodedResource.getEncoding() != null) {
                    inputSource.setEncoding(encodedResource.getEncoding());
                }

                //真正進入了邏輯核心部分
                var6 = this.doLoadBeanDefinitions(inputSource, encodedResource.getResource());
            } finally {
                ex.close();
            }
        } catch (IOException var15) {
            throw new BeanDefinitionStoreException("IOException parsing XML document from " + encodedResource.getResource(), var15);
        } finally {
            ((Set)currentResources).remove(encodedResource);
            if(((Set)currentResources).isEmpty()) {
                this.resourcesCurrentlyBeingLoaded.set((Object)null);
            }

        }

        return var6;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

資料準備階段的邏輯:首先對傳入的resource引數做封裝,目的是考慮到Resource可能存在編碼要求的情況,其次,通過SAX讀取XML檔案的方式來準備InputSource物件,最後將準備的資料通過引數傳入真正的核心處理部分doLoadBeanDefinitions(inputSource,encodedResource.getResource())

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException {
    try {
        int ex = this.getValidationModeForResource(resource);
        Document doc = this.documentLoader.loadDocument(inputSource, this.getEntityResolver(), this.errorHandler, ex, this.isNamespaceAware());
        return this.registerBeanDefinitions(doc, resource);
    } catch (BeanDefinitionStoreException var5) {
        throw var5;
    } catch (SAXParseException var6) {
        throw new XmlBeanDefinitionStoreException(resource.getDescription(), "Line " + var6.getLineNumber() + " in XML document from " + resource + " is invalid", var6);
    } catch (SAXException var7) {
        throw new XmlBeanDefinitionStoreException(resource.getDescription(), "XML document from " + resource + " is invalid", var7);
    } catch (ParserConfigurationException var8) {
        throw new BeanDefinitionStoreException(resource.getDescription(), "Parser configuration exception parsing XML from " + resource, var8);
    } catch (IOException var9) {
        throw new BeanDefinitionStoreException(resource.getDescription(), "IOException parsing XML document from " + resource, var9);
    } catch (Throwable var10) {
        throw new BeanDefinitionStoreException(resource.getDescription(), "Unexpected exception parsing XML document from " + resource, var10);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在上面冗長的程式碼中假如不考慮異常類程式碼,其實只做了三件事

  • 獲取對XML檔案的驗證模式
  • 載入XML檔案,並得到對應的Document
  • 根據返回的Document註冊Bean資訊

獲取XML的驗證模式

XML檔案的驗證模式保證了XML檔案的正確性,而比較常用的驗證模式有兩種:DTD和XSD

DTD和XSD區別

DTD(Document Type Definition)即文件型別定義,是一種XML約束模式語言,是XML檔案的驗證機制,屬於XML檔案組成的一部分。DTD是一種保證XML文件格式正確的有效方法,可以通過比較XML文件和DTD檔案來看文件是否符合規範,元素和標籤使用是否正確。一個DTD文件包含:元素的定義規則,元素間關係的定義規則,元素可使用的屬性,可使用的實體或符合規則

使用DTD驗證模式的時候需要在XML檔案的頭部宣告,以下是在Spring中使用DTD宣告方式的程式碼:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//Spring//DTD BEAN 2.0//EN" "http://www.Springframework.org/dtd/Spring-beans-2.0.dtd">
...
  • 1
  • 2
  • 3

而以Spring為例,具體的Spring-beans-2.0.dtd部分如下:

<!ELEMENT beans (
    description?,
    (import | alias | bean)*
)>
<!ATTLIST beans default-lazy-init (true | false) "false">
<!ATTLIST beans default-merge (true | false) "false">
<!ATTLIST beans default-autowire (no | byName | byType | constructor | autodetect) "no">
<!ATTLIST beans default-dependency-check (none | objects | simple | all) "none">
<!ATTLIST beans default-init-method CDATA #IMPLIED>
<!ATTLIST beans default-destroy-method CDATA #IMPLIED>
.....
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

XML Schema語言就是XSD(XML Schemas Definition)。XML Schema描述了XML文件的結構,可以用一個指定的XML Schema來驗證某個XML文件,以檢查該XML文件是否符合其要求,文件設計者可以通過XML Schema指定一個XML文件所允許的結構和內容,並可據此檢查一個XML文件是否是有效的

在使用XML Schema文件對XML例項文件進行檢驗,除了要宣告名稱空間外(xmlns=http://www.Springframework.org/schema/beans),還必須指定該名稱空間所對應的XML Schema文件的儲存位置,通過schemaLocation屬性來指定名稱空間所對應的XML Schema文件的儲存位置,它包含兩個部分,一部分是名稱空間的URI,另一部分就該名稱空間所標識的XML Schema檔案位置或URL地址(xsi:schemaLocation=”http://www.Springframework.org/schema/beans http://www.Springframework.org/schema/beans/Spring-beans.xsd“)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.Springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema/beans"
    xsi:schemaLocation="http://www.Springframework.org/schema/beans http://www.Springframework.org/schema/beans/Spring-beans.xsd">
....
</beans>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Spring-beans-3.0.xsd部分程式碼如下:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.springframework.org/schema/beans">

    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>

    <xsd:annotation>
        <xsd:documentation><![CDATA[
        ....
        ]]></xsd:documentation>
    </xsd:annotation>

    <!-- base types -->
    <xsd:complexType name="identifiedType" abstract="true">
        <xsd:annotation>
            <xsd:documentation><![CDATA[
    The unique identifier for a bean. The scope of the identifier
    is the enclosing bean factory.
            ]]></xsd:documentation>
        </xsd:annotation>
        <xsd:attribute name="id" type="xsd:ID">
            <xsd:annotation>
                <xsd:documentation><![CDATA[
    The unique identifier for a bean.
                ]]></xsd:documentation>
            </xsd:annotation>
        </xsd:attribute>
    </xsd:complexType>
    ......
</xsd:schema>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

驗證模式的讀取

Spring通過getValidationModeForResource方法來獲取對應資源的驗證模式

protected int getValidationModeForResource(Resource resource) {
    int validationModeToUse = this.getValidationMode();
    //如果手動指定了驗證模式則使用指定的驗證模式
    if(validationModeToUse != 1) {
        return validationModeToUse;
    } else {
        //如果未指定則使用自動檢測
        int detectedMode = this.detectValidationMode(resource);
        return detectedMode != 1?detectedMode:3;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

自動檢測驗證模式的功能是在函式detectValidationMode方法中實現的,在detectValidationMode函式中又將自動檢測驗證模式的工作委託給了專門處理類XmlValidationModeDetector的detectValidationMode方法

protected int detectValidationMode(Resource resource) {
    if(resource.isOpen()) {
        throw new BeanDefinitionStoreException("Passed-in Resource [" + resource + "] contains an open stream: " + "cannot determine validation mode automatically. Either pass in a Resource " + "that is able to create fresh streams, or explicitly specify the validationMode " + "on your XmlBeanDefinitionReader instance.");
    } else {
        InputStream inputStream;
        try {
            inputStream = resource.getInputStream();
        } catch (IOException var5) {
            throw new BeanDefinitionStoreException("Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " + "Did you attempt to load directly from a SAX InputSource without specifying the " + "validationMode on your XmlBeanDefinitionReader instance?", var5);
        }

        try {
            return this.validationModeDetector.detectValidationMode(inputStream);
        } catch (IOException var4) {
            throw new BeanDefinitionStoreException("Unable to determine validation mode for [" + resource + "]: an error occurred whilst reading from the InputStream.", var4);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

XmlValidationModeDetector類中的detectValidationMode方法如下

public int detectValidationMode(InputStream inputStream) throws IOException {
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

    try {
        boolean isDtdValidated = false;

        while(true) {
            String content;
            if((content = reader.readLine()) != null) {
                content = this.consumeCommentTokens(content);
                //如果讀取的行是空或者註釋則略過
                if(this.inComment || !StringUtils.hasText(content)) {
                    continue;
                }

                if(this.hasDoctype(content)) {
                    isDtdValidated = true;
                } else if(!this.hasOpeningTag(content)) {//讀取到<開始符號,驗證模式一定會會在開始符號之前
                    continue;
                }
            }

            int var6 = isDtdValidated?2:3;
            return var6;
        }
    } catch (CharConversionException var9) {
        ;
    } finally {
        reader.close();
    }

    return 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

Spring用來檢測驗證模式的辦法就是判斷是否包含DOCTYPE,如果包含就是DTD,否則就是XSD

獲取Document

經過了驗證模式準備的步驟就可以進行Document載入了,對於文件的讀取委託給了DocumentLoader去執行,這裡的DocumentLoader是個介面,而真正呼叫的是DefaultDocumentLoader,解析程式碼如下

public Document loadDocument(InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
    DocumentBuilderFactory factory = this.createDocumentBuilderFactory(validationMode, namespaceAware);
    if(logger.isDebugEnabled()) {
        logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
    }

    DocumentBuilder builder = this.createDocumentBuilder(factory, entityResolver, errorHandler);
    return builder.parse(inputSource);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

首選建立DocumentBuildFactory,再通過DocumentBuilderFactory建立DocumentBuilder,進而解析InputSource來返回Document物件。對於引數entityResolver,傳入的是通過getEntityResolver()函式獲取的返回值,程式碼如下

protected EntityResolver getEntityResolver() {
    if(this.entityResolver == null) {
        ResourceLoader resourceLoader = this.getResourceLoader();
        if(resourceLoader != null) {
            this.entityResolver = new ResourceEntityResolver(resourceLoader);
        } else {
            this.entityResolver = new DelegatingEntityResolver(this.getBeanClassLoader());
        }
    }

    return this.entityResolver;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

EntityResolver用法

如果SAX應用程式需要實現自定義處理外部實體,則必須實現此介面並使用setEntityResolver方法向SAX驅動器註冊一個例項。也就是說,對於解析一個XML,SAX首先讀取該XML文件上的宣告,根據宣告去尋找相應的DTD定義,以便對文件進行一個驗證,預設的尋找規則,即通過網路(實現上就是宣告DTD的URI地址)來下載相應的DTD宣告,並進行認證。下載的過程是一個漫長的過程,而且當網路中斷或不可用時,這裡會報錯,就是因為相應的DTD宣告沒有被找到的原因

EntityResolver的作用是專案本身就可以提供一個如何尋找DTD宣告的方法,即由程式來實現尋找DTD宣告的過程,比如將DTD檔案放到專案中某處,在實現時直接將此文件讀取並返回給SAX即可,entityResolver的介面方法宣告:

InputSource resolveEntity(String publicId, String systemId)
  • 1

這裡,它接收兩個引數publicId和systemId,並返回一個InputSource物件,以特定配置檔案來進行講解

(1)如果在解析驗證模式為XSD的配置檔案,程式碼如下

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.Springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.Springframework.org/schema/beans http://www.Springframework.org/schema/beans/Spring-beans.xsd">
....
</beans>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

讀取到以下兩個引數

(2)如果解析驗證模式為DTD的配置檔案,程式碼如下:

    <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//Spring//DTD BEAN 2.0//EN" "http://www.Springframework.org/dtd/Spring-beans-2.0.dtd">
....
</beans>
  • 1
  • 2
  • 3
  • 4

讀取到以下兩個引數

一般都會把驗證檔案放置在自己的工程裡,如果把URL轉換為自己工程裡對應的地址檔案呢?以載入DTD檔案為例來看看Spring是如何實現的。根據之前Spring中通過getEntityResolver()方法對EntityResolver的獲取,我們知道,Spring中使用DelegatingEntityResolver類為EntityResolver的實現類,resolveEntity實現方法如下:

//DelegatingEntityResolver.java

    public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
    if(systemId != null) {
        if(systemId.endsWith(".dtd")) {
            //dtd從這裡解析
            return this.dtdResolver.resolveEntity(publicId, systemId);
        }

        if(systemId.endsWith(".xsd")) {
            //通過呼叫META-INF/Spring.schemas解析
            return this.schemaResolver.resolveEntity(publicId, systemId);
        }
    }

    return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

對不同的驗證模式,Spring使用了不同的解析器解析,比如載入DTD型別的BeansDtdResolver的resolveEntity是直接擷取systemId最後的xx.dtd然後去當前路徑下尋找,而載入XSD型別的PluggableSchemaResolver類的resolveEntity是預設到META-INF/Spring.schemas檔案中找到systemId所對應的XSD檔案並載入

//BeansDtdResolver.java

    public InputSource resolveEntity(String publicId, String systemId) throws IOException {
    if(logger.isTraceEnabled()) {
        logger.trace("Trying to resolve XML entity with public ID [" + publicId + "] and system ID [" + systemId + "]");
    }

    if(systemId != null && systemId.endsWith(".dtd")) {
        int lastPathSeparator = systemId.lastIndexOf("/");
        String[] var7 = DTD_NAMES;
        int var6 = DTD_NAMES.length;

        for(int var5 = 0; var5 < var6; ++var5) {
            String DTD_NAME = var7[var5];
            // DTD_NAMES = {"Spring-beans-2.0", "Spring-beans"}
            int dtdNameStart = systemId.indexOf(DTD_NAME);
            if(dtdNameStart > lastPathSeparator) {
                String dtdFile = systemId.substring(dtdNameStart);
                if(logger.isTraceEnabled()) {
                    logger.trace("Trying to locate [" + dtdFile + "] in Spring jar");
                }

                try {
                    ClassPathResource ex = new ClassPathResource(dtdFile, this.getClass());
                    InputSource source = new InputSource(ex.getInputStream());
                    source.setPublicId(publicId);
                    source.setSystemId(systemId);
                    if(logger.isDebugEnabled()) {
                        logger.debug("Found beans DTD [" + systemId + "] in classpath: " + dtdFile);
                    }

                    return source;
                } catch (IOException var12) {
                    if(logger.isDebugEnabled()) {
                        logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in class path", var12);
                    }
                }
            }
        }
    }

    return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

解析並註冊BeanDefinition

當把檔案轉換成Document後,接下來就是對bean的提取及註冊,當程式已經擁有了XML文件檔案的Document例項物件時,就會被引入下面這個方法

//XmlBeanDefinitionReader.java

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
    //使用DefaultBeanDefinitionDocumentReader例項化BeanDefinitionDocumentReader
    BeanDefinitionDocumentReader documentReader = this.createBeanDefinitionDocumentReader();
    //記錄統計之前BeanDefinition的載入個數
    int countBefore = this.getRegistry().getBeanDefinitionCount();
    //載入及註冊bean
    documentReader.registerBeanDefinitions(doc, this.createReaderContext(resource));
    //記錄本次載入的BeanDefinition個數
    return this.getRegistry().getBeanDefinitionCount() - countBefore;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

BeanDefinitionDocumentReader是一個介面,而例項化的工作是在createBeanDefinitionDocumentReader()中完成的,而通過此方法,BeanDefinitionDocumentReader真正的型別其實已經是DefaultBeanDefinitionDocumentReader了,進入DefaultBeanDefinitionDocumentReader後,發現這個方法的重要目的之一就是提取root,以便於再次將root作為引數繼續BeanDefinition的註冊

public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
    this.readerContext = readerContext;
    this.logger.debug("Loading bean definitions");
    Element root = doc.getDocumentElement();
    BeanDefinitionParserDelegate delegate = this.createHelper(readerContext, root);
    //解析前處理,留給子類實現
    this.preProcessXml(root);
    this.parseBeanDefinitions(root, delegate);
    //解析後處理,留給子類實現
    this.postProcessXml(root);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

解析並註冊BeanDefinition

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    //對beans的處理
    if(delegate.isDefaultNamespace(delegate.getNamespaceURI(root))) {
        NodeList nl = root.getChildNodes();

        for(int i = 0; i < nl.getLength(); ++i) {
            Node node = nl.item(i);
            if(node instanceof Element) {
                Element ele = (Element)node;
                String namespaceUri = delegate.getNamespaceURI(ele);
                if(delegate.isDefaultNamespace(namespaceUri)) {
                    //對bean的處理
                    this.parseDefaultElement(ele, delegate);
                } else {
                    //對bean的處理
                    delegate.parseCustomElement(ele);
                }
            }
        }
    } else {
        delegate.parseCustomElement(root);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在Spring的XML配置裡面有兩大類Bean宣告,一個是預設的,如:

<bean id="test" class="test.TestBean"/>

另一類就是自定義的,如:

<tx:annotation-driven>
  • 1

而這兩種方式的讀取及解析差別是非常大的,如果採用Spring預設的配置,Spring當然知道該怎麼做,但如果是自定義的,那麼就需要使用者實現一些介面及配置了。對於根節點或子節點如果是預設名稱空間的話採用parseDefaultElement方法進行解析,否則使用delegate.parseCustomElement方法對自定義名稱空間進行解析。而判斷是否預設名稱空間還是自定義名稱空間的辦法其實是使用node.getNamespaceURI()獲取名稱空間,並與Spring中固定的名稱空間http://www.springframework.org/schema/beans進行對比,如果一致則認為是預設,否則就認為是自定義