[toc]php
從上一篇筆記能夠看出,在容器註冊 bean
信息的時候,作了不少解析操做,而 xml
文件中包含了不少標籤、屬性,例如 bean
、 import
標籤, meta
、look-up
和 replace
等子元素屬性。java
上一篇主要介紹 Spring
容器的基礎結構,沒有細說這些標籤是如何解析的。node
因此本篇是來進行補坑的,介紹這些標籤在代碼中是如何識別和解析的~git
本篇筆記的結構大體以下:github
demo
代碼,如何使用再次說下,下載項目看完整註釋,跟着源碼一塊兒分析~spring
碼雲 Gitee 地址設計模式
Github 地址mvc
在 Spring
中,標籤有兩種,默認和自定義:框架
默認標籤 這是咱們最常使用到的標籤類型了,像咱們一開始寫的 <bean id="book" class="domain.SimpleBook"/>
,它屬於默認標籤,除了這個標籤外,還有其它四種標籤(import
、 alias
、 bean
、 beans
)dom
自定義標籤 自定義標籤的用途,是爲了給系統提供可配置化支持,例如事務標籤 <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);
}
}
複製代碼
定位到上面第三個方法 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
實例已經包含剛纔咱們在配置文件中設定的各類屬性,例如 class
、 id
、 name
、 alias
等屬性。bdHolder
進行裝飾:在這個步驟中,實際上是掃描默認標籤下的自定義標籤,對這些自定義標籤進行元素解析,設定自定義屬性。bdHolder
信息:解析完成了,須要往容器的 beanDefinitionMap
註冊表註冊 bean
信息,註冊操做委託給了 BeanDefinitionReaderUtils.registerBeanDefinition
,經過工具類完成信息註冊。bean
已經加載完成看到這裏,同窗們應該能看出,Spring
源碼的接口和方法設計都很簡潔,上層接口描述了該方法要作的事情,而後分解成多個小方法,在小方法中進行邏輯處理,方法能夠被複用。
因此看源碼除了能瞭解到框架的實現邏輯,更好的去使用和定位問題,還可以學習到大佬們寫代碼時的設計模式,融入本身的工做或者學習中~
在上篇筆記中,已經總結了對屬性 id
和 name
的解析,再也不贅述,下面講下對標籤其它屬性的解析~
首先貼下源碼:
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;
...
}
複製代碼
這是一個完整的屬性解析過程,包含了 meta
、lookup-method
、replace-mthod
等其它屬性解析。
雖然不經常使用到,但你們多學一個屬性,到時遇到適合使用的場景就能進行使用,還有遇到這些屬性的問題也不用慌張,我會先講有什麼用,還有如何使用,讓你們有個印象~
關於 GenericBeanDefinition
的繼承體系上一篇已經講過了,因此這裏再簡單解釋一下這個方法的用途:
createBeanDefinition(className, parent);
從方法名字就能看出,它的用途是建立一個 beanDefinition
,用於承載屬性的實例。
在最後一步實例化 GenericBeanDefinition
時,還會判斷類加載器是非存在。若是存在的話,使用類加載器所在的 jvm
來加載類對象,不然只是簡單記錄一下 className
。
parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
這個方法解析的代碼實現有點多,因此感興趣的同窗,能夠在我上傳的代碼庫中全局搜索找到該方法,裏面有對它每一個方法用途介紹~
簡單描述的話,這個方法是用來解析 <bean>
標籤中每個基礎屬性,列表以下:
能夠清晰看到, Spring
完成了對全部 bean
屬性的解析,有些常用到,例如 autowire
自動織入、init-method
定義初始化調用哪一個方法。而有些的話,就須要同窗們本身深刻學習瞭解~
先講下 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
形式存儲和使用。
這個屬性也是不經常使用,引用書中的描述
一般將它成爲獲取器注入。獲取器注入是一個特殊的方法注入,它是把一個方法聲明爲返回某種類型的
bean
,但實際要返回的bean
是在配置文件裏面配置的,次方法可用在設計有些可插拔的功能上,解除程序依賴。
代碼寫的有點多,我貼張圖片,介紹一下關鍵信息:
首先我定義了一個基礎對象 BaseBook
和兩個繼承對象 SimpleBook
、 ComplexBook
,還新建一個抽象類,而且設定了一個方法 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>
解析跟元屬性的解析很相近,因此閱讀起來也很容易噢
這個方法的用途:能夠在運行時用新的方法替換現有的方法。不只能夠動態地替換返回實體 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
方法。
代碼解析過程當中,將識別到的屬性保存到 MethodOverrides
的 Set<MethodOverride> overrides
中,最終將會記錄在 AbstractBeanDefinition
的 methodOverrides
中。
我的並不推薦這種使用方法,若是常規工做中,業務驅動比較強烈的狀況,若是這樣寫,會致使別人誤解這個方法的意圖,若是想要調用查詢方法,卻被動態代理,調用了刪除方法,那就致使沒必要要的 BUG
(還好我沒遇到哈哈哈)。
解析構造函數這個屬性是很經常使用的,但同時它的解析也很複雜,下面貼一個實例配置:
<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
添加到當前 BeanDefinition
的 ConstructorArgumentValues
的 indexedArgumentValues
屬性中① 配置中沒有指定了 index
屬性
constructor-arg
的子元素ConstructorArgumentValues.ValueHolder(value)
類型來封裝解析出來的元素(包含type
name
index
屬性)addGenericArgumentValue
方法,將解析後的 value
添加到當前 BeanDefinition
的 ConstructorArgumentValues
的 genericArgumentValues
屬性中這兩個流程區別點在於,最後解析到的屬性信息保存的位置不一樣,指定下標狀況下,保存到 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 解析 -->
<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
屬性的用途同樣。
在使用 Spring
框架進行類注入的時候,匹配的候選 bean
數目必須有且只有一個,若是找不到一個匹配的 bean
時,容器就會拋出 BeanCreationException
異常。
例如咱們定義了一個抽象類 AbstractBook
,有兩個具體實現類 Book1
和 Book2
,若是使用代碼:
@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
對應屬性中這樣一看,是否是感受清晰一點了,對於源碼的分析也沒這麼懼怕了。
此次終於補了前一篇筆記的小坑,介紹了默認標籤的解析流程,下一篇筆記介紹一下自定義標籤的解析吧,下一篇再會~