1. 程式人生 > >簡單看Spring原始碼--對xml檔案解析

簡單看Spring原始碼--對xml檔案解析

Spring如何解析xml配置檔案?

xml配置檔案是Spring中極其重要的一部分,讓我們一起看一下spring解析xml檔案的。

以下是一段簡單的通過類路徑下的test.xml檔案載入bean獲得BeanFactory的程式碼:

BeanFactory bf = new XmlBeanFactory(new ClassPathResource("test.xml"));

一行程式碼,spring做的事情極其的複雜,主要分為以下幾步:

1, 把資原始檔進行封裝,封裝為Resource,有了Resource就可以對所有的資原始檔進行統一處理
ClassPathResource的核心邏輯: (其實是簡單的用class或classLoader讀取類路徑的資原始檔)

        InputStream is;
        if (this.clazz != null) {
            is = this.clazz.getResourceAsStream(this.path);
        }
        else if (this.classLoader != null) {
            is = this.classLoader.getResourceAsStream(this.path);
        }
        else {
            is = ClassLoader.getSystemResourceAsStream(this
.path); } if (is == null) { throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist"); } return is;

2,在XmlBeanFactory構造方法中呼叫XmlBeanDefinitionReader開始Bean的載入

this.reader.loadBeanDefinitions(resource);

這句程式碼是整個資源載入的切入點。

3, EncodedResource對Resoure進行編碼的處理,設定了編碼屬性的時候Spring會使用相應的編碼作為輸入流的編碼

public Reader getReader() throws IOException {
        if (this.charset != null) {
            return new InputStreamReader(this.resource.getInputStream(), this.charset);
        }
        else if (this.encoding != null) {
            return new InputStreamReader(this.resource.getInputStream(), this.encoding);
        }
        else {
            return new InputStreamReader(this.resource.getInputStream());
        }
    }

這段程式碼設定了輸入流的編碼,但是我在看原始碼的過程中並沒有發現其有被呼叫的情況,
在註釋上@see getInputStream()

    @Override
    public InputStream getInputStream() throws IOException {
        return this.resource.getInputStream();
    }

發現直接就是呼叫resource方法。
到底怎麼回事???我看的時候一臉矇蔽0。0

4,構造InputSource,InputSource並不來自於Spring,全路徑是org.xml.sax,SAX解析xml的時候會使用InputSource來決定如何讀取xml檔案

InputStream inputStream = encodedResource.getResource().getInputStream();
            try {
                InputSource inputSource = new InputSource(inputStream);
                if (encodedResource.getEncoding() != null) {
                    inputSource.setEncoding(encodedResource.getEncoding());
                }
                return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
            }

原來encode在這裡實現了,那麼上面的EncodeResource只是起到了簡單的提供encode或者charset的作用,
這樣的話那麼設計的時候是不是可以把EncodeResource去掉呢,直接在InputSource.setEncoding即可。

5,現在到達了核心處理部分

protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
        return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
        getValidationModeForResource(resource), isNamespaceAware());
    }

首先getValidationModeForResource獲取xml檔案的驗證模式(DTD或者XSD),可以自己設定驗證方式,否則預設是開啟VALIDATION_AUTO即自動獲取驗證模式的,底層實現是InputStream讀取xml檔案看xml檔案是否包含DOCTYPE單詞,包含的話就是DTD,否則返回XSD。看一下關鍵邏輯:

BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        try {
            boolean isDtdValidated = false;
            String content;
            while ((content = reader.readLine()) != null) {
                content = consumeCommentTokens(content);
                if (this.inComment || !StringUtils.hasText(content)) {
                    continue;
                }
                if (hasDoctype(content)) {
                    isDtdValidated = true;
                    break;
                }
                if (hasOpeningTag(content)) {
                    // End of meaningful data...
                    break;
                }
            }
            return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);

接著獲取Document,Spring並沒有進行特殊的對xml文件的處理,使用了SAX解析xml文件,三步走:先建立DocumentBuilderFactory,接著獲取DocumentBuilder,最後解析InputStream返回Document物件。

6, 可以看一下EntityResolver類,EntityResolver是什麼呢,對於一個xml的驗證,XSD或者DTD檔案預設是從網上下載的,可以的話一般都是把DTD檔案放在工程之中,而EntityResolver就是提供了一個如何尋找DTD檔案的宣告。

對於xsd模式:

xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

對於DTD模式驗證:

<!DOCTYPE beans PUBLIC "-//Spring//DTD BEAN 2.0//EN"
        "http://www.springframework.org/dtd/spring-beans-2.0.dtd">

預設spring對於兩種驗證方式提供了不同的解析器。

this.dtdResolver = new BeansDtdResolver();
this.schemaResolver = new PluggableSchemaResolver(classLoader);

XSD解析:
預設的XSD解析PluggableSchemaResolver使用了private volatile Map<String, String> schemaMappings;來儲存schemaURL->local schema path的對映,預設的schemaMappingLocation位於META-INF/spring.schemas ,通過getSchemaMapping來獲取這個對映,其中使用雙重校驗鎖的方式來實現單例模式:

if (this.schemaMappings == null) {
            synchronized (this) {
                if (this.schemaMappings == null) {
                 ...
                 }
            }
     }

有了map,接下來獲取InputSource就是使用systemId獲取resourceLocation,實現比較簡單。

DTD解析:
DTD解析是直接擷取systemId的最後的xx.dtd去當前路徑下面尋找。

7,快了,快了再堅持一下,最後解析及註冊bean
通過DocumentBuilder獲取Document之後,就剩下

return registerBeanDefinitions(doc, resource);

方法的返回值是發現的定義的bean的數目,方法主要內容:

//建立DocumentReader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
//註冊beanDefinition
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
//獲得已有的beanDefinition數目
int countBefore = getRegistry().getBeanDefinitionCount();

return getRegistry().getBeanDefinitionCount() - countBefore;

註冊bean的時候首先使用一個BeanDefinitionParserDelegate類來判斷是否是預設名稱空間,實現是通過判斷namespace uri 是否和預設的uri相等:

public static final String BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans";
public boolean isDefaultNamespace(String namespaceUri) {
        return (!StringUtils.hasLength(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri));
    }

對於預設的名稱空間,首先開始的是對profile屬性解析,profile用得最多的是DataSource在不同環境下使用不同的bean,spring使用StringTokenizer來進行字串的分割,但是jdk為了相容性是推薦使用String.split()方法的:

String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
            if (StringUtils.hasText(profileSpec)) {
                String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
                        profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
                if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
                                "] not matching: " + getReaderContext().getResource());
                    }
                    return;
                }
            }
preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);

接下來的解析使用了模板模式,preProcessXml和postProcessXml都是空方法,是為了方便之後的子類在解析之前之後進行一些處理。只需要覆寫這兩個方法即可。

在parseBeanDefinitions方法中spring對不同名稱空間的元素的解析使用不同的方法:

Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
    parseDefaultElement(ele, delegate);
     }
else {
    delegate.parseCustomElement(ele);
    }

對於不同的bean宣告,spring的處理方法我先看一下,下次再寫了。。

為了首尾呼應,回到開始的

BeanFactory bf = new XmlBeanFactory(new ClassPathResource("test.xml"));

我們已經進入到解析bean的比較深入的步驟了,接下來

<bean id="myTestBean" class="MyTestBean.class"/>

經過預設名稱空間的解析,接下來就是對bean標籤的解析以及註冊。
下次再寫了。。。

以上只是自己的一些淺薄見解,是為了做一下總結,想努力地提升自己。如果有不對的地方,或者是哪裡出錯的地方,希望大佬可以指點一下小弟。。。