Spring自定义标签的定义和解析

一、自定义标签的定义

1. 什么是自定义的标签?

1.1 自定义标签配置
<?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:muzi="http://www.jd.com/schema/mytags"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.jd.com/schema/mytags
       http://www.jd.com/schema/mytags.xsd
       ">
    <!--
        我自己的自定义标签
    -->
    <muzi:exercise-function id="redis" ip="118.89.164.186" port="6379" password="root"/>

</beans>

该案例是基于Jedis创建了一个自定义标签的案例,在上述配置代码中,有几点需要描述一下。

  1. xmlns是xml namespace的缩写,xmlns:muzi=“http://www.jd.com/schema/mytags”,其中muzi是当前配置文件自定义的命名空间的名称,URL是命名空间的值。

  2. xsi:schemaLocation定义的是命名空间的内容地址,第一个URI“http://www.jd.com/schema/mytags”是命名空间的值,第二个URI“http://www.jd.com/schema/mytags.xsd”是Schema文档的位置,Schema处理器将从这个位置读取Schema文档,且该文档的targetNamespace必须与第一个URI相匹配。

  3. muzi:exercise-function标签中的参数在命名空间在Schema文档中去定义。具体的需要先定义一个 mytags.xsd 文件,定义 exercise-function 标签有什么属性,属性是什么类型的。

1.2 自定义标签Schema文档
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.jd.com/schema/mytags"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.jd.com/schema/mytags"
 elementFormDefault="qualified" attributeFormDefault="unqualified">

    <xsd:element name="exercise-function">
        <xsd:complexType>
            <xsd:attribute name="id" type="xsd:string"></xsd:attribute>
            <xsd:attribute name="ip" type="xsd:string"></xsd:attribute>
            <xsd:attribute name="port" type="xsd:string"></xsd:attribute>
            <xsd:attribute name="password" type="xsd:string"></xsd:attribute>
        </xsd:complexType>
    </xsd:element>
</xsd:schema>
Schema文档概述
  • Schema文档定义的位置需要在项目resources文件夹下定义,META-INF/mytags.xsd

  • targetNamespace="http://www.jd.com/schema/mytags"与之前配置文件中的namespaceURI是相对应的。

  • xsd:element定义的是一类标签的内容,xsd:complexType标签具体内属性类型。

1.3 程序如何定位Schema

配置文件中,Spring是通过 xsi:schemaLocation=“.....” 里面定义的内容找到这个xsd去解析的,但是“http://www.jd.com/schema/mytags.xsd”不是一个有效的URL,那么Spring是如何找到Schema文档的呢?

# 地址映射文件:META-INF/spring.schemas

http\://www.jd.com/schema/mytags.xsd=META-INF/mytags.xsd

如上述代码所示,Spring会通过扫描各个依赖包下面的“spring.schemas”文件,通过文档的逻辑地址和物理地址的映射来找到自定义标签Schema文档的具体位置。

简单来看,自定义标签无非就是定义一个内容的格式,属性,就是一种常用的配置的约束。

2、为什么要创建自定义标签?

日常开发中接触到的自定义标签有很多,老版本的Spring基本都是依赖xml配置去使用的,spring扩展内容的jar包也好,第三方功能性jar包也好,在集成Spring的时候总要提供一个客户端的配置方式,所以开源的jar包集成Spring一般都会提供一些自定义标签。
例如:druid,mybatis,jedis,dubbo等。

二、理解SPI设计模式

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找配置文件,自动加载文件里所定义的类。是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

1. Java原生的SPI

SPI配置文件:META-INF/services/com.jd.nlp.dev.muzi.spring5.exercise.pattern.spi.SpiService
描述:
在resources的META-INF下创建一个services文件夹,以“com.jd.nlp.dev.muzi.spring5.exercise.pattern.spi.SpiService”接口的包路径命名创建一个文件。

SPI配置文件内容:

com.jd.nlp.dev.muzi.spring5.exercise.pattern.spi.SpiService01
com.jd.nlp.dev.muzi.spring5.exercise.pattern.spi.SpiService02

SPI接口定义:

/*
* service provider interface
* */
public interface SpiService {

    public String query(String param);
}

SPI接口两个实现类定义:

public class SpiService01 implements SpiService {
    @Override
    public String query(String param) {
        System.out.println("SpiService01");
        return "OK";
    }
}

public class SpiService02 implements SpiService {
    @Override
    public String query(String param) {
        System.out.println("SpiService02");
        return null;
    }
}

Java原生SPI一般是怎么用的呢?
首先他会加载所有SpiService在配置文件中配置的实现类并创建实例,使用的时候一般是遍历使用,通过判断其类型执行具体实现类的方法。当然这种SPI也是有利弊的,不能不分场合的随意效仿。其优点是扩展很容易,写一个类加入即可。缺点也很明显,粒度不够细,通过配置的方式写了很多的类,当需要通过配置的方式获取唯一一个类是这种形式就不可以了,就要考虑使用策略模式。

        ServiceLoader<SpiService> load = ServiceLoader.load(SpiService.class);
        // 这么使用其实和BeanPostProcessor的使用很像
        for (SpiService spiService : load) {
            if (spiService instanceof SpiService01){
                spiService.query("90");
            }
            if (spiService instanceof SpiService02){
                spiService.query("90");
            }
        }
----------------------------------------------------------------------------------------
执行结果:
SpiService01
SpiService02

Process finished with exit code 0

2. 了解Spring的SPI机制

上述代码可以通过instanceof去判断并具体类型对象的方法,这种方式其实和Spring中的SPI殊途同归,Spring是通过扫描各个jar的META-INF中的spring.handlers,提取namespaceURI和实现类路径,以namespaceURI做key,以实现类路径反射创建的对象作为值,来构建映射表。
spring.handlers中配置的解析类都需要继承NamespaceHandler这个接口实现多态,并且根据需求选择重写该接口的相关方法(init,parse,decorate)。

三、源码解读 - 自定义标签解析源码

1、自定义标签解析流程源码的位置

DefaultBeanDefinitionDocumentReader.java

    protected void doRegisterBeanDefinitions(Element root) {
        BeanDefinitionParserDelegate parent = this.delegate;
        /**
         * 主要是获取delegate,用来委托给第三方解析起解析自定义标签
         */
        this.delegate = createDelegate(getReaderContext(), root, parent);
        if (this.delegate.isDefaultNamespace(root)) {
            // ... ... 省略
        }
        /**
         * 预处理模版方法
         */
        preProcessXml(root);
        /**
         * 主要看这个方法,标签的具体解析过程
         */
        parseBeanDefinitions(root, this.delegate);
        /**
         * 后处理模版方法
         */
        postProcessXml(root);
        this.delegate = parent;
    }

找到DefaultBeanDefinitionDocumentReader类,查看doRegisterBeanDefinitions方法,在之前的《Spring解析XML注册BeanDefinition》的文章走过这个流程。先进入parseBeanDefinitions方法。

    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解析
                         */
                        delegate.parseCustomElement(ele);
                    }
                }
            }
        }
        // ... ... 
    }

进入自定义的标签解析的方法。

BeanDefinitionParserDelegate.java

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

再次进入重载的解析方法,找到了自定义标签解析的主流程内容。

    public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
        /**
         * 1.获取标签元素的NamespaceURI
         */
        String namespaceUri = getNamespaceURI(ele);
        if (namespaceUri == null) {
            return null;
        }
        /**
         * 2.通过URI来获得对应的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;
        }
        /**
         * 3.使用handler来解析该标签,带入 readerContext 进去。
         */
        return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
    }

上述代码中自定义标签解析主要分三步:

  1. 获取自定义标签的namespace URI。
  2. 根据namespace URI创建namespace URI和NamespaceHandler的映射并获取对应的NamespaceHandler的具体实例。
  3. 执行该NamespaceHandler的parse方法。
    其中重点内容是创建映射初始化和得到对应的NamespaceHandler实例。

2、基于我的 exercise-function 自定义标签解读源码

2.1 exercise-function 使用

Spring配置文件:spring.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"
       xmlns:muzi="http://www.jd.com/schema/mytags"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.jd.com/schema/mytags
       http://www.jd.com/schema/mytags.xsd
       ">
    <!--
        我自己的自定义标签
    -->
    <muzi:exercise-function id="redis" ip="118.89.164.186" port="6379" password="root"/>

</beans>

Schema 映射文件:META-INF/spring.schemas

http\://www.jd.com/schema/mytags.xsd=META-INF/mytags.xsd

Schema 文档文件:META-INF/mytags.xsd

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

    <xsd:element name="exercise-function">
        <xsd:complexType>
            <xsd:attribute name="id" type="xsd:string"></xsd:attribute>
            <xsd:attribute name="ip" type="xsd:string"></xsd:attribute>
            <xsd:attribute name="port" type="xsd:string"></xsd:attribute>
            <xsd:attribute name="password" type="xsd:string"></xsd:attribute>
        </xsd:complexType>
    </xsd:element>
</xsd:schema>

自定义标签解析类映射文件:META-INF/spring.handlers


http\://www.jd.com/schema/mytags=com.jd.nlp.dev.muzi.spring5.exercise.demo08.exercise02.TagsNamespaceHandler

自定义标签解析类:TagsNamespaceHandler.java

public class TagsNamespaceHandler extends NamespaceHandlerSupport {
    
    public void init() {
        this.registerBeanDefinitionParser("exercise-function",
                new RedisBeanDifinitionParser());
    }

    @Override
    public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) {
        return super.decorate(node, definition, parserContext);
    }
}

测试程序,加载上述配置文件,因为我的标签包装的是自定义注解的

    @Test
    public void run03(){
        // 自定义标签扫描
        ApplicationContext app = new ClassPathXmlApplicationContext(
                "classpath:spring5/exercise/demo08/spring.xml");
        Jedis client1 = (Jedis)app.getBean("redis");
        System.out.println(client1);
        System.out.println(client1.set("laosiji", "pa pa pa !"));
        System.out.println(client1.get("laosiji"));
    }

执行结果:

16:41:23.265 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@75bd9247
16:41:23.475 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 1 bean definitions from class path resource [spring5/exercise/demo08/spring.xml]
16:41:23.506 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'redis'
redis.clients.jedis.Jedis@57175e74
OK
pa pa pa !
2.2 DEBUG - 揭秘代码中的 namespace URI

直接看 BeanDefinitionParserDelegate.java 中的 parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) 方法。打断点DEBUG,可以看到程序走到这获取到的namespace URI 就是之前提到的 xmlns = " http://www.jd.com/schema/mytags " 这个配置的值。

DEBUG - namespace URI
2.3 exercise-function - resolve(namespaceUri)

点击代码中resolve(namespaceUri),可以看到有如下接口。

public interface NamespaceHandlerResolver {
    @Nullable
    NamespaceHandler resolve(String namespaceUri);
}

进入实现类的resolve方法,如下代码所示,其中最重要的是getHandlerMappings()这个方法,它就是扫描各个jar的META-INF目录下的spring.handlers文件,将内容构建成映射表。后续的处理就比较简单了,通过类路径,基于反射创建NamespaceHandler接口实现类的对象,调用init方法初始化,最后把该namespaceURI的值(类路径)替换成 实现类对象,便于下一次直接获取返回。

public NamespaceHandler resolve(String namespaceUri) {
        /**
         * 加载"META-INF/spring.handlers"文件,建立URI和处理类的映射关系。
         *
         * 方法:getHandlerMappings
         * 重要程度:* * * * *
         */
        Map<String, Object> handlerMappings = getHandlerMappings();

        // 根据URI就可以找到唯一的处理类(字符串)
        Object handlerOrClassName = handlerMappings.get(namespaceUri);
        // ... ... ... ... 
        else {
            // 处理类(字符串)反射
            String className = (String) handlerOrClassName;
            try {
                /**
                 * 反射这个类
                 */
                Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
                // ... ... ... ...
                /**
                 * 基于类对象来实例化
                 * 备注:所有处理类必须继承NamespaceHandler,实现多态。
                 * 例如:
                 *   SimpleConstructorNamespaceHandler implements NamespaceHandler
                 *   所有spring.handlers这些命名解析类都有一个特点是必须实现NamespaceHandler接口,来实现多态
                 */
                NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);

                // 调用处理类初始化方法
                namespaceHandler.init();

                // 替换映射关系key对应的值
                handlerMappings.put(namespaceUri, namespaceHandler);
                return namespaceHandler;
            }
            // ... ... ... ...
        }
    }
2.4 exercise-function - getHandlerMappings()

getHandlerMappings()方法主要就是加载"META-INF/spring.handlers"文件过程(所有jar包中的spring.handlers),构建映射。

private Map<String, Object> getHandlerMappings() {
        Map<String, Object> handlerMappings = this.handlerMappings;
        if (handlerMappings == null) {
            synchronized (this) {
                handlerMappings = this.handlerMappings;
                if (handlerMappings == null) {
                    try {
                        /**
                         * 加载"META-INF/spring.handlers"文件过程(所有jar包中的spring.handlers)
                         */
                        Properties mappings =PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
                        // ... ... ... ...
                        /**
                         * 和处理类建立映射关系
                         */
                        handlerMappings = new ConcurrentHashMap<>(mappings.size());
                        CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
                        this.handlerMappings = handlerMappings;
                    }
                    // ... ... ... ...
                }
            }
        }
        return handlerMappings;
    }

如下图所示,映射结果中有我们配置的 http://www.jd.com/schema/mytags ,这个自定义的namespaceURI,值目前还是配置的类路径,还没有进行反射实例化和初始化后替换值。

namespace URI 映射结果

2.5 exercise-function - 自定义标签解析类init()

初始化方法中注册RedisBeanDifinitionParser类对象。
TagsNamespaceHandler.java

public class TagsNamespaceHandler extends NamespaceHandlerSupport {
    
    public void init() {
        this.registerBeanDefinitionParser("exercise-function",
                new RedisBeanDifinitionParser());
    }

    @Override
    public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) {
        return super.decorate(node, definition, parserContext);
    }
}

RedisBeanDifinitionParser类的doParse方法是创建了一个JedisShardInfo参数,并设置到BeanDefinitionBuilder的ConstructorArgValue属性中,之前文章中提到过,ConstructorArgValue参数就是构造器的参数,在Bean实例化的时候会根据这个参数去对应选择构造器实例化。
getBeanClass指定的是类的对象,自定义标签中通过getBeanClass来指定beanDefinition的class,在Bean的实例化过程中会先取BeanClass属性,没有的话才会根据className属性的类路径去ClassUtils.forName获得BeanClass。
RedisBeanDifinitionParser.java

public class RedisBeanDifinitionParser extends
        AbstractSingleBeanDefinitionParser {
    
    protected Class<?> getBeanClass(Element element) {
        return Jedis.class;
    }
    
    protected void doParse(Element element, BeanDefinitionBuilder builder) {
        String ip = element.getAttribute("ip");
        String port = element.getAttribute("port");
        String password = element.getAttribute("password");

        JedisShardInfo jedisShardInfo = new JedisShardInfo(ip,Integer.parseInt(port));
        jedisShardInfo.setPassword(password);

        builder.addConstructorArgValue(jedisShardInfo);
    }
}
2.6 exercise-function - NamespaceHandlerSupport parse方法

TagsNamespaceHandler 继承了 NamespaceHandlerSupport 类,TagsNamespaceHandler上述没有实现parse方法,所以调用的是 NamespaceHandlerSupport 类的 parse方法。
在该parse方法中就是找之前init()方法中注册的解析起,下述代码中的localname就是"exercise-function"字符串。

    public BeanDefinition parse(Element element, ParserContext parserContext) {
        /**
         * 获取这个自定义标签元素的解析器
         */
        BeanDefinitionParser parser = findParserForElement(element, parserContext);
        return (parser != null ? parser.parse(element, parserContext) : null);
    }

    private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
        String localName = parserContext.getDelegate().getLocalName(element);
        /**
         * 获取解析的parser,parsers这个map是在获取Handler时调用init的时候把parser注册进Map的。
         */
        BeanDefinitionParser parser = this.parsers.get(localName);
        return parser;
    }

BeanDefinitionParser调用parse方法,首先是执行基类AbstractBeanDefinitionParser的parse方法,在第一行就执行子类 AbstractSingleBeanDefinitionParser 的 parseInternal(element, parserContext) 方法。
parseInternal(element, parserContext) 方法的最后一行会执行doParse(element, parserContext, builder), 因为我们的解析起实现了doParse方法所以执行的是自定义解析器的doParse。自定义解析器重写了getBeanClass(Element element)方法,也会在BeanDefinitionBuilder中插入beanClass属性。
在BeanDefinitionBuilder中插入了一些自定义的构造函数的参数,最后使用BeanDefinitionBuilder创建BeanDefinition时,就包含我们设置的构造参数了。

2.6 exercise-function - BeanDefinition注册DEBUG结果图
注册后的结果图.png

BeanDefinition注册成功后,就意味着我们随时可以在工厂中创建并获取我们自定义标签配置的实例。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342