第4章 自定義標簽的解析


??在之前的章節(jié)中,我們提到了在Spring中存在默認標簽與自定義標簽兩種,而在上一章節(jié)中我們分析了Spring中自定義標簽的加載過程.同樣,我們還是先再次回顧一下,當完成從配置文件到Document的轉換并提取對應的root后,將開始了所有元素的解析,而在這一過程中便開始了默認標簽與自定義標簽兩中格式的區(qū)分,方法如下:

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
        if (delegate.isDefaultNamespace(root)) {
            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)) {
                        parseDefaultElement(ele, delegate);
                    }
                    else {
                        delegate.parseCustomElement(ele);
                    }
                }
            }
        }
        else {
            delegate.parseCustomElement(root);
        }
    }

??在本章中,所有的功能都是圍繞其中的一句代碼delegate.parseCustomElement(root)開展的.從上面的函數我們可以看出,當Spring拿到一個元素時首先要做的是根據命名空間進行解析,如果是默認的命名空間,則使用parseDefaultElement方法進行元素解析,否則使用parseCustomElement方法進行解析.在分析自定義標簽的解析過程前,我們先了解一下自定義標簽的使用過程.


4.1 自定義標簽使用

??在很多情況下,我們需要為系統(tǒng)提供可配置化支持,簡單的做法可以直接基于Spring的標準bean來配置,但配置較為復雜或者需要更多豐富控制的時候,會顯得非常笨拙.一般的做法會用原生態(tài)的方式去解析定義好的XML文件,然后轉化為配置對象.這種方式當然可以解決所有問題,但實現起來比較繁瑣,特別是在配置非常復雜的時候,解析工作是一個不得不考慮的負擔.Spring提供了可擴展Schema的支持,這個一個不錯的折中方案,擴展Spring自定義標簽配置大致需要以下幾個步驟(前提是要把SpringCore包加入項目中).

  • 創(chuàng)建一個需要擴展的組件.
  • 定義一個XSD文件描述組件內容.
  • 創(chuàng)建一個文件,實現BeanDefinitionParser接口,用來解析XSD文件中的定義和組件定義.
  • 創(chuàng)建一個Handler文件,擴展自NamespaceHandlerSupport,目的是將組件注冊到Spring容器.
  • 編寫Spring.handlersSpring.schemas文件.

?現在我們就按照上面的步驟和大家一起體驗自定義標簽的過程.

(1)首先我們創(chuàng)建一個普通的POJO,這個POJO沒有任何特別之處,只是用來接收配置文件.

public class User {
    private String userName;
    private String email;
    // 省略get/set方法
}

(2)定義一個XSD文件描述組件內容.

<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.test.com/schema/user"
        xmlns:tns="http://www.test.com/schema/user"
        elementFormDefault="qualified">

    <element name="user">
        <complexType>
            <attribute name="id" type="string" />
            <attribute name="userName" type="string" />
            <attribute name="email" type="string" />
        </complexType>
    </element>

</schema>

??在上面的XSD文件中描述了一個新的targetNamespace,并在這個空間中定義了一個nameuserelement,user有3個屬性id,userNameemail,其中email的類型為string.這3個類主要用于驗證Spring配置文件中自定義格式.XSD文件是XML,DTD的替代者,使用XML Schema語言進行編寫,這里對XSD Schema不做太多解釋,大家有興趣可以自己研究一下.

(3)創(chuàng)建一個文件,實現BeanDefinitionParser接口,用來解析XSD文件中的定義和組件定義.

public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

    // Element對應的類
    protected Class getBeanClass(Element element) {
        return User.class;
    }

    // 從element中解析并提取對應的元素
    protected void doParse(Element element, BeanDefinitionBuilder bean) {
        String userName = element.getAttribute("userName");
        String email = element.getAttribute("email");
        // 將提取的數據放入到BeanDefinitionBuilder中,待到完成所有bean的解析后統(tǒng)一注冊到beanFatory中
        if (StringUtils.hasText(userName)) {
            bean.addPropertyValue("userName", userName);
        }
        if (StringUtils.hasText(email)) {
            bean.addPropertyValue("email", email);
        }
    }
}

(4) 創(chuàng)建一個Handler文件,擴展自NamespaceHandlerSupport,目的是將組件注冊到Spring容器.

public class MyNamespaceHandler extends NamespaceHandlerSupport {
    public void init() {
        registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
    }
}

??以上代碼很簡單,無非是當遇到自定義標簽<user:aaa>這樣類似于以user開頭的元素,就會把這個元素扔給對應的UserBeanDefinitionParser去解析.

(5) 編寫Spring.handlersSpring.schemas文件,默認位置是在工程的/ META-INF/文件夾下,當然,你可以通過Spring的擴展或者修改源碼的方式改變路徑.

  • Spring.handlers :
    http\://www.test.com/schema/user=test.MyNamespaceHandler
  • Spring.schema :
    http\://www.test.com/schema/user.xsd=META-INF/Spring-test.xsd

?到這里,自定義的配置就結束了,而Spring加載自定義的大致流程是遇到自定義標簽然后就去Spring.handlersSpring.schemas中去找對應的handlerXSD,默認位置是/META-INF/下,進而有找到對應的handler以及解析元素的Parser,從而完成整個自定義元素的解析,也就是說自定義與Spring中默認的標準配置不同在于Spring將自定義標簽解析的工作委托給了用戶去實現.

(6) 創(chuàng)建測試配置文件,在配置文件中引入對應的命名空間以及XSD后,便可以直接使用自定義標簽了.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:myname="http://www.test.com/schema/user"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.test.com/schema/user http://www.test.com/scheame/user.xsd">

    <myname:user id="testbean" userName="aaa" email="bbb" />

</beans>

(7) 測試

public class test {

    public static void main(String[] args) {
        ApplicationContext bf = new ClassPathXmlApplicationContext("test/test.xml");
        User user = (User) bf.getBean("testbean");
        System.out.println(user.getUserName() + ", " + user.getEmail());
    }
}

?不出意外的話,應該可以看到控制臺打印了如下結果:
?aaa,bbb
?在上面的例子中,我們實現了通過自定義標簽通過屬性的方式將user類型的Bean賦值,在Spring中自定義標簽非常常用,例如我們熟知的事物標簽:tx(<tx:annotation-driven>).


4.2 自定義標簽解析

??了解了自定義標簽的使用后,我們帶著強烈的好奇心來探究一下自定義的解析過程.

    @Nullable
    public BeanDefinition parseCustomElement(Element ele) {
        return parseCustomElement(ele, null);
    }

    // containingBd為父類bean,對頂層元素的解析應該設置為null
    @Nullable
    public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
        // 獲取對應的命名空間
        String namespaceUri = getNamespaceURI(ele);
        if (namespaceUri == null) {
            return null;
        }
        // 根據命名空間找到對應的NamespaceHandler
        NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
        if (handler == null) {
            error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
            return null;
        }
        // 調用自定義的NamespaceHandler進行解析
        return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
    }

??相信了解了自定義標簽的使用方法后,或多或少會對自定義標簽的實現過程有一個自己的想法.其實實現思路非常的簡單,無非是根據對應的bean獲取對應的命名空間,根據命名空間解析對應的處理器,然后根據用戶自定義的處理器進行解析.可是有些事情說起來簡單做起來難,我們接下來先看看如何獲取命名空間.

4.2.1 獲取標簽的命名空間

??標簽的解析是從命名空間的提起開始的,無論是區(qū)分Spring中默認標簽和自定義標簽還是區(qū)分自定義標簽中不同標簽的處理器都是以標簽所提供的命名空間為基礎的,而至于如何提取對應元素的命名空間其實不需要我們親自去實現,在org.w3c.dom.Node中已經提供了方法供我們直接調用:

public String getNamespaceURI(Node node){
    return node.getNamespaceURI();
}
4.2.2 提取自定義標簽處理器

??有了命名空間,就可以進行NamespaceHandler的提取了,繼續(xù)之前的parseCustomElement方法的跟蹤,分析NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);,在readerContext初始化的時候其屬性namespaceHandlerResolver已經被初始化為了DefaultNamespaceHandlerResolver的實例,所以這里調用的resolve方法其實調用的是DefaultNamespaceHandlerResolver類中的方法.我們進入DefaultNamespaceHandlerResolverresolve方法進行查看.


    @Override
    @Nullable
    public NamespaceHandler resolve(String namespaceUri) {
        // 獲取所有已經配置的handler映射
        Map<String, Object> handlerMappings = getHandlerMappings();
        // 根據命名空間找到對應的信息
        Object handlerOrClassName = handlerMappings.get(namespaceUri);
        if (handlerOrClassName == null) {
            return null;
        }
        else if (handlerOrClassName instanceof NamespaceHandler) {
            // 已經做過解析的情況直接從緩存讀取
            return (NamespaceHandler) handlerOrClassName;
        }
        else {
            // 沒有做過解析則返回類路徑
            String className = (String) handlerOrClassName;
            try {
                Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
                if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
                    throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
                            "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
                }
                // 初始化類
                NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
                // 調用自定義的NamespaceHandler的初始化方法
                namespaceHandler.init();
                // 記錄在緩存中
                handlerMappings.put(namespaceUri, namespaceHandler);
                return namespaceHandler;
            }
            catch (ClassNotFoundException ex) {
                throw new FatalBeanException("Could not find NamespaceHandler class [" + className +
                        "] for namespace [" + namespaceUri + "]", ex);
            }
            catch (LinkageError err) {
                throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" +
                        className + "] for namespace [" + namespaceUri + "]", err);
            }
        }
    }

??上面的函數清晰地闡述了解析自定義NamespaceHandler的過程,通過之前的示例程序我們了解到如果要使用自定義標簽,那么其中一項必不可少的操作就是在Spring.handlers文件中配置命名空間與命名空間處理的映射關系.只有這樣,Spring才能根據映射關系找到匹配的處理器,而尋找匹配的處理器就是在上面方法中實現,當獲取到自定義的NamespaceHandler之后就可以進行處理器初始化并解析了.我們這里在回憶一下對命名空間處理器的定義內容:

public class MyNamespaceHandler extends NamespaceHandlerSupport {
    public void init() {
        registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
    }
}

??當得到自定義命名空間處理后會馬上執(zhí)行namespaceHandler.init()來進行自定義BeanDefinitionParser的注冊.在這里,可以注冊多個標簽解釋器,當前實例中只有支持<myname:user>的寫法,也可以在這里注冊多個解析器,如<myname:A> <myname:B>等,是的myname的命名空間中可以支持多種標簽解析.
??注冊后,命名空間處理器就可以根據標簽的不同來調用不同的解析器進行解析.那么,根據上面的函數與之前介紹過的例子,我們基本上可以推斷getHandlerMappings的主要功能就是讀取Spring.handlers配置文件并將配置文件緩存在map中.

private Map<String, Object> getHandlerMappings() {
        Map<String, Object> handlerMappings = this.handlerMappings;
        // 如果沒有被緩存則開始進行緩存
        if (handlerMappings == null) {
            synchronized (this) {
                handlerMappings = this.handlerMappings;
                if (handlerMappings == null) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
                    }
                    try {
                        Properties mappings =
                                PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
                        if (logger.isDebugEnabled()) {
                            logger.debug("Loaded NamespaceHandler mappings: " + mappings);
                        }
                        handlerMappings = new ConcurrentHashMap<>(mappings.size());
                        // 將Properties格式文件合并到Map格式的handlerMappings中
                        CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
                        this.handlerMappings = handlerMappings;
                    }
                    catch (IOException ex) {
                        throw new IllegalStateException(
                                "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
                    }
                }
            }
        }
        return handlerMappings;
    }

??同我們想象的一樣,接住了工具類PropertiesLoaderUtils對屬性handlerMappingsLocation進行了配置文件的讀取,handlerMappingsLocation被默認初始化為META-INF/Spring.handlers.

4.2.3 標簽解析

??得到了解析器以及要分析的元素后,Spring就可以將解析工作委托給自定義解析器去解析了.在Spring中的代碼為:

return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));

??以之前提到的示例進行分析,此時的handler已經被實例化成我們自定義的MyNamespaceHandler了,而MyNamespaceHandler也已經完成了初始化的工作,但是在我們實現的自定義命名空間處理器中并沒有實現parse方法,所以推斷,這個方法是父類中的實現,查看父類NamespaceHandlerSupport中的parse方法.

    @Override
    @Nullable
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        // 尋找解析器并進行解析操作
        BeanDefinitionParser parser = findParserForElement(element, parserContext);
        return (parser != null ? parser.parse(element, parserContext) : null);
    }

??解析過程中首先是尋找元素對應的解析器,進而調用解析器中的parse方法,那么結合示例來講,其實就是首先獲取在MyNameSpaceHandler類中的init方法中注冊的對應的UserBeanDefinitionParser實例,并調用其parse方法進行進一步解析.

    @Nullable
    private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
        // 獲取元素名稱,也就是<myname:user中的user,若在示例中,此時localName為user
        String localName = parserContext.getDelegate().getLocalName(element);
        // 根據user找到對應的解析器,也就是在registerBeanDefinitionParser("user", new UserBeanDefinitionParser());注冊的解析器
        BeanDefinitionParser parser = this.parsers.get(localName);
        if (parser == null) {
            parserContext.getReaderContext().fatal(
                    "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
        }
        return parser;
    }

而對于parse方法的處理

    @Override
    @Nullable
    public final BeanDefinition parse(Element element, ParserContext parserContext) {
        AbstractBeanDefinition definition = parseInternal(element, parserContext);
        if (definition != null && !parserContext.isNested()) {
            try {
                String id = resolveId(element, definition, parserContext);
                if (!StringUtils.hasText(id)) {
                    parserContext.getReaderContext().error(
                            "Id is required for element '" + parserContext.getDelegate().getLocalName(element)
                                    + "' when used as a top-level tag", element);
                }
                String[] aliases = null;
                if (shouldParseNameAsAliases()) {
                    String name = element.getAttribute(NAME_ATTRIBUTE);
                    if (StringUtils.hasLength(name)) {
                        aliases = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name));
                    }
                }
                // 將AbstractBeanDefinition轉化為BeanDefinitionHolder并注冊
                BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
                registerBeanDefinition(holder, parserContext.getRegistry());
                if (shouldFireEvents()) {
                    // 需要通知監(jiān)聽器則進行處理
                    BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);
                    postProcessComponentDefinition(componentDefinition);
                    parserContext.registerComponent(componentDefinition);
                }
            }
            catch (BeanDefinitionStoreException ex) {
                String msg = ex.getMessage();
                parserContext.getReaderContext().error((msg != null ? msg : ex.toString()), element);
                return null;
            }
        }
        return definition;
    }

??雖說是對自定義配置文件的解析,但是,我們可以看到,在這個方法中大部分的代碼是用來處理將解析后的AbstractBeanDefinition轉化為BeanDefinitionHolder并注冊的功能,而真正去做解析的事情委托給了函數parseInternal,正是這句代碼調用了我們自定義的解析函數.
??在parseInternal中并不是直接調用自定義的doParse函數,而是進行了一系列的數據準備,包括對beanClass scope lazyInit等屬性的準備.

    @Override
    protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
        String parentName = getParentName(element);
        if (parentName != null) {
            builder.getRawBeanDefinition().setParentName(parentName);
        }
        // 獲取自定義標簽中的class,此時會調用自定義解析器如UserBeanDefinitionParser中的getBeanClass方法
        Class<?> beanClass = getBeanClass(element);
        if (beanClass != null) {
            builder.getRawBeanDefinition().setBeanClass(beanClass);
        }
        else {
            // 若子類沒有重寫getBeanClass方法則嘗試檢查子類是否重寫getBeanClassName方法
            String beanClassName = getBeanClassName(element);
            if (beanClassName != null) {
                builder.getRawBeanDefinition().setBeanClassName(beanClassName);
            }
        }
        builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
        BeanDefinition containingBd = parserContext.getContainingBeanDefinition();
        if (containingBd != null) {
            // 若存在父類則使用父類的scope屬性
            // Inner bean definition must receive same scope as containing bean.
            builder.setScope(containingBd.getScope());
        }
        if (parserContext.isDefaultLazyInit()) {
            // 調用子類重寫的doParse方法進行解析
            // Default-lazy-init applies to custom bean definitions as well.
            builder.setLazyInit(true);
        }
        doParse(element, parserContext, builder);
        return builder.getBeanDefinition();
    }

??回顧一下全部的自定義標簽處理過程,雖然在實例中我們定義UserBeanDefinitionParser,但是在其中我們只是做了與自己業(yè)務邏輯相關的部分.不過我們沒做但是并不代表沒有,在這個處理過程中同樣也是按照Spring中默認標簽的處理方式進行,包括創(chuàng)建BeanDefinition以及進行相應默認屬性的設置,對于這些工作Spring都默默地幫我們實現了,只是暴露出一些接口來供用戶實現個性化的業(yè)務.通過對本章的了解,相信讀者對Spring中自定義標簽的使用以及在解析自定義標簽過程中Spring為我們做了哪些工作會有一個全面的了解,到此為止我們已經完成了Spring中全部的解析工作,也就是說到現在為止我們已經理解了Springbean從配置文件到加載到內存中的全過程,而接下來的任務便是如何使用這些bean.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,462評論 2 378

推薦閱讀更多精彩內容