Spring 源碼學(xué)習(xí)(二)默認(rèn)標(biāo)簽解析

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-upreplace等子元素屬性。

上一篇主要介紹 Spring 容器的基礎(chǔ)結(jié)構(gòu),沒有細(xì)說這些標(biāo)簽是如何解析的。

所以本篇是來進(jìn)行補坑的,介紹這些標(biāo)簽在代碼中是如何識別和解析的~

本篇筆記的結(jié)構(gòu)大致如下:

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

再次說下,下載項目看完整注釋,跟著源碼一起分析~

碼云 Gitee 地址

Github 地址


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、 namealias等屬性。
  • 對實例 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é)了對屬性 idname 的解析,不再贅述,下面講下對標(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-methodreplace-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 方法。

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

個人并不推薦這種使用方法,如果常規(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) 類型來封裝解析出來的元素(包含type name index 屬性)
  • addIndexedArgumentValue 方法,將解析后的 value 添加到當(dāng)前 BeanDefinitionConstructorArgumentValuesindexedArgumentValues 屬性中

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

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

這兩個流程區(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)類 Book1Book2,如果使用代碼:

@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)簽的解析吧,下一篇再會~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容