1. 程式人生 > >Spring原始碼閱讀筆記03:xml配置讀取

Spring原始碼閱讀筆記03:xml配置讀取

  前面的文章介紹了IOC的概念,Spring提供的bean容器即是對這一思想的具體實現,在接下來的幾篇文章會側重於探究這一bean容器是如何實現的。在此之前,先用一段話概括一下bean容器的基本工作原理。顧名思義,bean容器的作用是替我們管理bean物件(簡單的Java類物件)的。不管框架如何強大,還是需要我們程式設計師來告訴其一些必要資訊的(比如要管理的bean物件的類相關資訊、是否開啟元件掃描等),這些我們稱之為對Spring框架的配置,目前主流的配置方式是通過使用配置檔案或註解。配置好之後,框架就需要將這些配置讀取並儲存到記憶體中(其實就是儲存在物件裡面)。經過這一步轉化之後,Spring框架就能夠幫助我們載入指定的類,然後將其例項化並且快取起來以供需要的時候直接使用,這就是容器。當我們將容器關閉時,Spring框架會將之前建立的所有相關物件全部銷燬,並釋放資源。

  如上只是簡單介紹了一下Spring提供的bean容器的基本工作原理,從中能夠了解大體流程即可,真實的容器其工作原理遠遠比這複雜。本文主要總結Spring對配置的讀取以及將配置轉化儲存到記憶體這部分,並且配置獲取這部分的原始碼也只限於對xml配置檔案的讀取。

  上面說到的配置讀取及初始化的功能對應前面文章中的程式碼看起來只有區區一行,如下:

BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("beans.xml"));

  這行程式碼做了兩件事情:

  • 將xml配置檔案封裝成Resource;
  • 初始化BeanFactory;

1. 配置檔案封裝

  Spring的配置檔案讀取功能是封裝在ClassPathResource中,對應前面的程式碼就是new ClassPathResource("bean.xml"),那麼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 contentLength() throws IOException;
    long lastModified() throws IOException;
    Resource createRelative(String relativePath) throws IOException;
    String getFilename();
    String getDescription();
}

  InputStreamSource是一個介面,它只有一個方法定義:getInputStream(),該方法返回一個新的InputStream物件,該介面定義任何能返回InputStream的類,比如file普通檔案、Classpath下的資原始檔和ByteArray等。

  Resource介面用於抽象所有Spring內部使用到的底層資源:File、URL、Classpath等,其定義了一系列方法:

  • 首先,它定義了3個判斷當前資源狀態的方法:存在性(exists)、可讀性(isReadable)、是否處於開啟狀態(isOpen);
  • 另外,Resource介面還提供了不同資源到URL、URI、File型別的轉換,以及獲取lastModified屬性、檔名(不帶路徑資訊的檔名,getFilename())的方法;
  • 為了便於操作,Resource還提供了基於當前資源建立一個相對資源的方法:createRelative();
  • 在錯誤處理中需要詳細地打印出錯的資原始檔,因而Resource還提供了getDescription()方法用於在錯誤處理中列印資訊;

  Spring中對不同來源的資原始檔型別都有相應的Resource實現:檔案(FileSystemResource)、Classpath資源( ClassPathResource)、URL資源(UrIResource)、InputStream資源(InputStreamResource)、Byte陣列(ByteArrayResource)等。

  Resource介面的作用是消除底層資源訪問的差異,允許程式以一致的方式來訪問不同的底層資源,而其實現是非常簡單的,以getInputStream()方法實現為例,ClassPathResource中的實現方式是直接呼叫class或者classLoader提供的底層方法getResourceAsStream,而對於FileSystemResource的實現其實更簡單,直接使用FileInputStream對檔案進行例項化。

// ClasspathResource.java
public InputStream getInputStream() throws IOException {
    InputStream is;
    if (this.clazz != null) {
        is = this.clazz.getResourceAs你Stream(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;
}

// FileSystemResource
public InputStream getInputStream()throws IOException{
    return new FileInputStream(this.file);
}

  這樣就可以將資源統一轉化成InputStream供後續使用了,而前面示例程式碼中使用的xml配置檔案是屬於什麼型別的Resource呢?其實從原始碼中我們就不難發現是屬於ClassPathResource的,而new ClassPathResource("bean.xml")這句程式碼的內部實現就不細說了,無非就是初始化配置檔案路徑。

  一句話總結,Spring通過Resource介面抽象所有的資源,在容器啟動的第一步就是將資原始檔對映成Resource物件,以供後續通過流的方式來獲取資源。

  現在配置檔案封裝到Resource中之後,後續Spring在初始化BeanFactory的過程中就可以方便地呼叫其getInputStream()方法來獲取其對應的流了,然後做進一步轉化。

public XmlBeanFactory(Resource resource) throws BeansException {
    this(resource, null);
}

public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
    super(parentBeanFactory);
    this.reader.loadBeanDefinitions(resource);
}

  這段程式碼主要作用是初始化BeanFactory,其中this.reader.loadBeanDefinitions(resource)就是資源載入的真正實現,也是我們接下來的分析重點。

 

2. 轉換成beanDefinition

  我們來看一下loadBeanDefinitions()方法具體的內部實現:

public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
    return loadBeanDefinitions(new EncodedResource(resource));
}

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
    Assert.notNull(encodedResource, "EncodedResource must not be null");
    if (logger.isInfoEnabled()) {
        logger.info("Loading XML bean definitions from " + encodedResource.getResource());
    }
    // 通過屬性來記錄已經載入的資源
    Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
    if (currentResources == null) {
        currentResources = new HashSet<EncodedResource>(4);
        this.resourcesCurrentlyBeingLoaded.set(currentResources);
    }
    if (!currentResources.add(encodedResource)) {
        throw new BeanDefinitionStoreException(
                "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
    }
    try {
        // 從encodeResource中獲取封裝的Resource物件並再次從Resouce中獲取其中的inputStream
        InputStream inputStream = encodedResource.getResource().getInputStream();
        try {
            // InputSource這個類並不來自於Spring,它來自org.xml.sax.InputSource
            InputSource inputSource = new InputSource(inputStream);
            if (encodedResource.getEncoding() != null) {
                inputSource.setEncoding(encodedResource.getEncoding());
            }
            // 核心邏輯部分
            return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
        }
        finally {
            inputStream.close();
        }
    }
    catch (IOException ex) {
        throw new BeanDefinitionStoreException(
                "IOException parsing XML document from " + encodedResource.getResource(), ex);
    }
    finally {
        currentResources.remove(encodedResource);
        if (currentResources.isEmpty()) {
            this.resourcesCurrentlyBeingLoaded.remove();
        }
    }
}

  這一部分其實還只是資料準備階段,主要做了如下三件事情:

  • 封裝資原始檔。當進入XmlBeanDefinitionReader後首先對引數Resource使用EncodedResource類進行封裝;
  • 獲取輸入流。從Resource中獲取對應的InputStream並構造InputSource;
  • 通過構造好的InputSource例項和Resource例項繼續呼叫方法:doLoadBeanDefinitions,這是真正的核心處理部分;
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
            throws BeanDefinitionStoreException {
    try {
        int validationMode = getValidationModeForResource(resource);
        Document doc = this.documentLoader.loadDocument(
                inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware());
        return registerBeanDefinitions(doc, resource);
    }
    catch (BeanDefinitionStoreException ex) {
        throw ex;
    }
    。。。catch若干異常
}

  不考慮其中異常類的程式碼,這段程式碼其實只做了三件事:

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

   這3個步驟支撐著整個Spring容器部分的實現基礎,尤其是第3步對配置檔案的解析,邏輯非常的複雜,這裡我們只分析第2步和第3步。

 2.1 獲取Document

  XmIBeanFactoryReader類對於文件讀取並沒有親力親為,而是委託給了DocumentLoader去執行,這裡的DocumentLoader只是個介面,實際物件則是DefaultDocumentLoader,解析程式碼如下: 

public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
        ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

    DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
    if (logger.isTraceEnabled()) {
        logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
    }
    DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
    return builder.parse(inputSource);
}

  首先建立DocumentBuilderFactory,再通過DocumentBuilderFactory建立DocumentBuilder,然後解析inputSource來返回Document物件。這部分是JDK提供的功能,有興趣的可以自行搜尋,此處就不再贅述。

2.2 解析及註冊BeanDefinitions

  當把檔案轉換為Document後,接下來的提取及註冊bean就是重頭戲。繼續上面的分析,當程式已經擁有XML文件檔案的Document例項物件時,就會被引入下面這個方法:

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
    // 使用DefaultBeanDefinitionDocumentReader例項化BeanDefinitionDocumentReader
    BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
    // 將環境變數設定其中
    documentReader.setEnvironment(getEnvironment());
    // 在例項化BeanDefinitionReader時候會將BeanDefinitionRegistry傳入,預設使用繼承自DefaultListableBeanFactory的子類
    // 記錄統計前BeanDefinition的載入個數
    int countBefore = getRegistry().getBeanDefinitionCount();
    // 載入及註冊bean
    documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
    // 記錄本次載入的BeanDefinition個數
    return getRegistry().getBeanDefinitionCount() - countBefore;
}

   在這個方法中,載入及註冊bean的邏輯是委託給BeanDefinitionDocumentReader指向的類來處理,這很好地應用了面向物件中單一職責的原則。BeanDefinitionDocumentReader是一個介面,其例項化的工作是在 createBeanDefinitionDocumentReader()中完成的,而通過此方法,BeanDefinitionDocumentReader真正的型別其實已經是DefaultBeanDefinitionDocumentReader了,進入DefaultBeanDefinitionDocumentReader的registerBeanDefinitions()方法後,發現這個方法的重要目的之一就是提取root,以便於再次將root作為引數繼續BeanDefinition的註冊:

public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
    this.readerContext = readerContext;
    logger.debug("Loading bean definitions");
    Element root = doc.getDocumentElement();
    doRegisterBeanDefinitions(root);
}

  終於到了核心邏輯的底部doRegisterBeanDefinitions(root),如果說之前一直是XML載入解析的準備階段,那麼doRegisterBeanDefinitions算是真正地開始進行解析了。

protected void doRegisterBeanDefinitions(Element root) {
    // 處理profile屬性
    String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
    if (StringUtils.hasText(profileSpec)) {
        String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
                profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
        if (!getEnvironment().acceptsProfiles(specifiedProfiles)) {
            return;
        }
    }
    // 專門處理解析
    BeanDefinitionParserDelegate parent = this.delegate;
    this.delegate = createDelegate(this.readerContext, root, parent);
    // 解析前處理,留給子類實現
    preProcessXml(root);
    parseBeanDefinitions(root, this.delegate);
    // 解析後處理,留給子類實現
    postProcessXml(root);
    
    this.delegate = parent;
}

  這裡首先是對profile的處理,然後開始進行解析,preProcessXml(root)和postProcessXml(root)方法是空實現,留待使用者繼承DefaultBeanDefinitionDocumentReader後需要在Bean解析前後做一些處理時重寫這兩個方法。跟蹤程式碼進入parseBeanDefinitions(root, this.delegate):

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    // 對beans的處理
    if (delegate.isDefaultNamespace(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;
                if (delegate.isDefaultNamespace(ele)) {
                    // 對bean的處理
                    parseDefaultElement(ele, delegate);
                }
                else {
                    // 對bean的處理
                    delegate.parseCustomElement(ele);
                }
            }
        }
    }
    else {
        delegate.parseCustomElement(root);
    }
}

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

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

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

<tx: annotation-driven/>

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

 

3. 總結

  本文主要總結Spring對配置(xml配置檔案)的讀取以及將配置轉化儲存到記憶體這部分,對xml配置的讀取主要是將其轉換成Resource,而將配置轉化儲存則主要是從Resource中獲取InputStream並將其解析轉化成BeanDefinition等物件儲存起