01.22 Spring 源碼學習(三)-自定義標籤

又來填坑啦,上一篇講完默認標籤的解析,這篇筆記記錄一下自定義標籤的解析吧。

我們知道,Spring 源碼的核心模塊是 Spring-core 和 Spring-beans,在此基礎上衍生出其他模塊,例如 context、 cache、 tx 等模塊,都是根據這兩個基礎模塊進行擴展的。

聰明如你,應該想到我們代碼中常用的緩存註解 @Cacheable、事務註解 @Transaction,還有阿里巴巴的 RPC 中間件 Dubbo,在配置文件中通過 <service> 或者 <reference> 進行服務註冊和訂閱,這些都都屬於 Spring 的自定義標籤的實現,通過自定義標籤可以實現更加強大的功能!

作為一個有追求的程序員,當然不能滿足於框架自帶默認的標籤,為了擴展性和配置化要求,這時候就需要學習自定義標籤和使用自定義標籤~


Table of Contents generated with DocToc

  • 官方例子
  • 自定義標籤使用定義普通的 POJO 組件定義 XSD 描述文件定義組件解析器創建處理類的註冊器編寫 spring.hanlders 和 spring.schemas 文件使用 Demo配置文件測試代碼小結
  • 自定義標籤解析① 獲取標籤的命名空間② 根據命名空間找到對應的 NamespaceHandler③ 調用自定義的 NamespaceHandler 進行解析
  • 總結
  • 參考資料

又來填坑啦,上一篇講完默認標籤的解析,這篇筆記記錄一下自定義標籤的解析吧。

我們知道,Spring 源碼的核心模塊是 Spring-core 和 Spring-beans,在此基礎上衍生出其他模塊,例如 context、 cache、 tx 等模塊,都是根據這兩個基礎模塊進行擴展的。

聰明如你,應該想到我們代碼中常用的緩存註解 @Cacheable、事務註解 @Transaction,還有阿里巴巴的 RPC 中間件 Dubbo,在配置文件中通過 <service> 或者 <reference> 進行服務註冊和訂閱,這些都都屬於 Spring 的自定義標籤的實現,通過自定義標籤可以實現更加強大的功能!

作為一個有追求的程序員,當然不能滿足於框架自帶默認的標籤,為了擴展性和配置化要求,這時候就需要學習自定義標籤和使用自定義標籤~


官方例子

先來看一張源碼圖片(紅框框圈著是重點喲)


Spring 源碼學習(三)-自定義標籤


剛才說了緩存和事務,那就拿這兩個舉例,還有一個標籤 <myname>(這個我也不太清楚,網上查的資料也不多,所以按照我的理解大家跟說下)/<myname>

首先我們看到, <cache> 和 <myname> 都是自定義標籤,左一是配置文件,進行 bean 的定義,頂部的 xmlns 是命名空間,表示標籤所屬的定義文件,像事務、緩存、MVC 的命名空間都是固定的。/<myname>/<cache>

而 myname 相當於萬金油,既可以定義為事務,又可以定義為緩存,只要我們在命名空間中進行相應的定義就能正確的識別。這個就是我們待會要使用到的自定義標籤,通過命名空間定位到我們想要的處理邏輯。

中間的是緩存定義的 xsd 文件,通過 <element> 定義元素,<complextype> 區間內定義屬性列表,<attribute> 定義單個屬性,詳細分析可以看下注釋~/<attribute>/<complextype>/<element>

右邊的是事務定義的 xsd 文件,大體內容的跟中間一樣,雖然元素名稱 <annotation-driven> 有相同的,但是下面的屬性定義是有所區別的。/<annotation-driven>

所以我們對自定義註解有個大概的瞭解,xsd 描述文件是個其中一個關鍵,在配置文件頂部的命名空間是標籤進行解析時,進行定位的配置,當然還有處理器,下面使用時進行介紹。

不知道理解的對不對,如果有誤的話請大佬們指出,我會進行修改的!


自定義標籤使用

Spring 提供了可擴展的 Schema 的支持,擴展 Spring 自定義標籤配置需要以下幾個步驟:

  • 創建一個需要擴展的組件
  • 定義一個 XSD 描述文件
  • 創建一個文件,實現 BeanDefinitionParse 接口,用來解析 XSD 文件中的定義和組件定義。
  • 創建一個 Handler 文件,擴展自 NamespaceHandlerSupport,將組件註冊到 Spring 容器
  • 編寫 Spring.handlers 和 Spring.schemas 文件

剛開始看到這些流程時,我還是有點慌的,畢竟從一個使用默認標籤的萌新小白,突然要我自己定義,感覺到很新鮮,所以請各位跟著下面的流程一起來看吧~


定義普通的 POJO 組件

這個沒啥好說的,就是一個普通的類:

<code>public class Product {

\tprivate Integer productId;

\tprivate String unit;

\tprivate String name;
}
/<code>

定義 XSD 描述文件

custom-product.xsd


<code><schema>\t\t\txmlns:xsd="http://www.w3.org/2001/XMLSchema"
\t\t\telementFormDefault="qualified">
\t
\t<element>
\t\t<complextype>

\t\t\t<attribute>
\t\t\t
\t\t\t<attribute>
\t\t\t<attribute>
\t\t\t<attribute>
\t\t/<complextype>
\t/<element>
/<schema>
/<code>

我在上面的描述文件中,定義了一個新的 targetNamespace,同時定義了一個 叫 product 的新元素,並且將組件中的屬性都列在 <attribute> 中。XSD 文件是 XML DTD 的替代者,具體就不多深入,感興趣的同學可以繼續深入瞭解。/<attribute>


定義組件解析器

base.label.custom.ProductBeanDefinitionParser


<code>public class ProductBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

\t@Override
\tprotected Class getBeanClass(Element element) {
\t\t// 返回對應的類型
\t\treturn Product.class;
\t}

\t// 從 element 中解析並提取對應的元素
\t@Override
\tprotected void doParse(Element element, BeanDefinitionBuilder builder) {

\t\tString productId = element.getAttribute("productId");
\t\tString productName = element.getAttribute("name");
\t\tString productUnit = element.getAttribute("unit");
\t\t// 將提取到的數據放入 BeanDefinitionBuilder 中,等到完成所有 bean 的解析之後統一註冊到 beanFactory 中
\t\tif (productId != null) {
\t\t\t// element.getAttribute("") 方法取出來的都是 string 類型,使用時記得手動轉換
\t\t\tbuilder.addPropertyValue("productId", Integer.valueOf(productId));
\t\t}
\t\tif (StringUtils.hasText(productName)) {
\t\t\tbuilder.addPropertyValue("name", productName);
\t\t}
\t\tif (StringUtils.hasText(productUnit)) {
\t\t\tbuilder.addPropertyValue("unit", productUnit);

\t\t}
\t}
}
/<code>

關鍵點在於,我們的解析器是繼承於 AbstractSingleBeanDefinitionParser,重載了兩個方法,詳細用途請看註釋~


創建處理類的註冊器

base.label.custom.ProductBeanHandler


<code>public class ProductBeanHandler extends NamespaceHandlerSupport {

\t@Override
\tpublic void init() {
\t\t// 將組件解析器進行註冊到 `Spring` 容器
\t\tregisterBeanDefinitionParser("product", new ProductBeanDefinitionParser());
\t}
}
/<code>

這個類也比較簡單,關鍵是繼承了 NamespaceHandlerSupport,對他進行了擴展,在該類初始化時將組件解析器進行註冊到 Spring 容器中。


編寫 spring.hanlders 和 spring.schemas 文件

我將文件位置放在 resources -> META-INF 目錄下:

spring.handlers

1http\\://vip-augus.github.io/schema/product=base.label.custom.ProductBeanHandler

spring.schemas

1http\\://vip-augus.github.io/schema/product.xsd=custom/custom-product.xsd

到了這一步,自定義的配置就結束了。下面是如何使用


使用 Demo

配置文件


<code>

<beans>\t xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
\t xmlns:myname="http://vip-augus.github.io/schema/product"
\t xsi:schemaLocation="http://www.springframework.org/schema/beans
\t http://vip-augus.github.io/schema/product
\t http://vip-augus.github.io/schema/product.xsd">

\t
\t<product>
/<beans>
/<code>

測試代碼

<code>public class ProductBootstrap {
\tpublic static void main(String[] args) {
\t\tApplicationContext context = new ClassPathXmlApplicationContext("custom/custom-label.xml");
\t\tProduct product = (Product) context.getBean("product");
\t\t// 輸出 Product{, productId ='1', unit='臺', name='Apple'}
\t\tSystem.out.println(product.toString());
\t}
}
/<code>

小結

現在來回顧一下,Spring 遇到自定義標籤是,加載自定義的大致流程:

  • 定位 spring.hanlders 和 spring.schemas:在兩個文件中找到對應的 handler 和 XSD,默認位置在 resources -> META-INF。
  • Handler 註冊 Parser:擴展了 NamespaceHandlerSupport 的類,在初始化註冊解析器
  • 運行解析器 Parser:擴展了 AbstractSingleBeanDefinitionParser,通過重載方法進行屬性解析,完成解析。

上面已經將自定義註解的使用講了,接下來講的是源碼中如何對自定義標籤進行解析。


自定義標籤解析

在上一篇筆記中,講了如何解析默認標籤,Spring 判斷一個標籤不是默認標籤的話,就會將這個標籤解析交給自定義標籤的解析方法

直接定位到解析自定義標籤的方法吧:

org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parseCustomElement(org.w3c.dom.Element, org.springframework.beans.factory.config.BeanDefinition)


<code>public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
\t\t// 註釋 3.8 ① 找到命名空間

\t\tString namespaceUri = getNamespaceURI(ele);
\t\t// ② 根據命名空間找到對應的 NamespaceHandler
\t\tNamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
\t\t// ③ 調用自定義的 NamespaceHandler 進行解析
\t\treturn handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
\t}
/<code>

看著流程是不是覺得很熟悉,我們剛才在自定義標籤使用時,定義的文件順序是一樣的,下面來講下這三個方法,具體代碼不會貼太多,主要記錄一些關鍵方法和流程,詳細代碼和流程請下載我上傳的工程~


① 獲取標籤的命名空間


<code>public String getNamespaceURI(Node node) {
\t\treturn node.getNamespaceURI();
\t}
/<code>

這個方法具體做的事情很簡單,而且傳參的類型 org.w3c.dom.Node,已經提供了現成的方法,所以我們只需要調用即可。


② 根據命名空間找到對應的 NamespaceHandler

具體解析方法這這個類中:

org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver#resolve


<code>public NamespaceHandler resolve(String namespaceUri) {
\t// 註釋 3.9 獲取所有已經配置的 handler 映射
\tMap<string> handlerMappings = getHandlerMappings();
\t// 從 map 中取出命名空間對應的 NamespaceHandler 的 className
\t// 這個映射 map 值,沒有的話,會進行實例化類,然後放入 map,等下次同樣命名空間進來就能直接使用了
\tObject handlerOrClassName = handlerMappings.get(namespaceUri);
\tif (handlerOrClassName == null) {
\t\treturn null;
\t}
\telse if (handlerOrClassName instanceof NamespaceHandler) {
\t\treturn (NamespaceHandler) handlerOrClassName;
\t}
\telse {
\t\tString className = (String) handlerOrClassName;
\t\t
\t\tClass> handlerClass = ClassUtils.forName(className, this.classLoader);
\t\tif (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
\t\t\tthrow new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
\t\t\t\t\t"] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
\t\t}
\t\t// 實例化類
\t\tNamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
\t\t// 調用 handler 的 init() 方法
\t\tnamespaceHandler.init();
\t\t// 放入 handler 映射中
\t\thandlerMappings.put(namespaceUri, namespaceHandler);
\t\treturn namespaceHandler;
\t}
}
/<string>/<code>

找對應的 NamespaceHandler,關鍵方法在於 getHandlerMappings():

<code>private Map<string> getHandlerMappings() {
\tMap<string> handlerMappings = this.handlerMappings;
\t// 如果沒有緩存,進行緩存加載,公共變量,加鎖進行操作,細節好評
\tif (handlerMappings == null) {

\t\tsynchronized (this) {
\t\t\thandlerMappings = this.handlerMappings;
\t\t\tif (handlerMappings == null) {
\t\t\t\tProperties mappings =
\t\t\t\t\t\tPropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
\t\t\t\thandlerMappings = new ConcurrentHashMap<>(mappings.size());
\t\t\t\tCollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
\t\t\t\tthis.handlerMappings = handlerMappings;
\t\t\t}
\t\t}
\t}
\treturn handlerMappings;
}
/<string>/<string>/<code>

所以我們能看到,找 Handler 時,使用的策略是延遲加載,在 map 緩存中找到了直接返回,沒找到對應的 Handler,將處理器實例化,執行 init() 方法,接著將 Handler 放入 map 緩存中,等待下一個使用。


③ 調用自定義的 NamespaceHandler 進行解析

回憶一下,我們在自定義標籤解析的時候,是沒有重載 parse() 方法,所以定位進去,看到實際調用方法是這兩行:

org.springframework.beans.factory.xml.NamespaceHandlerSupport#parse


<code>public BeanDefinition parse(Element element, ParserContext parserContext) {
\t\t// 尋找解析器並進行解析操作
\t\tBeanDefinitionParser parser = findParserForElement(element, parserContext);
\t\t// 真正解析調用調用的方法
\t\treturn (parser != null ? parser.parse(element, parserContext) : null);
\t}
/<code>

第一步獲取解析器,就是我們之前在 init() 方法中,註冊到 Spring 容器的解析器。

第二步才是解析器進行解析的方法,我們的解析器擴展的是 AbstractSingleBeanDefinitionParser,所以實際是調用了我們解析器父類的父類 AbstractBeanDefinitionParser 的 parse 方法:

org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#parse


<code>public final BeanDefinition parse(Element element, ParserContext parserContext) {
\t\t// 註釋 3.10 實際自定義標籤解析器調用的方法,在 parseInternal 方法中,調用了我們重載的方法
\t\tAbstractBeanDefinition definition = parseInternal(element, parserContext);
...
return definition;
}
/<code>

解析關鍵方法

org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#parseInternal


<code>protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
\tBeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
\tString parentName = getParentName(element);
\tif (parentName != null) {
\t\tbuilder.getRawBeanDefinition().setParentName(parentName);
\t}
\tClass> beanClass = getBeanClass(element);
\tif (beanClass != null) {
\t\tbuilder.getRawBeanDefinition().setBeanClass(beanClass);
\t}
\telse {
\t\tString beanClassName = getBeanClassName(element);
\t\tif (beanClassName != null) {

\t\t\tbuilder.getRawBeanDefinition().setBeanClassName(beanClassName);
\t\t}
\t}
\tbuilder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
\tBeanDefinition containingBd = parserContext.getContainingBeanDefinition();
\tif (containingBd != null) {
\t\t// Inner bean definition must receive same scope as containing bean.
\t\tbuilder.setScope(containingBd.getScope());
\t}
\tif (parserContext.isDefaultLazyInit()) {
\t\t// Default-lazy-init applies to custom bean definitions as well.
\t\tbuilder.setLazyInit(true);
\t}
\t// 註釋 3.11 在這裡調用了我們寫的解析方法
\tdoParse(element, parserContext, builder);
\treturn builder.getBeanDefinition();
}
/<code>

這裡我要倒著講,在第二步解析時,不是直接調用了自定義的 doParse 方法,而是進行了一系列的數據準備,包括了 beanClass、 class、 lazyInit 等屬性的準備。

第一步解析,在我省略的代碼中,是將第二步解析後的結果進行包裝,從 AbstractBeanDefinition 轉換成 BeanDefinitionHolder ,然後進行註冊。轉換和註冊流程在第一篇筆記已經介紹過了,不再贅述。

到這裡為止,我們自定義標籤的解析就完成了~


分享到:


相關文章: