本文就来分析自定义标签的解析,像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容器的重头戏,后面会详细学习的。