第4章 自定义标签的解析


  在之前的章节中,我们提到了在Spring中存在默认标签与自定义标签两种,而在上一章节中我们分析了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);
        }
    }

  在本章中,所有的功能都是围绕其中的一句代码delegate.parseCustomElement(root)开展的.从上面的函数我们可以看出,当Spring拿到一个元素时首先要做的是根据命名空间进行解析,如果是默认的命名空间,则使用parseDefaultElement方法进行元素解析,否则使用parseCustomElement方法进行解析.在分析自定义标签的解析过程前,我们先了解一下自定义标签的使用过程.


4.1 自定义标签使用

  在很多情况下,我们需要为系统提供可配置化支持,简单的做法可以直接基于Spring的标准bean来配置,但配置较为复杂或者需要更多丰富控制的时候,会显得非常笨拙.一般的做法会用原生态的方式去解析定义好的XML文件,然后转化为配置对象.这种方式当然可以解决所有问题,但实现起来比较繁琐,特别是在配置非常复杂的时候,解析工作是一个不得不考虑的负担.Spring提供了可扩展Schema的支持,这个一个不错的折中方案,扩展Spring自定义标签配置大致需要以下几个步骤(前提是要把SpringCore包加入项目中).

  • 创建一个需要扩展的组件.
  • 定义一个XSD文件描述组件内容.
  • 创建一个文件,实现BeanDefinitionParser接口,用来解析XSD文件中的定义和组件定义.
  • 创建一个Handler文件,扩展自NamespaceHandlerSupport,目的是将组件注册到Spring容器.
  • 编写Spring.handlersSpring.schemas文件.

 现在我们就按照上面的步骤和大家一起体验自定义标签的过程.

(1)首先我们创建一个普通的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)创建一个文件,实现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的解析后统一注册到beanFatory中
        if (StringUtils.hasText(userName)) {
            bean.addPropertyValue("userName", userName);
        }
        if (StringUtils.hasText(email)) {
            bean.addPropertyValue("email", email);
        }
    }
}

(4) 创建一个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) 创建测试配置文件,在配置文件中引入对应的命名空间以及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 获取标签的命名空间

  标签的解析是从命名空间的提起开始的,无论是区分Spring中默认标签和自定义标签还是区分自定义标签中不同标签的处理器都是以标签所提供的命名空间为基础的,而至于如何提取对应元素的命名空间其实不需要我们亲自去实现,在org.w3c.dom.Node中已经提供了方法供我们直接调用:

public String getNamespaceURI(Node node){
    return node.getNamespaceURI();
}
4.2.2 提取自定义标签处理器

  有了命名空间,就可以进行NamespaceHandler的提取了,继续之前的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());
    }
}

  当得到自定义命名空间处理后会马上执行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()) {
                    // 需要通知监听器则进行处理
                    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,但是在其中我们只是做了与自己业务逻辑相关的部分.不过我们没做但是并不代表没有,在这个处理过程中同样也是按照Spring中默认标签的处理方式进行,包括创建BeanDefinition以及进行相应默认属性的设置,对于这些工作Spring都默默地帮我们实现了,只是暴露出一些接口来供用户实现个性化的业务.通过对本章的了解,相信读者对Spring中自定义标签的使用以及在解析自定义标签过程中Spring为我们做了哪些工作会有一个全面的了解,到此为止我们已经完成了Spring中全部的解析工作,也就是说到现在为止我们已经理解了Springbean从配置文件到加载到内存中的全过程,而接下来的任务便是如何使用这些bean.

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

推荐阅读更多精彩内容

  • 上一章 默认标签的解析下一章 努力更新中 3.3 import标签的解析   对于Spring配置文件的编写,如果...
    十丈_红尘阅读 187评论 0 1
  • 下一章 容器的基本实现   Spring是于2003年兴起的一个轻量级的Java开源框架,由Rod Johnson...
    十丈_红尘阅读 931评论 0 1
  • 敏若昭蛛忌 ——写在倚天屠龙之后 张无忌最后和谁在一起了?赵敏,还是周芷若,可我最喜欢小昭,四个女子都对他情深义重...
    长伟阅读 177评论 0 0
  • 1.创建版本库 在项目目录运行Git Bash(相当于cd命令定位到项目目录),然后用 git init 命令初始...
    CsDefault阅读 244评论 0 0
  • 哈喽,我是现在的你,在给你写一些想跟你说的话, 2018年1月,你虚23,实22。 你有一个一直陪着你的女朋友 你...
    你是一个小胖丁阅读 224评论 0 0