spring 系列 轉(zhuǎn)載自掘金 VipAugus https://juejin.cn/user/2348212565601415/posts
Spring 解析默認(rèn)標(biāo)簽~
[toc]
從上一篇筆記可以看出,在容器注冊 bean 信息的時候,做了很多解析操作,而 xml 文件中包含了很多標(biāo)簽、屬性,例如 bean 、 import 標(biāo)簽, meta 、look-up 和 replace等子元素屬性。
上一篇主要介紹 Spring 容器的基礎(chǔ)結(jié)構(gòu),沒有細(xì)說這些標(biāo)簽是如何解析的。
所以本篇是來進(jìn)行補坑的,介紹這些標(biāo)簽在代碼中是如何識別和解析的~
本篇筆記的結(jié)構(gòu)大致如下:
- 介紹概念
- 展示
demo代碼,如何使用 - 結(jié)合源碼分析
- 聊聊天和思考
再次說下,下載項目看完整注釋,跟著源碼一起分析~
在 Spring 中,標(biāo)簽有兩種,默認(rèn)和自定義:
-
默認(rèn)標(biāo)簽 這是我們最常使用到的標(biāo)簽類型了,像我們一開始寫的
<bean id="book" class="domain.SimpleBook"/>,它屬于默認(rèn)標(biāo)簽,除了這個標(biāo)簽外,還有其它四種標(biāo)簽(import、alias、bean、beans) -
自定義標(biāo)簽 自定義標(biāo)簽的用途,是為了給系統(tǒng)提供可配置化支持,例如事務(wù)標(biāo)簽
<tx:annotation-driven />,它是Spring的自定義標(biāo)簽,通過繼承NamespaceHandler來完成自定義命名空間的解析。
先看源碼是如何區(qū)分這兩者:
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
// 注釋 1.12 遍歷 doc 中的節(jié)點列表
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 識別出默認(rèn)標(biāo)簽的 bean 注冊
// 根據(jù)元素名稱,調(diào)用不同的加載方法,注冊 bean
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
復(fù)制代碼
可以看到,在代碼中,關(guān)鍵方法是 delegate.isDefaultNamespace(ele) 進(jìn)行判斷,識別掃描到的元素屬于哪種標(biāo)簽。
找到命名空間 NamespaceURI 變量,如果是 http://www.springframework.org/schema/beans,表示它是默認(rèn)標(biāo)簽,然后進(jìn)行默認(rèn)標(biāo)簽的元素解析,否者使用自定義標(biāo)簽解析。
本篇筆記主要記錄的是默認(rèn)標(biāo)簽的解析,下來開始正式介紹~
默認(rèn)標(biāo)簽解析
parseDefaultElement 方法用來解析默認(rèn)標(biāo)簽,跟蹤下去,發(fā)現(xiàn)對四種標(biāo)簽做了不同的處理,其中 bean 標(biāo)簽的解析最為艱難(對比其它三種),所以我們將 bean 標(biāo)簽解析吃透的話,其它三種標(biāo)簽的解析也能更好的熟悉。
入口方法:
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
// 注釋 2.1 默認(rèn)標(biāo)簽解析
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
// 解析 import 標(biāo)簽
importBeanDefinitionResource(ele);
}
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
// 解析 alias 標(biāo)簽
processAliasRegistration(ele);
}
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
// 解析 bean 標(biāo)簽的方法
processBeanDefinition(ele, delegate);
}
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
// 解析 beans 標(biāo)簽,其實就是遞歸,重新對這個 element 下的標(biāo)簽進(jìn)行注冊解析
doRegisterBeanDefinitions(ele);
}
}
復(fù)制代碼
Bean 標(biāo)簽解析入口
定位到上面第三個方法 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. 通知相關(guān)的監(jiān)聽器,表示這個 bean 已經(jīng)加載完成
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}
復(fù)制代碼
上一篇筆記只是簡單描述這個方法的功能:將 xml 中配置的屬性對應(yīng)到 document 對象中,然后進(jìn)行注冊,下面來完整描述這個方法的處理流程:
-
創(chuàng)建實例 bdHolder:首先委托
BeanDefinitionParserDelegate類的parseBeanDefinitionElement方法進(jìn)行元素解析,經(jīng)過解析后,bdHolder實例已經(jīng)包含剛才我們在配置文件中設(shè)定的各種屬性,例如class、id、name、alias等屬性。 - 對實例 bdHolder 進(jìn)行裝飾:在這個步驟中,其實是掃描默認(rèn)標(biāo)簽下的自定義標(biāo)簽,對這些自定義標(biāo)簽進(jìn)行元素解析,設(shè)定自定義屬性。
-
注冊 bdHolder 信息:解析完成了,需要往容器的
beanDefinitionMap注冊表注冊bean信息,注冊操作委托給了BeanDefinitionReaderUtils.registerBeanDefinition,通過工具類完成信息注冊。 -
發(fā)送通知事件:通知相關(guān)監(jiān)聽器,表示這個
bean已經(jīng)加載完成
看到這里,同學(xué)們應(yīng)該能看出,Spring 源碼的接口和方法設(shè)計都很簡潔,上層接口描述了該方法要做的事情,然后分解成多個小方法,在小方法中進(jìn)行邏輯處理,方法可以被復(fù)用。
所以看源碼除了能了解到框架的實現(xiàn)邏輯,更好的去使用和定位問題,還能夠?qū)W習(xí)到大佬們寫代碼時的設(shè)計模式,融入自己的工作或者學(xué)習(xí)中~
Bean 標(biāo)簽其它屬性的解析過程
在上篇筆記中,已經(jīng)總結(jié)了對屬性 id 和 name 的解析,不再贅述,下面講下對標(biāo)簽其它屬性的解析~
首先貼下源碼:
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);
}
...
// 創(chuàng)建 GenericBeanDefinition
AbstractBeanDefinition bd = createBeanDefinition(className, parent);
// 解析默認(rèn) 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());
// 解析構(gòu)造函數(shù)
parseConstructorArgElements(ele, bd);
// 解析 property 子元素
parsePropertyElements(ele, bd);
// 解析 qualifier 子元素
parseQualifierElements(ele, bd);
bd.setResource(this.readerContext.getResource());
bd.setSource(extractSource(ele));
return bd;
...
}
復(fù)制代碼
這是一個完整的屬性解析過程,包含了 meta、lookup-method、replace-mthod等其它屬性解析。
雖然不常用到,但大家多學(xué)一個屬性,到時遇到適合使用的場景就能進(jìn)行使用,還有遇到這些屬性的問題也不用慌張,我會先講有什么用,還有如何使用,讓大家有個印象~
創(chuàng)建 GenericBeanDefinition
關(guān)于 GenericBeanDefinition 的繼承體系上一篇已經(jīng)講過了,所以這里再簡單解釋一下這個方法的用途:
createBeanDefinition(className, parent);
從方法名字就能看出,它的用途是創(chuàng)建一個 beanDefinition ,用于承載屬性的實例。
在最后一步實例化 GenericBeanDefinition 時,還會判斷類加載器是非存在。如果存在的話,使用類加載器所在的 jvm 來加載類對象,否則只是簡單記錄一下 className。
解析默認(rèn) bean 的各種屬性
parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
這個方法解析的代碼實現(xiàn)有點多,所以感興趣的同學(xué),可以在我上傳的代碼庫中全局搜索找到該方法,里面有對它每個方法用途介紹~
簡單描述的話,這個方法是用來解析 <bean> 標(biāo)簽中每一個基礎(chǔ)屬性,列表如下:
- scope
- abstract
- lazy-init
- autowire
- depends-on
- autowire-candidate
- primary
- init-method
- destroy-method
- factory-method
- factory-bean
可以清晰看到, Spring 完成了對所有 bean 屬性的解析,有些經(jīng)常使用到,例如 autowire 自動織入、init-method 定義初始化調(diào)用哪個方法。而有些的話,就需要同學(xué)們自己深入學(xué)習(xí)了解~
解析 meta 屬性
先講下 meta 屬性的使用(汗,在沒了解前,基本沒使用該屬性=-=)
<bean id="book" class="domain.SimpleBook">
<!-- 元標(biāo)簽 -->
<meta key="test_key" value="test_value"/>
</bean>
復(fù)制代碼
這個元屬性不會體現(xiàn)在對象的屬性中,而是一個額外的聲明,在 parseMetaElements(ele, bd); 方法中進(jìn)行獲取,具體實現(xiàn)是 element 對象的 getAttribute(key),將設(shè)定的元屬性放入 BeanMetadataAttributeAccessor 對象中
因為代碼比較簡單,所以通過圖片進(jìn)行說明:
最終屬性值是以 key-value 形式保存在鏈表中 Map<String, Object> attributes,之后使用只需要根據(jù) key 值就能獲取到 value 。想到之后在代碼設(shè)計上,為了擴展性,也可以進(jìn)行 key-value 形式存儲和使用。
解析 lookup-method 屬性
這個屬性也是不常用,引用書中的描述
通常將它成為獲取器注入。獲取器注入是一個特殊的方法注入,它是把一個方法聲明為返回某種類型的
bean,但實際要返回的bean是在配置文件里面配置的,次方法可用在設(shè)計有些可插拔的功能上,解除程序依賴。
代碼寫的有點多,我貼張圖片,介紹一下關(guān)鍵信息:
首先我定義了一個基礎(chǔ)對象 BaseBook 和兩個繼承對象 SimpleBook、 ComplexBook,還新建一個抽象類,并且設(shè)定了一個方法 getDomain,返回類型是基礎(chǔ)對象。
我覺得是因為抽象類無法被實例化,必須要有具體實現(xiàn)類,所以在這個時候,Spring 容器要加載 AbstractGetBookTest 對象,可以用到 <lookup method> 屬性,通過注入特定實現(xiàn)類,來完成類的加載。
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">
<!-- 元標(biāo)簽 -->
<meta key="test_key" value="test_value"/>
</bean>
<bean id="complexBook" class="domain.ComplexBook"/>
</beans>
復(fù)制代碼
Spring 會對 bean 指定的 class做動態(tài)代理,識別中 name 屬性所指定的方法,返回 bean 屬性指定的 bean 實例對象。
既然叫做獲取器注入,我們可以將 bean="complexBook" 替換一下,換成 bean="simpleBook",這樣注入的類就變成了 SimpleBook 對象了,這樣只需要修改配置文件就能更換類的注入~
然后代碼對 <lookup-method> 解析跟元屬性的解析很相近,所以閱讀起來也很容易噢
解析 replaced-method 屬性
這個方法的用途:可以在運行時用新的方法替換現(xiàn)有的方法。不僅可以動態(tài)地替換返回實體 bean,還能動態(tài)地更改原有方法的邏輯。
簡單來說,就是將某個類定義的方法,在運行時替換成另一個方法,例如明明看到代碼中調(diào)用的是 A 方法,但實際運行的卻是 B 方法。
從圖片中看出,輸出框打印出我替換后的文案,實現(xiàn)起來也不難,替換者需要實現(xiàn) org.springframework.beans.factory.support.MethodReplacer 接口,然后重寫 reimplement 方法,關(guān)鍵點在配置文件的 <replaced-method> 屬性:
<bean id="beforeMethodReplaced" class="base.label.parsing.BeforeMethodReplaced">
<!-- 注釋 2.7 方法替換 -->
<replaced-method name="printDefaultName" replacer="testMethodReplaced"/>
</bean>
復(fù)制代碼
同樣的,Spring 會識別這個 replaced-method 元素中的 name 屬性所指定的方法,替換成指定 bean 實例對象的 reimplement 方法。
代碼解析過程中,將識別到的屬性保存到 MethodOverrides 的 Set<MethodOverride> overrides 中,最終將會記錄在 AbstractBeanDefinition 的 methodOverrides中。
個人并不推薦這種使用方法,如果常規(guī)工作中,業(yè)務(wù)驅(qū)動比較強烈的情況,如果這樣寫,會導(dǎo)致別人誤解這個方法的意圖,如果想要調(diào)用查詢方法,卻被動態(tài)代理,調(diào)用了刪除方法,那就導(dǎo)致不必要的 BUG(還好我沒遇到哈哈哈)。
解析 constructor-arg 屬性
解析構(gòu)造函數(shù)這個屬性是很常用的,但同時它的解析也很復(fù)雜,下面貼一個實例配置:
<bean id="testConstructorArg" class="base.label.parsing.TestConstructorArg">
<!-- 這里展示一個構(gòu)造函數(shù)的情況下,如果有兩個以上,解析會更復(fù)雜 -->
<constructor-arg index="0" value="JingQ"/>
<constructor-arg index="1" value="23"/>
</bean>
復(fù)制代碼
這個配置所實現(xiàn)的功能很簡單,為 TestConstructorArg 自動尋找對應(yīng)的構(gòu)造函數(shù),然后根據(jù)下標(biāo) index 為對應(yīng)的屬性注入 value,實現(xiàn)構(gòu)造函數(shù)。
具體解析在這個方法中:
/**
* 注釋 2.8 解析 構(gòu)造函數(shù) 子元素
* 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)) {
// 循環(huán)解析 constructor-arg 屬性
parseConstructorArgElement((Element) node, bd);
}
}
}
復(fù)制代碼
代碼太多也不貼出來啦,感興趣的同學(xué)定位到我寫注釋的地方詳細(xì)看下吧~
下面來梳理下解析構(gòu)造函數(shù)代碼的流程:
① 配置中指定了 index 屬性
- 解析
constructor-arg的子元素 - 使用
ConstructorArgumentValues.ValueHolder(value)類型來封裝解析出來的元素(包含typenameindex屬性) -
addIndexedArgumentValue方法,將解析后的value添加到當(dāng)前BeanDefinition的ConstructorArgumentValues的indexedArgumentValues屬性中
① 配置中沒有指定了 index 屬性
- 解析
constructor-arg的子元素 - 使用
ConstructorArgumentValues.ValueHolder(value)類型來封裝解析出來的元素(包含typenameindex屬性) -
addGenericArgumentValue方法,將解析后的value添加到當(dāng)前BeanDefinition的ConstructorArgumentValues的genericArgumentValues屬性中
這兩個流程區(qū)別點在于,最后解析到的屬性信息保存的位置不同,指定下標(biāo)情況下,保存到 indexedArgumentValues 屬性,沒有指定下標(biāo)情況下,將會保存到 genericArgumentValues。
可以看到,這兩段代碼處理上,第一步和第二部其實是一樣的邏輯,存在重復(fù)代碼的情況,我剛學(xué)習(xí)和工作時,為了求快,也有很多這種重復(fù)類型的代碼。
在慢慢學(xué)習(xí)更多知識和設(shè)計模式后,回頭看之前寫的代碼,都有種刪掉重寫的沖動,所以如果如果在一開始寫的時候,就抽出相同處理代碼的邏輯,然后進(jìn)行代碼復(fù)用,減少代碼重復(fù)率,讓代碼更好看一些,這樣就以后就不用被別人和自己吐槽了Σ(o?д?o?)
ref value 屬性的處理比較簡單,所以大家看代碼就能了解它是如何解析的,比較難的是子元素處理,例如下面的例子:
<constructor-arg>
<map>
<entry key="key" value="value" />
</map>
</constructor-arg>
復(fù)制代碼
具體解析子元素的方法是:org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parsePropertySubElement(org.w3c.dom.Element, org.springframework.beans.factory.config.BeanDefinition, java.lang.String)
這個方法主要對各種子元素進(jìn)行解析,包括 idref value array set map 等等子元素的機械,這里不細(xì)說,同學(xué)們感興趣繼續(xù)去跟蹤吧~
解析 property 屬性
在配件文件中的使用方式:
<!-- property 解析 -->
<bean id="testPropertyParseElement" class="base.label.parsing.TestPropertyParseElement">
<property name="id" value="1"/>
<property name="name" value="JingQ"/>
</bean>
復(fù)制代碼
這個解析入口方法跟解析構(gòu)造函數(shù) 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)) {
// 循環(huán)解析 property 屬性
parsePropertyElement((Element) node, bd);
}
}
}
復(fù)制代碼
這個入口方法提取到 property 所有子元素,然后調(diào)用 parsePropertyElement 方法進(jìn)行處理,最后使用 PropertyValue 進(jìn)行封裝,最后記錄在 BeanDefinition 中的 propertyValues 屬性中。
經(jīng)歷過上面復(fù)雜屬性的解析,property 屬性的解析就顯得比較簡單,都是一樣的套路,循環(huán)遍歷元素進(jìn)行解析,所以熟悉前面的解析邏輯后,看后面的代碼就能更快理解~
解析 qualifer 屬性
大家更熟悉的應(yīng)該是 @qualifer 標(biāo)簽吧,它跟 qualifer 屬性的用途一樣。
在使用 Spring 框架進(jìn)行類注入的時候,匹配的候選 bean 數(shù)目必須有且只有一個,如果找不到一個匹配的 bean 時,容器就會拋出 BeanCreationException 異常。
例如我們定義了一個抽象類 AbstractBook,有兩個具體實現(xiàn)類 Book1 和 Book2,如果使用代碼:
@Autowired
private AbstractBook book;
復(fù)制代碼
這樣運行時就會拋出剛才說的錯誤異常,我們有兩種方式來消除歧義:
① 在配置文件中設(shè)定 quailfer
通過 qualifier 指定注入 bean 的名稱
<bean id="testBean" class="base.TestBean">
<qualifer type="org.Springframeword.beans.factory.annotation.Quailfier" value="book1"/>
</bean>
復(fù)制代碼
② 使用 @Qualifier("beanNeame")
@Qualifier("book1")
private AbstractBook book;
同樣的,代碼的解析過程跟前面的套路相近,留給同學(xué)們自己去分析吧~
總結(jié)
我們來回顧一下通用解析流程:
-
判斷元素類型:在每個入口方法中,都有個判斷方法
nodeNameEquals(node, XXXX_METHOD_ELEMENT),符合類型的才進(jìn)行解析 -
解析:解析一個子元素時,大多數(shù)情況下看到是
key-value形式的屬性對,通過ele.getAttribute(NAME_ATTRIBUTE)等形式進(jìn)行獲取 -
存儲:將上一步解析的結(jié)果存儲
beanDefinition對應(yīng)屬性中
這樣一看,是不是感覺清晰一點了,對于源碼的分析也沒這么害怕了。
這次終于補了前一篇筆記的小坑,介紹了默認(rèn)標(biāo)簽的解析流程,下一篇筆記介紹一下自定義標(biāo)簽的解析吧,下一篇再會~