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

  • Bean 標籤解析入口創建 GenericBeanDefinition解析 meta 屬性解析 lookup-method 屬性解析 constructor-arg 屬性解析 qualifer 屬性
  • 參考資料
  • 從上一篇筆記可以看出,在容器註冊 bean 信息的時候,做了很多解析操作,而 xml 文件中包含了很多標籤、屬性,例如 bean 、 import 標籤, meta 、look-up 和 replace等子元素屬性。

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

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

    本篇筆記的結構大致如下:

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

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

    碼雲 Gitee 地址

    Github 地址


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

    • 默認標籤 這是我們最常使用到的標籤類型了,像我們一開始寫的 <bean>,它屬於默認標籤,除了這個標籤外,還有其它四種標籤(import、 alias、 bean、 beans)
    • 自定義標籤 自定義標籤的用途,是為了給系統提供可配置化支持,例如事務標籤 <annotation-driven>,它是 Spring 的自定義標籤,通過繼承 NamespaceHandler 來完成自定義命名空間的解析。

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

    <code>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);
    }
    }
    /<code>

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

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

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


    Bean 標籤解析入口

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

    <code>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));
    }

    }
    /<code>

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

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

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

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


    創建 GenericBeanDefinition

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

    createBeanDefinition(className, parent);

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

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


    解析 meta 屬性

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

    <code><bean>


    /<bean>
    /<code>

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

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

    因為代碼比較簡單,所以通過圖片進行說明:

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


    解析 lookup-method 屬性

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

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

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


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


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

    我覺得是因為抽象類無法被實例化,必須要有具體實現類,所以在這個時候,Spring 容器要加載 AbstractGetBookTest 對象,可以用到 <lookup> 屬性,通過注入特定實現類,來完成類的加載。/<lookup>

    config.xml


    <code>
    <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>


    <lookup-method>
    /<bean>

    <bean>


    /<bean>

    <bean>

    /<beans>
    /<code>

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

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

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


    解析 constructor-arg 屬性

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

    <code><bean>

    <constructor-arg>
    <constructor-arg>
    /<bean>
    /<code>

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

    具體解析在這個方法中:

    <code>/**
    * 註釋 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);
    }
    }
    }
    /<code>

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

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

    ① 配置中指定了 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 屬性的處理比較簡單,所以大家看代碼就能瞭解它是如何解析的,比較難的是子元素處理,例如下面的例子:

    <code><constructor-arg>

    <entry>

    /<constructor-arg>
    /<code>

    具體解析子元素的方法是: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 等等子元素的機械,這裡不細說,同學們感興趣繼續去跟蹤吧~


    解析 qualifer 屬性

    大家更熟悉的應該是 @qualifer 標籤吧,它跟 qualifer 屬性的用途一樣。

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

    例如我們定義了一個抽象類 AbstractBook,有兩個具體實現類 Book1 和 Book2,如果使用代碼:

    <code>@Autowired
    private AbstractBook book;
    /<code>

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

    ① 在配置文件中設定 quailfer

    通過 qualifier 指定注入 bean 的名稱

    <code><bean>
    <qualifer>
    /<bean>

    /<code>

    ② 使用 @Qualifier("beanNeame")


    <code>@Qualifier("book1")
    private AbstractBook book;
    /<code>

    同樣的,代碼的解析過程跟前面的套路相近,留給同學們自己去分析吧~


    分享到:


    相關文章: