Spring 源碼學習(二)默認標籤解析

`Spring` 解析默認標籤~

[toc]php


從上一篇筆記能夠看出,在容器註冊 bean 信息的時候,作了不少解析操做,而 xml 文件中包含了不少標籤、屬性,例如 beanimport 標籤, metalook-upreplace等子元素屬性。java

上一篇主要介紹 Spring 容器的基礎結構,沒有細說這些標籤是如何解析的。node

因此本篇是來進行補坑的,介紹這些標籤在代碼中是如何識別和解析的~git

本篇筆記的結構大體以下:github

  • 介紹概念
  • 展現 demo 代碼,如何使用
  • 結合源碼分析
  • 聊聊天和思考

再次說下,下載項目看完整註釋,跟着源碼一塊兒分析~spring

碼雲 Gitee 地址設計模式

Github 地址mvc


Spring 中,標籤有兩種,默認和自定義框架

  • 默認標籤 這是咱們最常使用到的標籤類型了,像咱們一開始寫的 <bean id="book" class="domain.SimpleBook"/>,它屬於默認標籤,除了這個標籤外,還有其它四種標籤(importaliasbeanbeansdom

  • 自定義標籤 自定義標籤的用途,是爲了給系統提供可配置化支持,例如事務標籤 <tx:annotation-driven />,它是 Spring 的自定義標籤,經過繼承 NamespaceHandler 來完成自定義命名空間的解析。

先看源碼是如何區分這二者:

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
	if (delegate.isDefaultNamespace(root)) {
		// 註釋 1.12 遍歷 doc 中的節點列表
		NodeList nl = root.getChildNodes();
		for (int i = 0; i < nl.getLength(); i++) {
			Node node = nl.item(i);
			if (node instanceof Element) {
				Element ele = (Element) node;
				if (delegate.isDefaultNamespace(ele)) {
					// 註釋 1.13 識別出默認標籤的 bean 註冊
					// 根據元素名稱,調用不一樣的加載方法,註冊 bean
					parseDefaultElement(ele, delegate);
				}
				else {
					delegate.parseCustomElement(ele);
				}
			}
		}
	}
	else {
		delegate.parseCustomElement(root);
	}
}
複製代碼

能夠看到,在代碼中,關鍵方法是 delegate.isDefaultNamespace(ele) 進行判斷,識別掃描到的元素屬於哪一種標籤。

找到命名空間 NamespaceURI 變量,若是是 http://www.springframework.org/schema/beans,表示它是默認標籤,而後進行默認標籤的元素解析,否者使用自定義標籤解析。

本篇筆記主要記錄的是默認標籤的解析,下來開始正式介紹~


默認標籤解析

parseDefaultElement 方法用來解析默認標籤,跟蹤下去,發現對四種標籤作了不一樣的處理,其中 bean 標籤的解析最爲艱難(對比其它三種),因此咱們將 bean 標籤解析吃透的話,其它三種標籤的解析也能更好的熟悉。

入口方法:

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
	// 註釋 2.1 默認標籤解析
	if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
		// 解析 import 標籤
		importBeanDefinitionResource(ele);
	}
	else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
		// 解析 alias 標籤
		processAliasRegistration(ele);
	}
	else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
		// 解析 bean 標籤的方法
		processBeanDefinition(ele, delegate);
	}
	else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
		// recurse
		// 解析 beans 標籤,其實就是遞歸,從新對這個 element 下的標籤進行註冊解析
		doRegisterBeanDefinitions(ele);
	}
}
複製代碼

Bean 標籤解析入口

定位到上面第三個方法 processBeanDefinition(ele, delegate)

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
		// 註釋 1.15 解析 bean 名稱的元素
		BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
		if (bdHolder != null) {
			bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
			try {
				// Register the final decorated instance. (註釋 1.16 註冊最後修飾後的實例)
				BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
			}
			catch (BeanDefinitionStoreException ex) {
				getReaderContext().error("Failed to register bean definition with name '" +
						bdHolder.getBeanName() + "'", ele, ex);
			}
			// Send registration event. 通知相關的監聽器,表示這個 bean 已經加載完成
			getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
		}
	}
複製代碼

上一篇筆記只是簡單描述這個方法的功能:將 xml 中配置的屬性對應到 document 對象中,而後進行註冊,下面來完整描述這個方法的處理流程:

  • 建立實例 bdHolder:首先委託 BeanDefinitionParserDelegate 類的 parseBeanDefinitionElement 方法進行元素解析,通過解析後,bdHolder 實例已經包含剛纔咱們在配置文件中設定的各類屬性,例如 classidnamealias等屬性。
  • 對實例 bdHolder 進行裝飾:在這個步驟中,實際上是掃描默認標籤下的自定義標籤,對這些自定義標籤進行元素解析,設定自定義屬性。
  • 註冊 bdHolder 信息:解析完成了,須要往容器的 beanDefinitionMap 註冊表註冊 bean 信息,註冊操做委託給了 BeanDefinitionReaderUtils.registerBeanDefinition,經過工具類完成信息註冊。
  • 發送通知事件:通知相關監聽器,表示這個 bean 已經加載完成

看到這裏,同窗們應該能看出,Spring 源碼的接口和方法設計都很簡潔,上層接口描述了該方法要作的事情,而後分解成多個小方法,在小方法中進行邏輯處理,方法能夠被複用。

因此看源碼除了能瞭解到框架的實現邏輯,更好的去使用和定位問題,還可以學習到大佬們寫代碼時的設計模式,融入本身的工做或者學習中~


Bean 標籤其它屬性的解析過程

在上篇筆記中,已經總結了對屬性 idname 的解析,再也不贅述,下面講下對標籤其它屬性的解析~

首先貼下源碼:

org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parseBeanDefinitionElement(org.w3c.dom.Element, java.lang.String, org.springframework.beans.factory.config.BeanDefinition)

public AbstractBeanDefinition parseBeanDefinitionElement( Element ele, String beanName, @Nullable BeanDefinition containingBean) {
	this.parseState.push(new BeanEntry(beanName));
	String className = null;
	// 註釋 2.3 解析 class 屬性
	if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
		className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
	}
	String parent = null;
	// 解析 parent 屬性
	if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
		parent = ele.getAttribute(PARENT_ATTRIBUTE);
	}

    ...
	// 建立 GenericBeanDefinition
	AbstractBeanDefinition bd = createBeanDefinition(className, parent);
	// 解析默認 bean 的各類屬性
	parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
	// 提取描述 desc
	bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
	// 解析 meta 屬性
	parseMetaElements(ele, bd);
	// 解析 lookup-method 屬性
	parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
	// 解析 replace-method 屬性
	parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
	// 解析構造函數
	parseConstructorArgElements(ele, bd);
	// 解析 property 子元素
	parsePropertyElements(ele, bd);
	// 解析 qualifier 子元素
	parseQualifierElements(ele, bd);

	bd.setResource(this.readerContext.getResource());
	bd.setSource(extractSource(ele));

	return bd;
	...
}
複製代碼

這是一個完整的屬性解析過程,包含了 metalookup-methodreplace-mthod等其它屬性解析。

雖然不經常使用到,但你們多學一個屬性,到時遇到適合使用的場景就能進行使用,還有遇到這些屬性的問題也不用慌張,我會先講有什麼用,還有如何使用,讓你們有個印象~


建立 GenericBeanDefinition

關於 GenericBeanDefinition 的繼承體系上一篇已經講過了,因此這裏再簡單解釋一下這個方法的用途:

createBeanDefinition(className, parent);

從方法名字就能看出,它的用途是建立一個 beanDefinition ,用於承載屬性的實例。

在最後一步實例化 GenericBeanDefinition 時,還會判斷類加載器是非存在。若是存在的話,使用類加載器所在的 jvm 來加載類對象,不然只是簡單記錄一下 className


解析默認 bean 的各類屬性

parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);

這個方法解析的代碼實現有點多,因此感興趣的同窗,能夠在我上傳的代碼庫中全局搜索找到該方法,裏面有對它每一個方法用途介紹~

簡單描述的話,這個方法是用來解析 <bean> 標籤中每個基礎屬性,列表以下:

  • scope
  • abstract
  • lazy-init
  • autowire
  • depends-on
  • autowire-candidate
  • primary
  • init-method
  • destroy-method
  • factory-method
  • factory-bean

能夠清晰看到, Spring 完成了對全部 bean 屬性的解析,有些常用到,例如 autowire 自動織入、init-method 定義初始化調用哪一個方法。而有些的話,就須要同窗們本身深刻學習瞭解~


解析 meta 屬性

先講下 meta 屬性的使用(汗,在沒了解前,基本沒使用該屬性=-=)

<bean id="book" class="domain.SimpleBook">
	<!-- 元標籤 -->
	<meta key="test_key" value="test_value"/>
</bean>
複製代碼

這個元屬性不會體如今對象的屬性中,而是一個額外的聲明,在 parseMetaElements(ele, bd); 方法中進行獲取,具體實現是 element 對象的 getAttribute(key),將設定的元屬性放入 BeanMetadataAttributeAccessor 對象中

由於代碼比較簡單,因此經過圖片進行說明:

最終屬性值是以 key-value 形式保存在鏈表中 Map<String, Object> attributes,以後使用只須要根據 key 值就能獲取到 value 。想到以後在代碼設計上,爲了擴展性,也能夠進行 key-value 形式存儲和使用。


解析 lookup-method 屬性

這個屬性也是不經常使用,引用書中的描述

一般將它成爲獲取器注入。獲取器注入是一個特殊的方法注入,它是把一個方法聲明爲返回某種類型的 bean,但實際要返回的 bean 是在配置文件裏面配置的,次方法可用在設計有些可插拔的功能上,解除程序依賴。

代碼寫的有點多,我貼張圖片,介紹一下關鍵信息:

首先我定義了一個基礎對象 BaseBook 和兩個繼承對象 SimpleBookComplexBook,還新建一個抽象類,而且設定了一個方法 getDomain,返回類型是基礎對象。

我以爲是由於抽象類沒法被實例化,必需要有具體實現類,因此在這個時候,Spring 容器要加載 AbstractGetBookTest 對象,能夠用到 <lookup method> 屬性,經過注入特定實現類,來完成類的加載。

config.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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">


	<bean id="getBookTest" class="base.label.parsing.AbstractGetBookTest">
		<!-- 註釋 2.6 loop-up 屬性🌰 -->
		<!-- 獲取器注入 name 表示方法,bean 表示要注入的類-->
		<lookup-method name="getDomain" bean="complexBook"/>
	</bean>

	<bean id="book" class="domain.SimpleBook">
		<!-- 元標籤 -->
		<meta key="test_key" value="test_value"/>
	</bean>

	<bean id="complexBook" class="domain.ComplexBook"/>

</beans>
複製代碼

Spring 會對 bean 指定的 class作動態代理,識別中 name 屬性所指定的方法,返回 bean 屬性指定的 bean 實例對象。

既然叫作獲取器注入,咱們能夠將 bean="complexBook" 替換一下,換成 bean="simpleBook",這樣注入的類就變成了 SimpleBook 對象了,這樣只須要修改配置文件就能更換類的注入~

而後代碼對 <lookup-method> 解析跟元屬性的解析很相近,因此閱讀起來也很容易噢


解析 replaced-method 屬性

這個方法的用途:能夠在運行時用新的方法替換現有的方法。不只能夠動態地替換返回實體 bean,還能動態地更改原有方法的邏輯。

簡單來講,就是將某個類定義的方法,在運行時替換成另外一個方法,例如明明看到代碼中調用的是 A 方法,但實際運行的倒是 B 方法。

從圖片中看出,輸出框打印出我替換後的文案,實現起來也不難,替換者須要實現 org.springframework.beans.factory.support.MethodReplacer 接口,而後重寫 reimplement 方法,關鍵點在配置文件的 <replaced-method> 屬性:

<bean id="beforeMethodReplaced" class="base.label.parsing.BeforeMethodReplaced">
	<!-- 註釋 2.7 方法替換 -->
	<replaced-method name="printDefaultName" replacer="testMethodReplaced"/>
</bean>
複製代碼

一樣的,Spring 會識別這個 replaced-method 元素中的 name 屬性所指定的方法,替換成指定 bean 實例對象的 reimplement 方法。

代碼解析過程當中,將識別到的屬性保存到 MethodOverridesSet<MethodOverride> overrides 中,最終將會記錄在 AbstractBeanDefinitionmethodOverrides中。

我的並不推薦這種使用方法,若是常規工做中,業務驅動比較強烈的狀況,若是這樣寫,會致使別人誤解這個方法的意圖,若是想要調用查詢方法,卻被動態代理,調用了刪除方法,那就致使沒必要要的 BUG(還好我沒遇到哈哈哈)。


解析 constructor-arg 屬性

解析構造函數這個屬性是很經常使用的,但同時它的解析也很複雜,下面貼一個實例配置:

<bean id="testConstructorArg" class="base.label.parsing.TestConstructorArg">
	<!-- 這裏展現一個構造函數的狀況下,若是有兩個以上,解析會更復雜 -->
	<constructor-arg index="0" value="JingQ"/>
	<constructor-arg index="1" value="23"/>
</bean>
複製代碼

這個配置所實現的功能很簡單,爲 TestConstructorArg 自動尋找對應的構造函數,而後根據下標 index 爲對應的屬性注入 value,實現構造函數。

具體解析在這個方法中:

/** * 註釋 2.8 解析 構造函數 子元素 * Parse constructor-arg sub-elements of the given bean element. */
public void parseConstructorArgElements(Element beanEle, BeanDefinition bd) {
	NodeList nl = beanEle.getChildNodes();
	for (int i = 0; i < nl.getLength(); i++) {
		Node node = nl.item(i);
		if (isCandidateElement(node) && nodeNameEquals(node, CONSTRUCTOR_ARG_ELEMENT)) {
		    // 循環解析 constructor-arg 屬性
			parseConstructorArgElement((Element) node, bd);
		}
	}
}
複製代碼

代碼太多也不貼出來啦,感興趣的同窗定位到我寫註釋的地方詳細看下吧~

下面來梳理下解析構造函數代碼的流程:

① 配置中指定了 index 屬性

  • 解析 constructor-arg 的子元素
  • 使用 ConstructorArgumentValues.ValueHolder(value) 類型來封裝解析出來的元素(包含type name index 屬性)
  • addIndexedArgumentValue 方法,將解析後的 value 添加到當前 BeanDefinitionConstructorArgumentValuesindexedArgumentValues 屬性中

① 配置中沒有指定了 index 屬性

  • 解析 constructor-arg 的子元素
  • 使用 ConstructorArgumentValues.ValueHolder(value) 類型來封裝解析出來的元素(包含type name index 屬性)
  • addGenericArgumentValue 方法,將解析後的 value 添加到當前 BeanDefinitionConstructorArgumentValuesgenericArgumentValues 屬性中

這兩個流程區別點在於,最後解析到的屬性信息保存的位置不一樣,指定下標狀況下,保存到 indexedArgumentValues 屬性,沒有指定下標狀況下,將會保存到 genericArgumentValues

能夠看到,這兩段代碼處理上,第一步和第二部實際上是同樣的邏輯,存在重複代碼的狀況,我剛學習和工做時,爲了求快,也有不少這種重複類型的代碼。

在慢慢學習更多知識和設計模式後,回頭看以前寫的代碼,都有種刪掉重寫的衝動,因此若是若是在一開始寫的時候,就抽出相同處理代碼的邏輯,而後進行代碼複用,減小代碼重複率,讓代碼更好看一些,這樣就之後就不用被別人和本身吐槽了Σ(o゚д゚oノ)

ref value 屬性的處理比較簡單,因此你們看代碼就能瞭解它是如何解析的,比較難的是子元素處理,例以下面的例子:

<constructor-arg>
    <map>
        <entry key="key" value="value" />
    </map>
</constructor-arg>
複製代碼

具體解析子元素的方法是:org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parsePropertySubElement(org.w3c.dom.Element, org.springframework.beans.factory.config.BeanDefinition, java.lang.String)

這個方法主要對各類子元素進行解析,包括 idref value array set map 等等子元素的機械,這裏不細說,同窗們感興趣繼續去跟蹤吧~


解析 property 屬性

在配件文件中的使用方式:

<!-- property 解析 -->
<bean id="testPropertyParseElement" class="base.label.parsing.TestPropertyParseElement">
	<property name="id" value="1"/>
	<property name="name" value="JingQ"/>
</bean>
複製代碼

這個解析入口方法跟解析構造函數 constructor-arg 的入口方法很像,代碼以下:

/** * 註釋 2.10 解析 property 屬性 * Parse property sub-elements of the given bean element. */
public void parsePropertyElements(Element beanEle, BeanDefinition bd) {
	NodeList nl = beanEle.getChildNodes();
	for (int i = 0; i < nl.getLength(); i++) {
		Node node = nl.item(i);
		if (isCandidateElement(node) && nodeNameEquals(node, PROPERTY_ELEMENT)) {
			// 循環解析 property 屬性
			parsePropertyElement((Element) node, bd);
		}
	}
}
複製代碼

這個入口方法提取到 property 全部子元素,而後調用 parsePropertyElement 方法進行處理,最後使用 PropertyValue 進行封裝,最後記錄在 BeanDefinition 中的 propertyValues 屬性中。

經歷過上面複雜屬性的解析,property 屬性的解析就顯得比較簡單,都是同樣的套路,循環遍歷元素進行解析,因此熟悉前面的解析邏輯後,看後面的代碼就能更快理解~


解析 qualifer 屬性

你們更熟悉的應該是 @qualifer 標籤吧,它跟 qualifer 屬性的用途同樣。

在使用 Spring 框架進行類注入的時候,匹配的候選 bean 數目必須有且只有一個,若是找不到一個匹配的 bean 時,容器就會拋出 BeanCreationException 異常。

例如咱們定義了一個抽象類 AbstractBook,有兩個具體實現類 Book1Book2,若是使用代碼:

@Autowired
private AbstractBook book;
複製代碼

這樣運行時就會拋出剛纔說的錯誤異常,咱們有兩種方式來消除歧義:

① 在配置文件中設定 quailfer

經過 qualifier 指定注入 bean 的名稱

<bean id="testBean" class="base.TestBean">
    <qualifer type="org.Springframeword.beans.factory.annotation.Quailfier" value="book1"/>
</bean>
複製代碼

② 使用 @Qualifier("beanNeame")

@Qualifier("book1")
private AbstractBook book;
複製代碼

一樣的,代碼的解析過程跟前面的套路相近,留給同窗們本身去分析吧~


總結

咱們來回顧一下通用解析流程:

  • 判斷元素類型:在每一個入口方法中,都有個判斷方法 nodeNameEquals(node, XXXX_METHOD_ELEMENT),符合類型的才進行解析
  • 解析:解析一個子元素時,大多數狀況下看到是 key-value 形式的屬性對,經過 ele.getAttribute(NAME_ATTRIBUTE) 等形式進行獲取
  • 存儲:將上一步解析的結果存儲 beanDefinition 對應屬性中

這樣一看,是否是感受清晰一點了,對於源碼的分析也沒這麼懼怕了。

此次終於補了前一篇筆記的小坑,介紹了默認標籤的解析流程,下一篇筆記介紹一下自定義標籤的解析吧,下一篇再會~


參考資料

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

傳送門:

相關文章
相關標籤/搜索