Spring 中的 XML schema 擴充套件機制

Kirito的技術分享發表於2019-01-22

前言

很久沒有寫關於 Spring 的文章了,最近在系統梳理 Dubbo 程式碼的過程中發現了 XML schema 這個被遺漏的知識點。由於工作中使用 SpringBoot 比較多的原因,幾乎很少接觸 XML,此文可以算做是亡羊補牢,另一方面,也為後續的 Dubbo 原始碼解析做個鋪墊。

XML schema 擴充套件機制是啥?這並不是一塊很大的知識點,翻閱一下 Spring 的文件,我甚至沒找到一個貫穿上下文的詞來描述這個功能,XML Schema Authoring 是文件中對應的標題,簡單來說:

Spring 為基於 XML 構建的應用提供了一種擴充套件機制,用於定義和配置 Bean。 它允許使用者編寫自定義的 XML bean 解析器,並將解析器本身以及最終定義的 Bean 整合到 Spring IOC 容器中。

dubbo.xml

Dubbo 依賴了 Spring,並提供了一套自定義的 XML 標籤,<dubbo:application> ,<dubbo:registry> ,<dubbo:protocol>,<dubbo:service>。作為使用者,大多數人只需要關心這些引數如何配置,但不知道有沒有人好奇過,它們是如何載入進入 Spring 的 IOC 容器中被其他元件使用的呢?這便牽扯出了今天的主題:Spring 對 XML schema 的擴充套件支援。

自定義 XML 擴充套件

為了搞懂 Spring 的 XML 擴充套件機制,最直接的方式便是實現一個自定義的擴充套件。實現的步驟也非常簡單,分為四步:

  1. 編寫一個 XML schema 檔案描述的你節點元素。
  2. 編寫一個 NamespaceHandler 的實現類
  3. 編寫一個或者多個 BeanDefinitionParser 的實現 (關鍵步驟).
  4. 註冊上述的 schema 和 handler。

我們的目的便是想要實現一個 kirito XML schema,我們的專案中可以自定義 kirito.xml,在其中會以 kirito 為標籤來定義不同的類,並在最終的測試程式碼中驗證這些宣告在 kirito.xml 的類是否被 Spring 成功載入。大概像這樣,是不是和 dubbo.xml 的格式很像呢?

kirito.xml

動手實現

有了明確的目標,我們逐步開展自己的工作。

1 編寫kirito.xsd

resources/META-INF/kirito.xsd

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.cnkirito.moe/schema/kirito"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            xmlns:beans="http://www.springframework.org/schema/beans"
            targetNamespace="http://www.cnkirito.moe/schema/kirito"><xsd:import namespace="http://www.springframework.org/schema/beans"/>

    <xsd:element name="application"><xsd:complexType>
            <xsd:complexContent>
                <xsd:extension base="beans:identifiedType">
                    <xsd:attribute name="name" type="xsd:string" use="required"/>
                </xsd:extension>
            </xsd:complexContent>
        </xsd:complexType>
    </xsd:element>

    <xsd:element name="service"><xsd:complexType>
            <xsd:complexContent>
                <xsd:extension base="beans:identifiedType">
                    <xsd:attribute name="name" type="xsd:string" use="required"/>
                </xsd:extension>
            </xsd:complexContent>
        </xsd:complexType>
    </xsd:element>


</xsd:schema>
複製程式碼

① 注意這裡的 targetNamespace="http://www.cnkirito.moe/schema/kirito" 這便是之後 kirito 標籤的關鍵點。

② kirito.xsd 定義了兩個元素: application 和 service,出於簡單考慮,都只有一個 name 欄位。

schema 的意義在於它可以和 eclipse/IDEA 這樣智慧化的整合開發環境形成很好的搭配,在編輯 XML 的過程中,使用者可以獲得告警和提示。 如果配置得當,可以使用自動完成功能讓使用者在事先定義好的列舉型別中進行選擇。

2 編寫KiritoNamespaceHandler

public class KiritoNamespaceHandler extends NamespaceHandlerSupport {

    @Override
    public void init() {
        super.registerBeanDefinitionParser("application", new KiritoBeanDefinitionParser(ApplicationConfig.class));
        super.registerBeanDefinitionParser("service", new KiritoBeanDefinitionParser(ServiceBean.class));
    }

}
複製程式碼

完成 schema 之後,還需要一個 NamespaceHandler 來幫助 Spring 解析 XML 中不同名稱空間的各類元素。

<kirito:application name="kirito"/>
<dubbo:application name="dubbo"/>
<motan:application name="motan"/>
複製程式碼

不同的名稱空間需要不同的 NamespaceHandler 來處理,在今天的示例中,我們使用 KiritoNamespaceHandler 來解析 kirito 名稱空間。KiritoNamespaceHandler 繼承自 NamespaceHandlerSupport 類,並在其 init() 方法中註冊了兩個 BeanDefinitionParser ,用於解析 kirito 名稱空間/kirito.xsd 約束中定義的兩個元素:application,service。BeanDefinitionParser 是下一步的主角,我們暫且跳過,將重心放在父類 NamespaceHandlerSupport 之上。

public interface NamespaceHandler {
   void init();
   BeanDefinition parse(Element element, ParserContext parserContext);
   BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder definition, ParserContext parserContext);
}
複製程式碼

NamespaceHandlerSupport 是 NamespaceHandler 名稱空間處理器的抽象實現,我粗略看了NamespaceHandler 的幾個實現類,parse 和 decorate 方法可以完成元素節點的組裝並通過 ParserContext 註冊到 Ioc 容器中,但實際我們並沒有呼叫這兩個方法,而是通過 init() 方法註冊 BeanDefinitionParser 來完成解析節點以及註冊 Bean 的工作,所以對於 NamespaceHandler,我們主要關心 init 中註冊的兩個 BeanDefinitionParser 即可。

3 編寫KiritoBeanDefinitionParser

在文章開始我們便標記到 BeanDefinitionParser 是最為關鍵的一環,每一個 BeanDefinitionParser 實現類都負責一個對映,將一個 XML 節點解析成 IOC 容器中的一個實體類。

public class KiritoBeanDefinitionParser implements BeanDefinitionParser {

    private final Class<?> beanClass;

    public KiritoBeanDefinitionParser(Class<?> beanClass) {
        this.beanClass = beanClass;
    }

    private static BeanDefinition parse(Element element, ParserContext parserContext, Class<?> beanClass) {
        RootBeanDefinition beanDefinition = new RootBeanDefinition();
        beanDefinition.setBeanClass(beanClass);
        beanDefinition.setLazyInit(false);
        String name = element.getAttribute("name");
        beanDefinition.getPropertyValues().addPropertyValue("name", name);
        parserContext.getRegistry().registerBeanDefinition(name, beanDefinition);
        return beanDefinition;
    }

    @Override
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        return parse(element, parserContext, beanClass);
    }

}
複製程式碼

由於我們的實體類是非常簡單的,所以不存在很複雜的解析程式碼,而實際專案中,往往需要大量的解析步驟。parse 方法會解析一個個 XML 中的元素,使用 RootBeanDefinition 組裝成物件,並最終通過 parserContext 註冊到 IOC 容器中。

至此,我們便完成了 XML 檔案中定義的物件到 IOC 容器的對映。

4 註冊schema和handler

最後一步還需要通知 Spring,告知其自定義 schema 的所在之處以及對應的處理器。

resources/META-INF/spring.handlers

http\://www.cnkirito.moe/schema/kirito=moe.cnkirito.sample.xsd.KiritoNamespaceHandler
複製程式碼

resources/META-INF/spring.schemas

http\://www.cnkirito.moe/schema/kirito/kirito.xsd=META-INF/kirito.xsd
複製程式碼

沒有太多可以說的,需要遵守 Spring 的約定。

至此一個自定義的 XML schema 便擴充套件完成了,隨後來驗證一下。

驗證擴充套件

我們首先定義好 kirito.xml

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

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:kirito="http://www.cnkirito.moe/schema/kirito"
       xsi:schemaLocation=" http://www.springframework.org/schema/beans
                                http://www.springframework.org/schema/beans/spring-beans.xsd
                                http://www.cnkirito.moe/schema/kirito
                                http://www.cnkirito.moe/schema/kirito/kirito.xsd">

    <kirito:application name="kirito-demo-application"/>

    <kirito:service name="kirito-demo-service"/>

</beans>
複製程式碼

使用 Spring 去載入它,並驗證 IOC 容器中是否存在註冊成功的 Bean。

@SpringBootApplication
@ImportResource(locations = {"classpath:kirito.xml"})
public class XmlSchemaAuthoringSampleApplication {

   public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(XmlSchemaAuthoringSampleApplication.class, args);
        ServiceBean serviceBean = applicationContext.getBean(ServiceBean.class);
        System.out.println(serviceBean.getName());
        ApplicationConfig applicationConfig = applicationContext.getBean(ApplicationConfig.class);
        System.out.println(applicationConfig.getName());
    }
}
複製程式碼

觀察控制檯的輸出:

kirito-demo-service kirito-demo-application

一個基礎的基於 XML schema 的擴充套件便完成了。

Dubbo中的XML schema擴充套件

最後我們以 Dubbo 為例,看看一個成熟的 XML schema 擴充套件是如何被應用的。

Dubbo中的應用

剛好對應了四個標準的擴充套件步驟,是不是對 XML 配置下的 Dubbo 應用有了更好的理解了呢?

順帶一提,僅僅完成 Bean 的註冊還是不夠的,在“註冊”的同時,Dubbo 還進行了一系列其他操作如:暴露埠,開啟伺服器,完成註冊中心的註冊,生成代理物件等等行為,由於不在本文的範圍內,後續的 Dubbo 專題會專門介紹這些細節,本文便是瞭解 Dubbo 載入流程的前置文章了。

歡迎關注我的微信公眾號:「Kirito的技術分享」,關於文章的任何疑問都會得到回覆,帶來更多 Java 相關的技術分享。

關注微信公眾號

相關文章