spring原始碼深度解析— IOC 之 自定義標籤解析

chen_hao發表於2019-07-05

概述

之前我們已經介紹了spring中預設標籤的解析,解析來我們將分析自定義標籤的解析,我們先回顧下自定義標籤解析所使用的方法,如下圖所示: 

我們看到自定義標籤的解析是通過BeanDefinitionParserDelegate.parseCustomElement(ele)進行的,解析來我們進行詳細分析。

自定義標籤的使用

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

  1. 建立一個需要擴充套件的元件
  2. 定義一個 XSD 檔案,用於描述元件內容
  3. 建立一個實現 AbstractSingleBeanDefinitionParser 介面的類,用來解析 XSD 檔案中的定義和元件定義
  4. 建立一個 Handler,繼承 NamespaceHandlerSupport ,用於將元件註冊到 Spring 容器
  5. 編寫 Spring.handlers 和 Spring.schemas 檔案

下面就按照上面的步驟來實現一個自定義標籤元件。

建立元件

該元件就是一個普通的 JavaBean,沒有任何特別之處。這裡我建立了兩個元件,為什麼是兩個,後面有用到

User.java

package chenhao.spring01;

/**
 * @author: ChenHao
 * @Description:
 * @Date: Created in 16:26 2019/7/2
 * @Modified by:
 */
public class User {

    private String id;

    private String userName;

    private String email;public void setId(String id) {
        this.id = id;
    }public void setUserName(String userName) {
        this.userName = userName;
    }public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("{");
        sb.append("\"id\":\"")
                .append(id).append('\"');
        sb.append(",\"userName\":\"")
                .append(userName).append('\"');
        sb.append(",\"email\":\"")
                .append(email).append('\"');
        sb.append('}');
        return sb.toString();
    }
}

Phone.java

package chenhao.spring01;

/**
 * @author: ChenHao
 * @Description:
 * @Date: Created in 16:26 2019/7/2
 * @Modified by:
 */
public class Phone {

    private String color;

    private int size;

    private String remark;


    public void setColor(String color) {
        this.color = color;
    }

    public void setSize(int size) {
        this.size = size;
    }

    public void setRemark(String remark) {
        this.remark = remark;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("{");
        sb.append("\"color\":\"")
                .append(color).append('\"');
        sb.append(",\"size\":")
                .append(size);
        sb.append(",\"remark\":\"")
                .append(remark).append('\"');
        sb.append('}');
        return sb.toString();
    }
}

定義 XSD 檔案

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            xmlns="http://www.chenhao.com/schema/user"
            targetNamespace="http://www.chenhao.com/schema/user"
            elementFormDefault="qualified">
    <xsd:element name="user">
        <xsd:complexType>
            <xsd:attribute name="id" type="xsd:string" />
            <xsd:attribute name="userName" type="xsd:string" />
            <xsd:attribute name="email" type="xsd:string" />
        </xsd:complexType>
    </xsd:element>

    <xsd:element name="phone">
        <xsd:complexType>
            <xsd:attribute name="id" type="xsd:string" />
            <xsd:attribute name="color" type="xsd:string" />
            <xsd:attribute name="size" type="xsd:int" />
            <xsd:attribute name="remark" type="xsd:string" />
        </xsd:complexType>
    </xsd:element>

</xsd:schema>

在上述XSD檔案中描述了一個新的targetNamespace,並在這個空間裡定義了一個name為userphone的element 。user裡面有三個attribute。主要是為了驗證Spring配置檔案中的自定義格式。再進一步解釋,就是,Spring位置檔案中使用的user自定義標籤中,屬性只能是上面的三種,有其他的屬性的話,就會報錯。 

Parser 類

定義一個 Parser 類,該類繼承 AbstractSingleBeanDefinitionParser ,並實現 getBeanClass() 和 doParse() 兩個方法。主要是用於解析 XSD 檔案中的定義和元件定義。這裡定義了兩個Parser類,一個是解析User類,一個用來解析Phone類。

UserBeanDefinitionParser.java

package chenhao.spring01;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;

/**
 * @author: ChenHao
 * @Description:
 * @Date: Created in 16:29 2019/7/2
 * @Modified by:
 */
public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
    @Override
    protected Class getBeanClass(Element ele){
        return User.class;
    }

    @Override
    protected void doParse(Element element, BeanDefinitionBuilder builder) {
        String id = element.getAttribute("id");
        String userName=element.getAttribute("userName");
        String email=element.getAttribute("email");
        if(StringUtils.hasText(id)){
            builder.addPropertyValue("id",id);
        }
        if(StringUtils.hasText(userName)){
            builder.addPropertyValue("userName", userName);
        }
        if(StringUtils.hasText(email)){
            builder.addPropertyValue("email", email);
        }

    }
}

PhoneBeanDefinitionParser.java

package chenhao.spring01;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;

/**
 * @author: ChenHao
 * @Description:
 * @Date: Created in 16:29 2019/7/2
 * @Modified by:
 */
public class PhoneBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
    @Override
    protected Class getBeanClass(Element ele){
        return Phone.class;
    }

    @Override
    protected void doParse(Element element, BeanDefinitionBuilder builder) {
        String color = element.getAttribute("color");
        int size=Integer.parseInt(element.getAttribute("size"));
        String remark=element.getAttribute("remark");
        if(StringUtils.hasText(color)){
            builder.addPropertyValue("color",color);
        }
        if(StringUtils.hasText(String.valueOf(size))){
            builder.addPropertyValue("size", size);
        }
        if(StringUtils.hasText(remark)){
            builder.addPropertyValue("remark", remark);
        }

    }
}

Handler 類

定義 Handler 類,繼承 NamespaceHandlerSupport ,主要目的是將上面定義的解析器Parser類註冊到 Spring 容器中。

package chenhao.spring01;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

/**
 * @author: ChenHao
 * @Description:
 * @Date: Created in 16:38 2019/7/2
 * @Modified by:
 */
public class MyNamespaceHandler extends NamespaceHandlerSupport {

    @Override
    public void init() {
        registerBeanDefinitionParser("user",new UserBeanDefinitionParser());

        registerBeanDefinitionParser("phone",new PhoneBeanDefinitionParser());
    }

}

我們看看 registerBeanDefinitionParser 方法做了什麼

private final Map<String, BeanDefinitionParser> parsers = new HashMap<>();

protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) {
    this.parsers.put(elementName, parser);
}

就是將解析器 UserBeanDefinitionParser和 PhoneBeanDefinitionParser 的例項放到全域性的Map中,key為user和phone。

Spring.handlers和Spring.schemas

編寫Spring.handlers和Spring.schemas檔案,預設位置放在工程的META-INF資料夾下

Spring.handlers

http\://www.chenhao.com/schema/user=chenhao.spring01.MyNamespaceHandler

Spring.schemas

http\://www.chenhao.com/schema/user.xsd=org/user.xsd

而 Spring 載入自定義的大致流程是遇到自定義標籤然後 就去 Spring.handlers 和 Spring.schemas 中去找對應的 handler 和 XSD ,預設位置是 META-INF 下,進而有找到對應的handler以及解析元素的 Parser ,從而完成了整個自定義元素的解析,也就是說 Spring 將向定義標籤解析的工作委託給了 使用者去實現。

建立測試配置檔案

經過上面幾個步驟,就可以使用自定義的標籤了。在 xml 配置檔案中使用如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:myTag="http://www.chenhao.com/schema/user"
       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
       http://www.chenhao.com/schema/user http://www.chenhao.com/schema/user.xsd">

    <bean id="myTestBean" class="chenhao.spring01.MyTestBean"/>

    <myTag:user id="user" email="chenhao@163.com" userName="chenhao" />

    <myTag:phone id="iphone" color="black" size="128" remark="iphone XR"/>

</beans>

xmlns:myTag表示myTag的名稱空間是 http://www.chenhao.com/schema/user ,在文章開頭的判斷處 if (delegate.isDefaultNamespace(ele)) 肯定會返回false,將進入到自定義標籤的解析

測試

import chenhao.spring01.MyTestBean;
import chenhao.spring01.Phone;
import chenhao.spring01.User;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author: ChenHao
 * @Description:
 * @Date: Created in 10:36 2019/6/19
 * @Modified by:
 */
public class AppTest {
    @Test
    public void MyTestBeanTest() {
        BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml"));
        //MyTestBean myTestBean01 = (MyTestBean) bf.getBean("myTestBean");
        User user = (User) bf.getBean("user");
        Phone iphone = (Phone) bf.getBean("iphone");

        System.out.println(user);
        System.out.println(iphone);
    }

}

輸出結果:

專案整體檔案目錄如下

自定義標籤的解析

 瞭解了自定義標籤的使用後,接下來我們分析下自定義標籤的解析,自定義標籤解析用的是方法:parseCustomElement(Element ele, @Nullable BeanDefinition containingBd),進入方法體:

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

    // 呼叫自定義的 Handler 處理
    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

相信瞭解了自定義標籤的使用方法後,或多或少會對向定義標籤的實現過程有一個自己的 想法 其實思路非常的簡單,無非是根據對應的bean 獲取對應的名稱空間 ,根據名稱空間解析對應的處理器,然後根據使用者自定義的處理器進行解析。

獲取標籤的名稱空間

 標籤的解析是從名稱空間的提起開始的,元論是區分 Spring中預設標籤和自定義標 還是 區分自定義標籤中不同標籤的處理器都是以標籤所提供的名稱空間為基礎的,而至於如何提取對應元素的名稱空間其實並不需要我們親內去實現,在 org.w3c.dom.Node 中已經提供了方法供我們直接呼叫:

String namespaceUri = getNamespaceURI(ele);
@Nullable
public String getNamespaceURI(Node node) {
    return node.getNamespaceURI();
}

這裡我們通過DEBUG看出myTag:user自定義標籤對應的 namespaceUri 是 http://www.chenhao.com/schema/user

讀取自定義標籤處理器 

根據 namespaceUri 獲取 Handler,這個對映關係我們在 Spring.handlers 中已經定義了,所以只需要找到該類,然後初始化返回,最後呼叫該 Handler 物件的 parse() 方法處理,該方法我們也提供了實現。所以上面的核心就在於怎麼找到該 Handler 類。呼叫方法為:this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri)

public NamespaceHandler resolve(String namespaceUri) {
    // 獲取所有已經配置的 Handler 對映
    Map<String, Object> handlerMappings = getHandlerMappings();

    // 根據 namespaceUri 獲取 handler的資訊:這裡一般都是類路徑
    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() 方法
            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);
        }
    }
}

首先呼叫 getHandlerMappings() 獲取所有配置檔案中的對映關係 handlerMappings ,就是我們在 Spring.handlers 檔案中配置 名稱空間與名稱空間處理器的對映關係,該關係為 <名稱空間,類路徑>,然後根據名稱空間 namespaceUri 從對映關係中獲取相應的資訊,如果為空或者已經初始化了就直接返回,否則根據反射對其進行初始化,同時呼叫其 init()方法,最後將該 Handler 物件快取。我們再次回憶下示例中對於名稱空間處理器的內容:

public class MyNamespaceHandler extends NamespaceHandlerSupport {

    @Override
    public void init() {
        registerBeanDefinitionParser("user",new UserBeanDefinitionParser());

        registerBeanDefinitionParser("phone",new PhoneBeanDefinitionParser());
    }

}

當得到自定義名稱空間處理後會馬上執行 namespaceHandler.init() 來進行自定義 BeanDefinitionParser的註冊,在這裡,你可以註冊多個標籤解析器,如當前示例中 <myTag:user 標籤就使用 new UserBeanDefinitionParser()解析器; <myTag:phone就使用new PhoneBeanDefinitionParser()解析器。

上面我們已經說過, init()中的registerBeanDefinitionParser 方法 其實就是將對映關係放在一個 Map 結構的 parsers 物件中:private final Map<String, BeanDefinitionParser> parsers 。

推薦部落格

  程式設計師寫程式碼之外,如何再賺一份工資?

標籤解析 

得到了解析器和分析的元素後,Spring就可以將解析工作委託給自定義解析器去解析了,對於標籤的解析使用的是:NamespaceHandler.parse(ele, new ParserContext(this.readerContext, this, containingBd))方法,進入到方法體內:

public BeanDefinition parse(Element element, ParserContext parserContext) {
    BeanDefinitionParser parser = findParserForElement(element, parserContext);
    return (parser != null ? parser.parse(element, parserContext) : null);
}

呼叫 findParserForElement() 方法獲取 BeanDefinitionParser 例項,其實就是獲取在 init() 方法裡面註冊的例項物件。如下:

private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    //獲取元素名稱,也就是<myTag:user中的 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;
}

獲取 localName,在上面的例子中就是 : user,然後從 Map 例項 parsers 中獲取 UserBeanDefinitionParser例項物件。返回 BeanDefinitionParser 物件後,呼叫其 parse(),該方法在 AbstractBeanDefinitionParser 中實現:

我們可以從DEBUG中看出,當前標籤是 <myTag:user  ,對應的localName是user,對應的自定義解析器是UserBeanDefinitionParser,返回的是UserBeanDefinitionParser例項物件。接下來我們看看 parser.parse (element, parserContext),該方法在 AbstractBeanDefinitionParser 中實現:

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));
                }
            }
            BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
            //將 AbstractBeanDefinition 轉換為 BeanDefinitionHolder 並註冊
            registerBeanDefinition(holder, parserContext.getRegistry());
            if (shouldFireEvents()) {
                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等屬性的準備。 我們進入到AbstractSingleBeanDefinitionParser.parseInternal方法中:

protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
    // 建立一個BeanDefinitionBuilder,內部實際上是建立一個GenericBeanDefinition的例項,用於儲存自定義標籤的元素
    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 {
        // beanClass 為 null,意味著子類並沒有重寫 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) {
        // Inner bean definition must receive same scope as containing bean.
        builder.setScope(containingBd.getScope());
    }
    if (parserContext.isDefaultLazyInit()) {
        // Default-lazy-init applies to custom bean definitions as well.
        builder.setLazyInit(true);
    }

    // 呼叫子類的 doParse() 進行解析
    doParse(element, parserContext, builder);
    return builder.getBeanDefinition();
}

public static BeanDefinitionBuilder genericBeanDefinition() {
    return new BeanDefinitionBuilder(new GenericBeanDefinition());
}

protected Class<?> getBeanClass(Element element) {
    return null;
}

protected void doParse(Element element, BeanDefinitionBuilder builder) {
}

在該方法中我們主要關注兩個方法:getBeanClass() 、doParse()。對於 getBeanClass() 方法,AbstractSingleBeanDefinitionParser 類並沒有提供具體實現,而是直接返回 null,意味著它希望子類能夠重寫該方法,當然如果沒有重寫該方法,這會去呼叫 getBeanClassName() ,判斷子類是否已經重寫了該方法。對於 doParse() 則是直接空實現。所以對於 parseInternal() 而言它總是期待它的子類能夠實現 getBeanClass()doParse(),其中 doParse() 尤為重要,如果你不提供實現,怎麼來解析自定義標籤呢?最後將自定義的解析器:UserDefinitionParser 再次回觀。

public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
    @Override
    protected Class getBeanClass(Element ele){
        return User.class;
    }

    @Override
    protected void doParse(Element element, BeanDefinitionBuilder builder) {
        String id = element.getAttribute("id");
        String userName=element.getAttribute("userName");
        String email=element.getAttribute("email");
        if(StringUtils.hasText(id)){
            builder.addPropertyValue("id",id);
        }
        if(StringUtils.hasText(userName)){
            builder.addPropertyValue("userName", userName);
        }
        if(StringUtils.hasText(email)){
            builder.addPropertyValue("email", email);
        }

    }
}

我們看看 builder.addPropertyValue ("id",id) ,實際上是將自定義標籤中的屬性解析,存入 BeanDefinitionBuilder 中的 beanDefinition例項中

private final AbstractBeanDefinition beanDefinition;

public BeanDefinitionBuilder addPropertyValue(String name, @Nullable Object value) {
    this.beanDefinition.getPropertyValues().add(name, value);
    return this;
}

最後 將 AbstractBeanDefinition 轉換為 BeanDefinitionHolder 並註冊 registerBeanDefinition(holder, parserContext.getRegistry());這就和預設標籤的註冊是一樣了。

至此,自定義標籤的解析過程已經分析完成了。其實整個過程還是較為簡單:首先會載入 handlers 檔案,將其中內容進行一個解析,形成 <namespaceUri,類路徑> 這樣的一個對映,然後根據獲取的 namespaceUri 就可以得到相應的類路徑,對其進行初始化等到相應的 Handler 物件,呼叫 parse() 方法,在該方法中根據標籤的 localName 得到相應的 BeanDefinitionParser 例項物件,呼叫 parse() ,該方法定義在 AbstractBeanDefinitionParser 抽象類中,核心邏輯封裝在其 parseInternal() 中,該方法返回一個 AbstractBeanDefinition 例項物件,其主要是在 AbstractSingleBeanDefinitionParser 中實現,對於自定義的 Parser 類,其需要實現 getBeanClass() 或者 getBeanClassName() 和 doParse()。最後將 AbstractBeanDefinition 轉換為 BeanDefinitionHolder 並註冊 。

 

相關文章