1. 程式人生 > >Spring原始碼閱讀筆記05:自定義xml標籤解析

Spring原始碼閱讀筆記05:自定義xml標籤解析

  在上篇文章中,提到了在Spring中存在預設標籤與自定義標籤兩種,並且詳細分析了預設標籤的解析,本文就來分析自定義標籤的解析,像Spring中的AOP就是通過自定義標籤來進行配置的,這裡也是為後面學習AOP原理打下基礎。

  這裡先回顧一下,當Spring完成了從配置檔案到Document的轉換並提取對應的root後,將開始所有元素的解析,而在這一過程中便會區分預設標籤與自定義標籤兩種格式,並分別解析,可以再看一下這部分的原始碼加深理解:

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

  從上面的函式中也可以看出,當Spring拿到一個元素時首先要做的是根據名稱空間進行解析,如果是預設的名稱空間,則使用parseDefaultElement()方法進行元素解析,否則使用parseCustomElement()方法進行解析。在本文中,所有的功能解析都是圍繞其中的那句程式碼delegate.parseCustomElement(root)開展的。

  在分析自定義標籤的解析過程之前,我們先了解一下自定義標籤的使用過程,這裡參考spring文件中的例子。

1. 自定義標籤使用

  擴充套件Spring自定義標籤配置大致需要以下幾個步驟:

  • 定義一個XML檔案來描述你的自定義標籤元素
  • 建立一個Handler,擴充套件自NamespaceHandlerSupport
  • 建立若干個BeanDefinitionParser的實現,用來解析XML檔案中的定義
  • 將上述檔案註冊到Spring中,這裡其實是做一下配置

  接下來我們將建立一個自定義XML元素,便於通過一個更容易的方式配置SimpleDateFormat型別的bean。配置好之後我們可以通過下面的方式來定義一個SimpleDateFormat型別的bean:

<myns:dateformat id = "dateFormat" pattern = "yyyy-MM-dd HH:mm" lenient = "true"/>

1.1 編寫schema

  給Spring IoC容器建立XML擴充套件標籤的第一步是建立一個新的XML模式來描述對應的標籤(下面是我們將要用來配置SimpleDateFormat物件的schema):

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.mycompany.com/schema/myns"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:beans="http://www.springframework.org/schema/beans"
    targetNamespace="http://www.mycompany.com/schema/myns"
    elementFormDefault="qualified"
    attributeFormDefault="unqualified">

    <xsd:import namespace="http://www.springframework.org/schema/beans"/>

    <xsd:element name="dateformat">
        <xsd:complexType>
            <xsd:complexContent>
                <xsd:extension base="beans:identifiedType">
                    <xsd:attribute name="lenient" type="xsd:boolean"/>
                    <xsd:attribute name="pattern" type="xsd:string" use="required"/>
                </xsd:extension>
            </xsd:complexContent>
        </xsd:complexType>
    </xsd:element>
</xsd:schema>

  定義了上面的schema之後,我們就可以直接使用元素<myns:dateformat/>來配置SimpleDateFormat型別的物件了:

<myns:dateformat id="dateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>

  如果沒有做上面的工作,我們可能就需要通過下面的方式來配置SimpleDateFormat型別的物件了:

<bean id="dateFormat" class="java.text.SimpleDateFormat">
    <constructor-arg value="yyyy-HH-dd HH:mm"/>
    <property name="lenient" value="true"/>
</bean>

1.2 編寫一個BeanDefinitionParser

  這個是繼承自AbstractSingleBeanDefinitionParser,主要是用來將自定義標籤解析成BeanDefinition。

public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser{
    protected Class getBeanClass(Element element) {
        return SimpleDateFormat.class;
    }
    protected void doParse(Element element, BeanDefinitionBuilder bean) {
        // this will never be null since the schema explicitly requires that a value be supplied
        String pattern = element.getAttribute("pattern");
        bean.addConstructorArg(pattern);
        // this however is an optional property
        String lenient = element.getAttribute("lenient");
        if (StringUtils.hasText(lenient)) {
            bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
        }
    }
}

1.3 編寫一個NamespaceHandler

  這個是繼承自NamespaceHandlerSupport,主要是將上面的BeanDefinitionParser註冊到Spring容器:

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

}

1.4 編寫Spring.handlers和Spring.schemas檔案

  這兩個檔案預設位置是在工程資源目錄的/META-INF/資料夾下,內容如下(注意要改成自己的包名):

META-INF/spring.handlers
http\://www.mycompany.com/schema/myns=spring.customElement.MyNamespaceHandler

META-INF/spring.schemas
http\://www.mycompany.com/schema/myns/myns.xsd=spring/customElement/myns.xsd

1.5 自定義標籤使用示例

  使用自定義的擴充套件標籤和使用Spring提供的預設標籤是類似的,可以按照如下配置一個SimpleDateFormat型別的bean:

<?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:myns="http://www.mycompany.com/schema/myns"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.mycompany.com/schema/myns http://www.mycompany.com/schema/myns/myns.xsd">

    <!-- as a top-level bean -->
    <myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>
</beans>

  配置好之後可以測試一下:

public static void main(String[] args) {
    XmlBeanFactory xmlBeanFactory = new XmlBeanFactory(new ClassPathResource("customElement.xml"));
    SimpleDateFormat myTestBean = (SimpleDateFormat)xmlBeanFactory.getBean("defaultDateFormat");
    System.out.println( "now time --- "+ myTestBean.format(new Date()));
}

// 輸出結果:
now time --- 2020-03-07 20:37

2. 自定義標籤解析

  瞭解了自定義標籤的使用之後,我們來探究一下自定義標籤的解析過程。接著文章開頭提到的,我們要從BeanDefinitionParserDelegate的parseCustomElement()方法開始:

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

public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
    // 獲取對應的名稱空間
    String namespaceUri = getNamespaceURI(ele);
    // 根據名稱空間找到對應的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));
}

  這裡可以看出對自定義標籤進行解析的思路是根據Element獲取對應的名稱空間,然後根據名稱空間獲取對應的處理器,最後根據使用者自定義的處理器進行解析,可是看起來簡單,實現起來就不是這麼簡單了,先來看一下名稱空間的獲取吧。

2.1 獲取標籤的名稱空間

  自定義標籤的解析是從名稱空間的提取開始的,無論是區分預設標籤和自定義標籤,還是區分自定義標籤對應的不同處理器,都是以標籤所提供的名稱空間為基礎的。至於如何提取對應元素的名稱空間,已經有現成的實現可供使用,spring中是直接呼叫org.w3c.dom.Node提供的相應方法來完成名稱空間的提取:

public String getNamespaceURI(Node node) {
    return node.getNamespaceURI();
}

2.2 獲取自定義標籤處理器

  有了名稱空間,就可以此來提取對應的NamespaceHandler了,這項工作是由下面這句程式碼來完成的:

NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);

  這裡readerContext的getNamespaceHandlerResolver()方法返回的其實是DefaultNamespaceHandlerResolver,所以我們直接進入其resolve()方法中往下看:

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.init();
            // 記錄在快取
            handlerMappings.put(namespaceUri, namespaceHandler);
            return namespaceHandler;
        }
        catch (ClassNotFoundException ex) {
            throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" +
                    namespaceUri + "] not found", ex);
        }
        catch (LinkageError err) {
            throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" +
                    namespaceUri + "]: problem with handler class file or dependent class", err);
        }
    }
}

  上面函式中的流程還是比較清晰的,在前面的自定義標籤使用示例中有說到,如果要使用自定義標籤,需要在Spring.handlers檔案中配置名稱空間與名稱空間處理器的對映關係。只有這樣,Spring才能根據對映關係找到匹配的處理器。

  而尋找匹配的處理器就是在上面函式中實現的,當獲取到自定義的NamespaceHandler之後就可以進行處理器初始化並解析了。這裡我們再回憶一下前面自定義標籤示例中,對於名稱空間處理器的內容(我們在其init()方法中註冊了一個解析器)。

  在上面的程式碼中,獲取到自定義名稱空間處理器後會馬上執行其init()方法來進行自定義BeanDefinitionParser的註冊。當然在init()中可以註冊多個標籤解析器,如<myns:A、<myns:B等,使得myns的名稱空間中可以支援多種標籤解析。

  註冊好之後,名稱空間處理器就可以根據標籤的不同來呼叫不同的解析器進行解析。根據上面的函式和之前的例子,我們基本可以判斷getHandlerMappings()的主要功能就是讀取Spring.handlers配置檔案並將配置檔案快取在map中:

private Map<String, Object> getHandlerMappings() {
    // 如果沒有被快取則開始進行快取
    if (this.handlerMappings == null) {
        synchronized (this) {
            if (this.handlerMappings == null) {
                try {
                    // this.handlerMappingsLocation在建構函式中已經被初始化為:META-INF/Spring.handlers
                    Properties mappings =
                            PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
                    if (logger.isDebugEnabled()) {
                        logger.debug("Loaded NamespaceHandler mappings: " + mappings);
                    }
                    Map<String, Object> handlerMappings = new ConcurrentHashMap<String, Object>(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 this.handlerMappings;
}

  這裡是藉助工具類PropertiesLoaderUtils對Spring.handlers配置檔案進行了讀取,然後將讀取的內容放到快取中並返回。

2.3 標籤解析

  獲取到解析器以及要解析的元素後,Spring將解析工作委託給自定義解析器來解析,即下面程式碼所完成的:

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

  此時我們拿到的handler其實是我們自定義的MyNamespaceHandler了,但是我們前面並沒有實現parse()方法,所以這裡這個應該是呼叫的父類中的parse()方法,看一下NamespaceHandlerSupport中的parse()方法:

public BeanDefinition parse(Element element, ParserContext parserContext) {
    // 尋找解析器並進行解析操作
    return findParserForElement(element, parserContext).parse(element, parserContext);
}

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

  首先是尋找元素對應的解析器,然後呼叫其parse()方法。結合我們前面的示例,其實就是首先獲取在MyNamespaceHandler類中的init()方法中註冊對應的SimpleDateFormatBeanDefinitionParser例項,並呼叫其parse()方法進行進一步解析,同樣這裡parse()方法我們前面是沒有實現的,我們也試著從其父類找一下:

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 = new String[0];
            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()) {
            // 需要通知監聽器則進行處理
                BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);
                postProcessComponentDefinition(componentDefinition);
                parserContext.registerComponent(componentDefinition);
            }
        }
        catch (BeanDefinitionStoreException ex) {
            parserContext.getReaderContext().error(ex.getMessage(), 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,此時會呼叫自定義解析器中的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));
    if (parserContext.isNested()) {
        // 若存在父類則使用父類的scope屬性
        builder.setScope(parserContext.getContainingBeanDefinition().getScope());
    }
    if (parserContext.isDefaultLazyInit()) {
        // 配置延遲載入
        builder.setLazyInit(true);
    }
    // 呼叫子類重寫的doParse方法進行解析
    doParse(element, parserContext, builder);
    return builder.getBeanDefinition();
}

// 這裡就是呼叫前面示例中我們自己寫的doParse()方法
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
    doParse(element, builder);
}

  到這裡就完成了對自定義標籤轉換成BeanDefinition的整個過程了,回顧一下整個過程,在我們定義的SimpleDateFormatBeanDefinitionParser中我們只是做了與自己業務邏輯相關的部分,剩下的包括建立BeanDefinition以及進行相應預設屬性的設定,Spring都幫我們預設實現了,我們當然也可以自己來完成這一過程,比如AOP就是這樣做的,但是本文還是用最簡單的方式來做一個說明。

3. 總結

  其實從Spring對自定義標籤的解析中也可以體會到Spring的可擴充套件式設計思路,通過暴露一些介面,我們就能夠方便地實現自己的個性化業務,不僅如此,Spring自己便是這項功能的踐行者,像AOP、事務都是通過這種方式來定製對應的標籤來完成配置需求的。

  到這裡我們已經完成了Spring中全部的解析工作的學習,也就是說到這裡我們已經學習了Spring將bean從配置檔案載入到記憶體的完整過程,接下來的任務便是如果使用這些bean,這才是IoC容器的重頭戲,後面會詳細學習