好久沒有寫關於 Spring 的文章了,最近在系統梳理 Dubbo 代碼的過程當中發現了 XML schema 這個被遺漏的知識點。因爲工做中使用 SpringBoot 比較多的緣由,幾乎不多接觸 XML,此文能夠算作是亡羊補牢,另外一方面,也爲後續的 Dubbo 源碼解析作個鋪墊。php
XML schema 擴展機制是啥?這並非一塊很大的知識點,翻閱一下 Spring 的文檔,我甚至沒找到一個貫穿上下文的詞來描述這個功能,XML Schema Authoring
是文檔中對應的標題,簡單來講:spring
Spring 爲基於 XML 構建的應用提供了一種擴展機制,用於定義和配置 Bean。 它容許使用者編寫自定義的 XML bean 解析器,並將解析器自己以及最終定義的 Bean 集成到 Spring IOC 容器中。服務器
Dubbo 依賴了 Spring,並提供了一套自定義的 XML 標籤,<dubbo:application>
,<dubbo:registry>
,<dubbo:protocol>
,<dubbo:service>
。做爲使用者,大多數人只須要關心這些參數如何配置,但不知道有沒有人好奇過,它們是如何加載進入 Spring 的 IOC 容器中被其餘組件使用的呢?這便牽扯出了今天的主題:Spring 對 XML schema 的擴展支持。微信
爲了搞懂 Spring 的 XML 擴展機制,最直接的方式即是實現一個自定義的擴展。實現的步驟也很是簡單,分爲四步:app
NamespaceHandler
的實現類BeanDefinitionParser
的實現 (關鍵步驟).咱們的目的即是想要實現一個 kirito XML schema
,咱們的項目中能夠自定義 kirito.xml,在其中會以 kirito 爲標籤來定義不一樣的類,並在最終的測試代碼中驗證這些聲明在 kirito.xml 的類是否被 Spring 成功加載。大概像這樣,是否是和 dubbo.xml 的格式很像呢?eclipse
有了明確的目標,咱們逐步開展本身的工做。ide
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 標籤的關鍵點。ui
② kirito.xsd 定義了兩個元素: application 和 service,出於簡單考慮,都只有一個 name 字段。this
schema 的意義在於它能夠和 eclipse/IDEA 這樣智能化的集成開發環境造成很好的搭配,在編輯 XML 的過程當中,用戶能夠得到告警和提示。 若是配置得當,可使用自動完成功能讓用戶在事先定義好的枚舉類型中進行選擇。
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 便可。
在文章開始咱們便標記到 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 容器的映射。
最後一步還須要通知 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 擴展是如何被應用的。
恰好對應了四個標準的擴展步驟,是否是對 XML 配置下的 Dubbo 應用有了更好的理解了呢?
順帶一提,僅僅完成 Bean 的註冊仍是不夠的,在「註冊」的同時,Dubbo 還進行了一系列其餘操做如:暴露端口,開啓服務器,完成註冊中心的註冊,生成代理對象等等行爲,因爲不在本文的範圍內,後續的 Dubbo 專題會專門介紹這些細節,本文即是瞭解 Dubbo 加載流程的前置文章了。
歡迎關注個人微信公衆號:「Kirito的技術分享」,關於文章的任何疑問都會獲得回覆,帶來更多 Java 相關的技術分享。