一、概述
還記得我們在上一講末尾提到的關(guān)于默認標(biāo)簽解析和自定義標(biāo)簽解析吧。本講就來針對默認標(biāo)簽解析進行講解。為了便于銜接上一講的內(nèi)容,我們將源碼部分粘貼出來:

從上圖中的源碼中,我們可以看出默認標(biāo)簽的解析是在parseDefaultElement(ele, delegate)方法中實現(xiàn)的。我們來看一下這個方法如何實現(xiàn)的:

在parseDefaultElement(ele, delegate)方法中我們可以看到,它分別針對4種不同的標(biāo)簽(即:import標(biāo)簽、alias標(biāo)簽、bean標(biāo)簽和beans標(biāo)簽)做了解析操作。那么下面我們就通過下面的4部分內(nèi)容來對這些標(biāo)簽的解析進行深度剖析。
二、bean標(biāo)簽的解析
在上面的4種標(biāo)簽中,對bean標(biāo)簽的解析最為復(fù)雜和重要,所以我們先從這個標(biāo)簽開始深入分析,如果能夠理解它的解析過程,那么其他標(biāo)簽就不難理解了。我們廢話不多說,言歸正傳。先來看看processBeanDefinition(ele, delegate)方法內(nèi)部的具體實現(xiàn)邏輯:

bean標(biāo)簽的解析和注冊的時序圖如下所示:

2.1> parseBeanDefinitionElement(ele)
首先我們來看一下元素解析部分內(nèi)容,在parseBeanDefinitionElement(ele)方法中,但是這個方法其實只起到了“周轉(zhuǎn)”的作用,它的內(nèi)部其實又調(diào)用了parseBeanDefinitionElement(ele, null)方法,所以,我們看下一個方法的內(nèi)部實現(xiàn):

在 parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) 方法中,其實一共執(zhí)行了如下4個步驟(【注】但是下圖中只列出了其中的第1步驟和第2步驟,后面文章內(nèi)容,再給大家展示剩下的后兩個步驟)。
【步驟1】:提取Element元素中的“
id”和“name”屬性,并將name解析為aliases,然后為beanName賦值。
【步驟2】:解析其他屬性并封裝到GenericBeanDefinition類型的實例中。
【步驟3】:如果發(fā)現(xiàn)bean沒有指定beanName,那么使用默認規(guī)則生成beanName。
【步驟4】:將獲取到的信息封裝到GenericBeanDefinition類型的實例中。
2.1.1> 步驟1:解析beanName和aliases

其中,checkNameUniqueness(beanName, aliases, ele)是用于校驗beanName和aliases是否是唯一的,即:不允許出現(xiàn)重復(fù)的名字(name)。如果發(fā)現(xiàn)有重復(fù)的,則直接拋出異常。具體邏輯實現(xiàn),請見下圖源碼注釋:

2.1.2> 步驟2:解析其他屬性
對于其他屬性的解析,是在parseBeanDefinitionElement(ele, beanName, containingBean)方法中實現(xiàn)的,具體源碼如下所示:

a> 創(chuàng)建GenericBeanDefinition實例
創(chuàng)建BeanDefinition的這部分內(nèi)容,是通過調(diào)用createBeanDefinition(className, parent)方法實現(xiàn)的。但是,在介紹整個方法內(nèi)部邏輯之前,我們先來了解一下BeanDefinition,它到底是做什么用的?
BeanDefinition是配置文件中 <bean>元素標(biāo)簽在Spring容器中的表現(xiàn)形式 ,也就是說,它是用來承載bean信息的。在配置文件中可以定義父級<bean> 和 子集<bean> ,它們分別由RootBeanDefinition和ChildBeanDefinition表示。而如果沒有父級<bean>的話,則用RootBeanDefinition表示。而GenericBeanDefinition是從2.5版本之后加入進來的,用于為bean文件配置屬性屬性定義提供一站式服務(wù)。這三個類之間的關(guān)系如下圖所示:

了解了BeanDefinition的3個實現(xiàn)類之后,我們再來看一下createBeanDefinition(className, parent)方法的具體實現(xiàn):

上圖中的createBeanDefinition(...)方法內(nèi)部基本沒做什么,關(guān)鍵的內(nèi)容在紅框的BeanDefinitionReaderUtils.createBeanDefinition(...)方法調(diào)用上,下圖是createBeanDefinition(...)方法的源碼部分:

通過上圖源碼,我們可以看出來createBeanDefinition(...) 方法執(zhí)行了很簡單的 4步 操作:
【步驟1】創(chuàng)建
GenericBeanDefinition實例對象bd。
【步驟2】為bd設(shè)置parentName屬性。
【步驟3】為bd設(shè)置beanClass屬性。(如果classLoader不為空,則利用它去創(chuàng)建className對應(yīng)的Class實例對象)
【步驟4】為bd設(shè)置beanClassName屬性。(如果classLoader為空,則只需賦值className即可)
b> 解析bean的各種屬性
有了可以承載bean信息的GenericBeanDefinition實例對象之后,我們就來繼續(xù)往下分析,看看負責(zé) 解析bean中各個屬性 的邏輯代碼——parseBeanDefinitionAttributes(ele, beanName, containingBean, bd):需要補充的一點是,此時入?yún)ontainingBean等于null。
在分析該方法源碼之前,我們先來看如下兩個遍歷中的內(nèi)容:一個是入?yún)⒌?code>ele,另一個是全局變量defaults。在下面的源碼解析中,我們會經(jīng)?;剡^來參照這兩個參數(shù)所存儲的值。

針對parseBeanDefinitionAttributes(ele, beanName, containingBean, bd)方法的源碼,如下圖所示:


c> 解析元數(shù)據(jù)
下面我們在來看一下元數(shù)據(jù)解析方法——parseMetaElements(ele, bd);再介紹源碼之前,我們先來看一下Spring中的meta標(biāo)簽使用方式如下:

從上面的例子我們可以看出來,使用了
meta標(biāo)簽后,配置的desc并不會體現(xiàn)在Gun的屬性當(dāng)中,而只是一個額外的聲明。當(dāng)需要使用里面的信息的時候,可以通過BeanDefinition的getAttribute(key)方法進行獲取。
下面我們再來看一下parseMetaElements(ele, bd)的源碼部分:

d> 解析lookup-method屬性
我們平時對于lookup-method的使用其實是不多的,所以,我們在介紹關(guān)于lookup-method屬性解析之前,先了解一下它是怎么使用的。如下代碼所示,我們有一個抽象類Writer,它有一個寫作的方法write(),但是具體使用哪種類型的筆去寫作,則需要抽象方法getPen()來決定。那么關(guān)于筆的類型,我們提供了鉛筆(Pencil)和毛筆(Brush)這兩種。具體如下所示:

然后,我們通過在xml的配置文件中,使用lookup-method標(biāo)簽,將pencil的bean賦值給getPen()方法,那么運行結(jié)果顯示“Pencil print !”

然后,我們可以通過修改xml配置中的lookup-method標(biāo)簽,將原來的“pencil”替換為“brush”,再運行一下,那么運行結(jié)果顯示“Brush print !”

【總結(jié)】根據(jù)上面的演示,我們可以知道
lookup-method它的作用是獲取器注入。即:獲取器注入是一種特殊的方法注入,它是把一個方法聲明為返回某種類型的bean,但實際要返回的bean是在配置文件里面配置的,此方法可用在設(shè)計有些可插拔的功能上,解除程序依賴。
好了,講完了lookup-method的使用方法和作用之后,我們再來看一下parseLookupOverrideSubElements(ele, bd.getMethodOverrides())方法的源碼實現(xiàn):

【總結(jié)】
parseLookupOverrideSubElements(ele, bd.getMethodOverrides())方法的內(nèi)部邏輯跟我們解析元數(shù)據(jù)的方法parseMetaElements(ele, bd)非常類似。此次就不再贅述了。
e> 解析replaced-method屬性
在介紹對replaced-method屬性解析之前,我們還是來看一下它的使用場景吧。假設(shè)有個程序員Coder覺得工資又少,工作又多,非常煩躁。他打算去跟他們老板吼叫(shout)一番,表達自己內(nèi)心的不滿——“老板!我要離職!我不想寫代碼了!煩死了!”。具體實現(xiàn)如下所示:

但是在他馬上要到達老板辦公室門口的時候,接到了他女朋友的電話,電話那頭說:“我爸媽同意咱倆結(jié)婚了,但是有個前提,就是要買個樓房?!背绦騿T一想,自己買房的錢還沒湊夠呢!這不能離職啊!但是他此時已經(jīng)推開了領(lǐng)導(dǎo)辦公室的門。為了不喊出離職的那句話,我們可以采用MethodReplacer的方式改變shout方法的實現(xiàn)邏輯,從而讓程序員Coder說出:“老板!我最愛寫代碼了!我會一直忠于公司為您工作的!”這句話,具體實現(xiàn)如下所示:

【總結(jié)】
replaced-mothod可以實現(xiàn)方法替換,即:可以在運行時用新的方法替換現(xiàn)有的方法。
好了,理解了replaced-mothod標(biāo)簽的使用方式之后,我們來看一下parseReplacedMethodSubElements(ele, bd.getMethodOverrides())方法是如何對replaced-method屬性進行解析的。詳細解題源碼如下所示:

f> 解析構(gòu)造函數(shù)的參數(shù)
對于構(gòu)造函數(shù)的配置方式,請見如下所示:

【解釋】默認情況下是按照參數(shù)的順序注入的,當(dāng)指定index索引后,就可以改變注入?yún)?shù)的順序。
下面是parseConstructorArgElements(ele, bd)方法的源碼實現(xiàn):

我們可以在上面看到,方法內(nèi)部又調(diào)用了parseConstructorArgElement((Element) node, bd)方法,這里面才是真正解析邏輯的地方:



上面的代碼還是比較好理解的,但是關(guān)于parsePropertyValue(ele, bd, null)方法,我們還需要再看一下。源碼如下所示:



那么關(guān)于上圖藍色框所標(biāo)注的parsePropertySubElement(subElement, bd)方法,我們來看一下它的具體實現(xiàn):


通過上圖中源碼的注釋,可以看出逐一的對constructor-arg的子元素進行解析,針對每個子元素的解析此處不再進行講解。這些子元素都可以通過如下配置方式進行配置:

g> 解析property子元素
在方法parsePropertyElements(ele, bd)中,對property標(biāo)簽進行了解析。關(guān)于property標(biāo)簽的使用,如下所示:

那么解析property子元素的源碼如下所示:

【解釋】可以看到上面函數(shù)與構(gòu)造函數(shù)注入方式不同的是,返回值使用PropertyValue進行封裝,并記錄在了
BeanDefinition的propertyValues屬性里。
h> 解析qualifier子元素
當(dāng)同一類型的bean注入到IOC之后,Spring容器中匹配的候選Bean數(shù)目必須有且僅有一個,那么此時,我們可以通過Qualifier指出注入Bean的名稱,這樣其一就消除掉了,配置方式如下所示:

具體源碼實現(xiàn),如下圖所示:


2.2> decorateBeanDefinitionIfRequired(ele, bdHolder)
上面是針對parseBeanDefinitionElement(ele)方法進行的解析,下面我們要解析的是下圖中紅框標(biāo)注的方法:

當(dāng)Spring中的<bean>標(biāo)簽的子元素使用了自定義標(biāo)簽配置,則會被decorateBeanDefinitionIfRequired(ele, bdHolder)方法解析,如下所示:
<bean id="test" class="com.muse.Test">
<mybean:user username="muse">
</bean>
那么,下面我們來看一下decorateBeanDefinitionIfRequired(ele, bdHolder)方法的源碼實現(xiàn):

【解釋】上面調(diào)用
decorateBeanDefinitionIfRequired(ele, originalDef, null)方法的時候,第三個參數(shù)傳遞的是null,因為第三個參數(shù)是父類bean,當(dāng)堆某個嵌套配置進行分析時,這里需要傳遞父類的BeanDefinition。分析源碼得知這里傳遞的參數(shù)其實是為了使用父類的scope屬性,以備子類若是沒有設(shè)置scope時,默認使用父類的屬性,這里分析的是頂層配置,所以傳遞null。
在上面代碼中,我們看到無論是對所有屬性還是所有子節(jié)點,都會執(zhí)行decorateIfRequired(node, finalDefinition, containingBd)方法,那么我們再來看一下這個方法的內(nèi)部實現(xiàn):

其中,isDefaultNamespace(namespaceUri)是通過判斷 namespaceUri不為空,并且等于"http://www.springframework.org/schema/beans",如果都滿足,則是默認的命名空間。否則是自定義的命名空間。

通過調(diào)用readerContext.getNamespaceHandlerResolver(),我們可以獲得如下紅框中的 命名空間對應(yīng)的處理器(NamespaceHandler)。

其中關(guān)于自定義標(biāo)簽的解析過程,我們會在第3講部分介紹,此處就直接略過了。
2.3> registerBeanDefinition(...)
上面我們執(zhí)行完了對配置的解析和裝飾操作,那么下面就該到注冊階段了。涉及源碼部分如下圖所示:

在下面的代碼中,我們可以看到總共有兩個步驟的操作,分別是:注冊BeanDefinition 和 注冊別名Alias。

那么下面我們先來看一下注冊BeanDefinition的方法registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition())的處理邏輯:


【解釋】從上面的代碼中,我們可以看到針對bean的注冊處理方式上,主要進行了以下幾個步驟:
【步驟1】對AbstractBeanDefinition的校驗。在解析XML文件的時候我們提過校驗,但是此校驗非彼校驗,之前的校驗是針對XML格式的校驗,而此時的校驗是針對于AbstractBeanDefinition的methodOverrides屬性的。
【步驟2】對beanName已經(jīng)注冊的情況的處理。如果設(shè)置了不允許bean的覆蓋,則需要拋出異常,否則直接覆蓋。
【步驟3】加入map緩存。
【步驟4】清除解析之前留下的對應(yīng)beanName的緩存。
那么下面我們先來看一下注冊別名Alias的方法registry.registerAlias(beanName, alias)的處理邏輯:

【解釋】由以上代碼中可以得知,注冊alias的步驟如下:
【步驟1】alias與beanName相同情況處理。若alias與beanName名稱相同,則不需要處理并刪除掉原有alias。
【步驟2】alias覆蓋處理。若aliasName之前已經(jīng)被配置了,則進行3個判斷處理。
【步驟3】alias循環(huán)檢查。
【步驟4】注冊alias和beanName到aliasMap中。
2.4> fireComponentRegistered(...)
該方法的目的是為了通知監(jiān)聽器解析及注冊完成,這里的實現(xiàn)只為擴展,目前Spring并沒有對其進行任何實現(xiàn)。

三、alias標(biāo)簽的解析
在對bean進行定義時,除了使用id屬性來指定名稱之外,為了提供多個bean的名稱,我們可以使用alias標(biāo)簽來指定。例如,通過在<bean>標(biāo)簽中設(shè)置name屬性來為bean設(shè)置別名(alias)。如下所示:
<bean id="gun" name="m416, ak47" class="com.muse.Gun" />
另外,Spring還有另外一種聲明別名的方式:
<bean id="gun" class="com.muse.Gun" />
<alias name="gun" alias="m416, ak47" />
關(guān)于alias標(biāo)簽的解析的代碼,是在processAliasRegistration(ele)方法內(nèi)實現(xiàn)的,具體源碼請見下圖所示:

processAliasRegistration(ele)方法的內(nèi)部源碼如下圖所示:

【解釋】
processAliasRegistration(ele)這個方法的代碼邏輯比較簡單,與在上文的2.3中講的內(nèi)容一樣,都是將別名alias與beanName組成一對注冊到registry中。此處不再贅述。
四、import標(biāo)簽的解析
對于項目中的大量Spring配置文件而言,如果我們采取分模塊維護,那么更易于我們的管理。我們可以通過采用<import>標(biāo)簽,來引入不同模塊的配置文件,具體如下所示:
<beans>
<import resource="order.xml" />
<import resource="stock.xml" />
</beans>
關(guān)于import標(biāo)簽的解析邏輯,我們來看如下源碼:

其中,importBeanDefinitionResource(ele)方法的詳細源碼內(nèi)容和注釋,如下圖所示:


【解釋】
1> 獲取resource屬性所表示的路徑。
2> 解析路徑中的系統(tǒng)屬性,格式如“${user.dir}”。
3> 判定location是絕對路徑還是相對路徑。
4> 如果是絕對路徑,則遞歸調(diào)用bean的解析過程,進行另一次的解析。
5> 如果是相對路徑,則計算出絕對路徑并進行解析。
6> 通知監(jiān)聽器,解析完成(Spring沒有實現(xiàn)內(nèi)部邏輯)。
五、beans標(biāo)簽的解析
對于嵌入式的beans標(biāo)簽,非常類似于import標(biāo)簽所提供的功能。具體源碼位置為下圖所示:

對于嵌入式beans標(biāo)簽來講,并沒有太多可講,與單獨的配置文件并沒有太大的差別,無非是遞歸調(diào)用beans的解析過程。并且,在第1講的2.3.2章節(jié),就對doRegisterBeanDefinitions(ele)方法進行了解析,此處就不在贅述了。
今天的文章內(nèi)容就這些了:
寫作不易,筆者幾個小時甚至數(shù)天完成的一篇文章,只愿換來您幾秒鐘的 點贊 & 分享 。
更多技術(shù)干貨,歡迎大家關(guān)注公眾號“爪哇繆斯” ~ \(o)/ ~ 「干貨分享,每天更新」