1. 程式人生 > >Spring 自定義 XML 配置擴充套件

Spring 自定義 XML 配置擴充套件

XML 配置用的越來越少了,不過還是有比較瞭解一下 Spring 對 XML 的擴充套件機制。Spring 是基於 Dom 進行操作的。

可以先看看官方文件的介紹:

9.2. XML Schema Authoring

9.2.1. Introduction

Since version 2.0, Spring has featured a mechanism for schema-based extensions to the basic Spring XML format for defining and configuring beans. This section is devoted to detailing how you would go about writing your own custom XML bean definition parsers and integrating such parsers into the Spring IoC container.

To facilitate the authoring of configuration files using a schema-aware XML editor, Spring’s extensible XML configuration mechanism is based on XML Schema. If you are not familiar with Spring’s current XML configuration extensions that come with the standard Spring distribution, please first read the appendix entitled

[xsd-config].

Creating new XML configuration extensions can be done by following these (relatively) simple steps:

  • Authoring an XML schema to describe your custom element(s).

  • Coding a custom NamespaceHandler implementation (this is an easy step, don’t worry).

  • Coding one or more BeanDefinitionParser implementations (this is where the real work is done).

  • Registering the above artifacts with Spring (this too is an easy step).

在 resources 下新建 context.xml 檔案,從 Spring 官網(https://docs.spring.io/spring/docs/5.0.11.BUILD-SNAPSHOT/spring-framework-reference/core.html#xml-custom)找一段 Demo :

可以複製這段 XML :

xmlns="http://www.springframework.org/schema/beans"

修改為: 

其中:

"http://www.springframework.org/schema/context"

就稱之為名稱空間,同時有一個字首 context。

根據 XML Schema 的機制,需要配置地址,而且還需要成對出現:

為什麼要成對出現呢,因為這裡就相當於是個 Key-Value 結構一樣:

這個 xsd 從網頁上開啟是不存在的(主要是因為這裡的路徑寫錯了,後面會有說明):

但是我們可以通過 Ctrl + 滑鼠左鍵看到檔案地址:

這個檔案在 jar 裡面:

再多說一個,這裡有很多人喜歡帶上版本:

其實不帶版本更好一點,不帶版本會跟著 jar 包走。

這樣在配置檔案中可以使用 <context> 標籤:

其實有這麼一個概念:prefix:local-element-name。比如在上面這個 XML 中,context 就是 prefix,component-scan 就是 local-element-name。

在 xsd 檔案中也是這麼定義的:

也能夠看到一些子屬性:

也就是說可以在 context 標籤中增加 這個子屬性:

再比如這裡還有一個 sequence:

也就是說這兩個屬性是有順序要求的,比如這麼使用就報錯了:

必須要嚴格遵守順序:

再比如屬性就是使用 attribute 來表達,同時還可以定義型別,這裡定義的 string:

那麼 XML 進行了配置後,必然會有一些相應的類去處理,比如我們常用的這個配置:

新建一個 User 類,並在 application.properties 檔案中新增內容:

配置 User Bean:

因為這裡使用了中文,先看看 application.properties 檔案的編碼:

在 PropertyPlaceholderConfigurer 中指定編編碼一直:

簡單使用 ClassPathXmlApplicationContext 測試一下,那麼為什麼 ClassPathXmlApplicationContext 可以進行 Spring 測試呢,因為在 ClassPathXmlApplicationContext 的構造中執行了 refresh() 方法:

測試程式碼:

輸出:

Spring 3.1 之後也有一個讀取配置檔案的新的寫法:

而本質也是使用的這個類:

這個類和之前使用的 PropertyPlaceholderConfigurer 還是不一樣的。但是都是繼承自同一個類:

Spring 3.1 後基於 Schema 的擴充套件替代了之前的寫法。

在執行測試程式碼:

發現出現了異常:

org.xml.sax.SAXParseException: schema_reference.4: 無法讀取方案文件 'http://www.springframework.org/schema/spring-context.xsd', 原因為 1) 無法找到文件; 2) 無法讀取文件; 3) 文件的根元素不是 <xsd:schema>。
	at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(ErrorHandlerWrapper.java:203)
	at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.warning(ErrorHandlerWrapper.java:99)
	at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:392)
	at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:306)
	at com.sun.org.apache.xerces.internal.impl.xs.traversers.XSDHandler.reportSchemaErr(XSDHandler.java:4162)
	at com.sun.org.apache.xerces.internal.impl.xs.traversers.XSDHandler.reportSchemaWarning(XSDHandler.java:4153)
	at com.sun.org.apache.xerces.internal.impl.xs.traversers.XSDHandler.getSchemaDocument1(XSDHandler.java:2486)
	at com.sun.org.apache.xerces.internal.impl.xs.traversers.XSDHandler.getSchemaDocument(XSDHandler.java:2183)
	at com.sun.org.apache.xerces.internal.impl.xs.traversers.XSDHandler.parseSchema(XSDHandler.jav
......
......
Exception in thread "main" org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException: Line 21 in XML document from class path resource [context.xml] is invalid; nested exception is org.xml.sax.SAXParseException; lineNumber: 21; columnNumber: 92; cvc-complex-type.2.4.c: 萬用字元的匹配很全面, 但無法找到元素 'context:property-placeholder' 的宣告。
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:404)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:336)

應該是這部分出現了問題:

那到底是哪裡出現了問題呢,其實 Spring XML 擴充套件主要基於 Schema 配置(META-INF/spring.schema)和 Namespace Hander 配置(META-INF/spring.handlers),可以搜尋一下 spring.schema 檔案,選擇和 context 有關的:

找到原因了,這裡少了一個 context。

修改 context.xml 檔案:

重新執行測試程式碼,執行成功:

有了 Schema 就肯定需要定義 namespace,即 Handler ,也就是要定義處理類,可以搜尋 META-INF/spring.handlers 檔案,選擇 context :

 也就是存在這樣的關聯關係:

進入 context 的那個類,可以看到很熟悉的 property-placeholder:

這個就是之前寫的這個:

而後面有一個處理類:

Spring XML 擴充套件

      Schema 配置

           META-INF/spring.schemas(作為 properties 檔案處理,所以 : 需要轉義,類似 k = v 或者 k : v )

              schema 絕對路徑 = schema 相對路徑

 在 Scheme 中需要定義 namespace ,也就是需要定義處理類。

       Namespace Handler 配置

             META-INF/spring.handlers

Bean 的定義:BeanDefination

Bean 定義解析器:BeanDefinitionParser

Spring XML Schema -> 自定義元素 -> 解析Bean的定義

總的來說,存在一個對映的關係。

所以照葫蘆畫瓢:

第一步:定義 Schema.xsd:

從 spring-context.xsd 檔案中複製過來:

定義元素 user -> User

定義 targetNamespace:

第二步:建立 schema 絕對路徑和相對路徑的對映(META-INF/spring.schemas)

絕對的XSD = 相對的XSD

建立相關的檔案:

第三步:新增 my.xsd 到 context.xml檔案中

引入 namespace(就是剛剛的 targetNamespace):

配置 namespace Schema 對映:

需要成對出現:

引入配置:

第四步:建立 Schema namespace 與 Handler 對映( META-INF/spring.handlers)

建立  META-INF/spring.handlers 檔案:

將剛剛的 namespace 複製過來,並實現 NamespaceHandlerSupport:

建立 user 元素的 BeanDefinitionParser(實現 BeanDefinitionParser):

package com.example.demoXML.config;

import com.example.demoXML.model.User;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.lang.Nullable;
import org.w3c.dom.Element;

/**
 * @author Dongguabai
 * @date 2018/11/26 20:44
 */
public class MyParser implements BeanDefinitionParser{

    /**
     *  解析  <my:user age="12" name="${name}" />
     *
     */

    @Nullable
    @Override
    public BeanDefinition parse(Element element, ParserContext parserContext) {

        //獲取 age(原始為String)
        String age = element.getAttribute("age");

        //這裡需要處理 Placeholder
        String name = element.getAttribute("name");

        //構建BeanDefination
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(User.class);

        //新增 Property的值
        builder.addPropertyValue("age",age);
        builder.addPropertyValue("name",name);

        //建立 BeanDefinition
        return builder.getBeanDefinition();
    }

}

建立 local-element-name 與 BeanDefinitionParser 的聯絡:

執行測試類:

出現了異常:

Exception in thread "main" org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Cannot locate BeanDefinitionParser for element [user]
Offending resource: class path resource [context.xml]
	at org.springframework.beans.factory.parsing.FailFastProblemReporter.fatal(FailFastProblemReporter.java:62)
	at org.springframework.beans.factory.parsing.ReaderContext.fatal(ReaderContext.java:90)
	at org.springframework.beans.factory.parsing.ReaderContext.fatal(ReaderContext.java:68)
	at org.springframework.beans.factory.xml.NamespaceHandlerSupport.findParserForElement(NamespaceHandlerSupport.java:86)
	at org.springframework.beans.factory.xml.NamespaceHandlerSupport.parse(NamespaceHandlerSupport.java:73)

原來剛剛這個地方寫錯了 element,要更改為 user:

再啟動測試類:

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.demoXML.model.User' available
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:343)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:335)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1101)
	at com.example.demoXML.SpringTest.main(SpringTest.java:14)

又出現異常了,說 User 沒有註冊。為什麼會沒有註冊呢。檢查一下到底是什麼原因。找到 org.springframework.context.support.AbstractApplicationContext#refresh 這個方法,這個方法應該很熟悉了,初始化整個容器的方法。在 finishBeanFactoryInitialization()方法上打個斷點,這個方法是初始化所有的 BeanDefinition:

接下來再在這個方法上打個斷點,看看兩個斷點的執行順序:

的確是先執行的這個方法:

但是發現此時 User 並未註冊進 Spring 容器:

也就是說我們需要在 MyParser 中將 User 的BeanDefinition 註冊進去:

再來執行測試程式碼,可以發現此時 User 已經註冊進去了:

也輸出了想要的結果:

我這裡註冊的時候 Bean 的 id 是寫死了,可以擴充套件一下,在 my.xsd 檔案中:


 

修改 MyParser:

在 context.xml 檔案中也定義一下:

再次執行測試程式碼,出現了異常:

Exception in thread "main" org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.example.demoXML.model.User' available: expected single matching bean but found 2: myUser1,myUser2
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1139)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:407)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:341)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:335)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1101)
	at com.example.demoXML.SpringTest.main(SpringTest.java:14)

這個異常的原因就很好解釋了,修改測試程式碼:

再次執行,測試成功:

Demo 原始碼地址:https://gitee.com/white_melon_white/spring-xml