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


看了這篇,自定義 Spring 標籤不是夢~

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

咱們知道,Spring 源碼的核心模塊是 Spring-coreSpring-beans,在此基礎上衍生出其餘模塊,例如 contextcachetx 等模塊,都是根據這兩個基礎模塊進行擴展的。java

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

做爲一個有追求的程序員,固然不能知足於框架自帶默認的標籤,爲了擴展性和配置化要求,這時候就須要學習自定義標籤和使用自定義標籤~git


官方例子

先來看一張源碼圖片(紅框框圈着是重點喲)程序員

myname_default_define

剛纔說了緩存和事務,那就拿這兩個舉例,還有一個標籤 <myname:>(這個我也不太清楚,網上查的資料也很少,因此按照個人理解你們跟說下)github

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

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

中間的是緩存定義的 xsd 文件,經過 <xsd:element name="annotation-driven"> 定義元素,<xsd:complexType> 區間內定義屬性列表,<xsd:attribute> 定義單個屬性,詳細分析能夠看下注釋~bash

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

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

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


自定義標籤使用

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

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

剛開始看到這些流程時,我仍是有點慌的,畢竟從一個使用默認標籤的萌新小白,忽然要我本身定義,感受到很新鮮,因此請各位跟着下面的流程一塊兒來看吧~


定義普通的 POJO 組件

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

public class Product {

	private Integer productId;

	private String unit;

	private String name;
}
複製代碼

定義 XSD 描述文件

custom-product.xsd

<xsd:schema targetNamespace="http://vip-augus.github.io/schema/product" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
	<!-- 註釋 3.4 自定義元素 -->
	<xsd:element name="product">
		<xsd:complexType>
            <!-- 這個是類註冊時的名字,組件中請不要佔用該字段~ -->
			<xsd:attribute name="id" type="xsd:string"/>
			<!-- 屬性定義列表,名字和類型 -->
			<xsd:attribute name="productId" type="xsd:integer"/>
			<xsd:attribute name="unit" type="xsd:string"/>
			<xsd:attribute name="name" type="xsd:string"/>
		</xsd:complexType>
	</xsd:element>
</xsd:schema>
複製代碼

我在上面的描述文件中,定義了一個新的 targetNamespace,同時定義了一個 叫 product 的新元素,而且將組件中的屬性都列在 <xsd:attribute> 中。XSD 文件是 XML DTD 的替代者,具體就很少深刻,感興趣的同窗能夠繼續深刻了解。


定義組件解析器

base.label.custom.ProductBeanDefinitionParser

public class ProductBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

	@Override
	protected Class getBeanClass(Element element) {
		// 返回對應的類型
		return Product.class;
	}

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

		String productId = element.getAttribute("productId");
		String productName = element.getAttribute("name");
		String productUnit = element.getAttribute("unit");
		// 將提取到的數據放入 BeanDefinitionBuilder 中,等到完成全部 bean 的解析以後統一註冊到 beanFactory 中
		if (productId != null) {
			// element.getAttribute("") 方法取出來的都是 string 類型,使用時記得手動轉換
			builder.addPropertyValue("productId", Integer.valueOf(productId));
		}
		if (StringUtils.hasText(productName)) {
			builder.addPropertyValue("name", productName);
		}
		if (StringUtils.hasText(productUnit)) {
			builder.addPropertyValue("unit", productUnit);
		}
	}
}
複製代碼

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


建立處理類的註冊器

base.label.custom.ProductBeanHandler

public class ProductBeanHandler extends NamespaceHandlerSupport {

	@Override
	public void init() {
		// 將組件解析器進行註冊到 `Spring` 容器
		registerBeanDefinitionParser("product", new ProductBeanDefinitionParser());
	}
}
複製代碼

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


編寫 spring.hanldersspring.schemas 文件

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

spring.handlers

http\://vip-augus.github.io/schema/product=base.label.custom.ProductBeanHandler
複製代碼

spring.schemas

http\://vip-augus.github.io/schema/product.xsd=custom/custom-product.xsd
複製代碼

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


使用 Demo

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!-- 注意 schema 位置,最後兩行是我新增的自定義配置 -->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:myname="http://vip-augus.github.io/schema/product" xsi:schemaLocation="http://www.springframework.org/schema/beans http://vip-augus.github.io/schema/product http://vip-augus.github.io/schema/product.xsd">

	<!-- 自定義標籤使用 -->
	<myname:product id="product" productId="1" name="Apple" unit="臺"/>
</beans>
複製代碼

測試代碼

public class ProductBootstrap {
	public static void main(String[] args) {
		ApplicationContext context = new ClassPathXmlApplicationContext("custom/custom-label.xml");
		Product product = (Product) context.getBean("product");
		// 輸出 Product{, productId ='1', unit='臺', name='Apple'}
		System.out.println(product.toString());
	}
}
複製代碼

小結

如今來回顧一下,Spring 遇到自定義標籤是,加載自定義的大體流程:

  • 定位 spring.hanldersspring.schemas:在兩個文件中找到對應的 handlerXSD,默認位置在 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)

public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
		// 註釋 3.8 ① 找到命名空間
		String namespaceUri = getNamespaceURI(ele);
		// ② 根據命名空間找到對應的 NamespaceHandler
		NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
		// ③ 調用自定義的 NamespaceHandler 進行解析
		return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
	}
複製代碼

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


① 獲取標籤的命名空間

public String getNamespaceURI(Node node) {
		return node.getNamespaceURI();
	}
複製代碼

這個方法具體作的事情很簡單,並且傳參的類型 org.w3c.dom.Node,已經提供了現成的方法,因此咱們只須要調用便可。


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

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

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

public NamespaceHandler resolve(String namespaceUri) {
	// 註釋 3.9 獲取全部已經配置的 handler 映射
	Map<String, Object> handlerMappings = getHandlerMappings();
	// 從 map 中取出命名空間對應的 NamespaceHandler 的 className
	// 這個映射 map 值,沒有的話,會進行實例化類,而後放入 map,等下次一樣命名空間進來就能直接使用了
	Object handlerOrClassName = handlerMappings.get(namespaceUri);
	if (handlerOrClassName == null) {
		return null;
	}
	else if (handlerOrClassName instanceof NamespaceHandler) {
		return (NamespaceHandler) handlerOrClassName;
	}
	else {
		String className = (String) handlerOrClassName;
		
		Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
		if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
			throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
					"] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
		}
		// 實例化類
		NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
		// 調用 handler 的 init() 方法
		namespaceHandler.init();
		// 放入 handler 映射中
		handlerMappings.put(namespaceUri, namespaceHandler);
		return namespaceHandler;
	}
}
複製代碼

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

private Map<String, Object> getHandlerMappings() {
	Map<String, Object> handlerMappings = this.handlerMappings;
	// 若是沒有緩存,進行緩存加載,公共變量,加鎖進行操做,細節好評👍
	if (handlerMappings == null) {
		synchronized (this) {
			handlerMappings = this.handlerMappings;
			if (handlerMappings == null) {
				Properties mappings =
						PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
				handlerMappings = new ConcurrentHashMap<>(mappings.size());
				CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
				this.handlerMappings = handlerMappings;
			}
		}
	}
	return handlerMappings;
}
複製代碼

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


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

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

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

public BeanDefinition parse(Element element, ParserContext parserContext) {
		// 尋找解析器並進行解析操做
		BeanDefinitionParser parser = findParserForElement(element, parserContext);
		// 真正解析調用調用的方法
		return (parser != null ? parser.parse(element, parserContext) : null);
	}
複製代碼

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

第二步纔是解析器進行解析的方法,咱們的解析器擴展的是 AbstractSingleBeanDefinitionParser因此實際是調用了咱們解析器父類的父類 AbstractBeanDefinitionParserparse 方法:

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

public final BeanDefinition parse(Element element, ParserContext parserContext) {
		// 註釋 3.10 實際自定義標籤解析器調用的方法,在 parseInternal 方法中,調用了咱們重載的方法
		AbstractBeanDefinition definition = parseInternal(element, parserContext);
    ...
    return definition;
}
複製代碼

解析關鍵方法

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

protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
	BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
	String parentName = getParentName(element);
	if (parentName != null) {
		builder.getRawBeanDefinition().setParentName(parentName);
	}
	Class<?> beanClass = getBeanClass(element);
	if (beanClass != null) {
		builder.getRawBeanDefinition().setBeanClass(beanClass);
	}
	else {
		String beanClassName = getBeanClassName(element);
		if (beanClassName != null) {
			builder.getRawBeanDefinition().setBeanClassName(beanClassName);
		}
	}
	builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
	BeanDefinition containingBd = parserContext.getContainingBeanDefinition();
	if (containingBd != null) {
		// Inner bean definition must receive same scope as containing bean.
		builder.setScope(containingBd.getScope());
	}
	if (parserContext.isDefaultLazyInit()) {
		// Default-lazy-init applies to custom bean definitions as well.
		builder.setLazyInit(true);
	}
	// 註釋 3.11 在這裏調用了咱們寫的解析方法
	doParse(element, parserContext, builder);
	return builder.getBeanDefinition();
}
複製代碼

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

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

到這裏爲止,咱們自定義標籤的解析就完成了~


總結

在咱們自定義標籤時,是否是感受使用起來很簡單,只需定義幾個文件,而後在自定義解析器中寫上業務處理邏輯,而後就能使用。

在咱們分析完整個解析流程,就能看到,Spring 在背後默默幫咱們完成了不少事情,相似默認標籤解析過程,根據命名空間找到對應的處理器,而後再找到解析器,在解析器裏面調用咱們個性化的處理邏輯。

這兩篇文章填了默認標籤和自定義標籤解析的坑,也完整的介紹了 Springbean 從配置中加載到內存中的全過程,下一篇開始分析解析類的加載~

因爲我的技術有限,若是有理解不到位或者錯誤的地方,請留下評論,我會根據朋友們的建議進行修正

spring-analysis-note 碼雲 Gitee 地址

spring-analysis-note Github 地址


參考資料

  1. Spring自定義標籤使用及原理

  2. Spring 源碼深度解析 / 郝佳編著. -- 北京 : 人民郵電出版社


傳送門:

相關文章
相關標籤/搜索