42. 可扩展的 XML 创作

42.1 简介

自 version 2.0 以来,Spring 已经为 schema-based extensions 提供了基本 Spring XML 格式的机制,用于定义和配置 beans。本节将详细介绍如何编写自己的自定义 XML bean 定义解析器以及将这些解析器集成到 Spring IoC 容器中。

为了便于使用 schema-aware XML 编辑器创建 configuration files,Spring 的可扩展 XML configuration 机制基于 XML Schema。如果您不熟悉标准 Spring 发行版附带的 Spring 当前的 XML configuration extensions,请首先阅读标题为???的附录。

创建新的 XML configuration extensions 可以通过以下(相对)简单的步骤来完成:

  • 创作用于描述自定义 element(s 的 XML schema。

  • 编码自定义NamespaceHandler implementation(这是一个简单的 step,不用担心)。

  • 编码一个或多个BeanDefinitionParser __mplement(这是真正的工作完成的地方)。

  • 注册上面的 artifact 与 Spring(这也是一个简单的 step)。

以下是对这些步骤的描述。对于 example,我们将创建一个 XML 扩展(一个自定义 XML 元素),允许我们以简单的方式配置SimpleDateFormat类型的 objects(来自java.text包)。完成后,我们将能够像这样定义SimpleDateFormat类型的 bean 定义:

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

(不要担心这个例子很简单;之后会有更详细的例子.第一个简单例子的意图是引导你完成基本步骤 involved.)

42.2 创作 schema

创建用于 Spring 的 IoC 容器的 XML configuration 扩展,首先创建一个 XML Schema 来描述扩展。以下是我们将用于配置SimpleDateFormat objects 的 schema。

<!-- myns.xsd (inside package org/springframework/samples/xml) -->

<?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>

(强调的 line 包含所有可识别标签的扩展基础(意味着它们具有id属性,将用作容器中的 bean 标识符)。我们可以使用此属性,因为我们导入了 Spring-provided 'beans' namespace.)

上面的 schema 将用于使用<myns:dateformat/>元素直接在 XML application context 文件中配置SimpleDateFormat objects。

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

请注意,在我们创建了基础结构 classes 之后,上面的 XML 片段基本上与以下 XML 片段完全相同。换句话说,我们只是在容器中创建一个 bean,由SimpleDateFormat类型的 name 'dateFormat'标识,并设置了几个 properties。

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

方法 creating configuration 格式允许使用具有 schema-aware XML 编辑器的 IDE 进行紧密整合。使用正确创作的 schema,您可以使用自动完成功能让用户在枚举中定义的几个 configuration 选项之间进行选择。

42.3 编码 NamespaceHandler

除了 schema 之外,我们需要一个NamespaceHandler来解析 Spring 在解析 configuration files 时遇到的特定命名空间的所有元素。在我们的例子中,NamespaceHandler应该处理myns:dateformat元素的解析。

NamespaceHandler接口非常简单,它只需要三个方法:

  • init() - 允许初始化NamespaceHandler,并在使用处理程序之前由 Spring 调用

  • BeanDefinition parse(Element, ParserContext) - Spring 遇到 top-level 元素时调用(不嵌套在 bean 定义或不同的命名空间内)。此方法可以注册 bean 定义本身 and/or return bean 定义。

  • BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext) - 当 Spring 遇到不同命名空间的属性或嵌套元素时调用。一个或多个 bean 定义的装饰用于out-of-the-box 范围 Spring 2.0 支持的 example。我们首先突出显示一个简单的 example,不使用装饰,然后我们将在更高级的 example 中显示装饰。

虽然完全可以为整个命名空间编码自己的NamespaceHandler(因此提供解析命名空间中每个元素的 code),但通常情况是 Spring XML configuration 文件中的每个 top-level XML 元素都会产生一个 bean 定义(在我们的例子中,单个<myns:dateformat/>元素导致单个SimpleDateFormat bean 定义)。 Spring features 支持这种情况的许多便利 classes。在这个 example 中,我们将使用NamespaceHandlerSupport class:

package org.springframework.samples.xml;

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

public class MyNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
    }

}

观察者 reader 会注意到这个 class 中实际上并没有很多解析逻辑。确实...... NamespaceHandlerSupport class 有一个内置的授权概念。它支持注册任何数量的BeanDefinitionParser实例,当它需要解析其命名空间中的元素时,它将委托给它们。这种干净的关注分离允许NamespaceHandler处理其命名空间中所有自定义元素的解析编排,同时委托BeanDefinitionParsers来完成 XML 解析的繁琐工作;这意味着每个BeanDefinitionParser将只包含解析单个自定义元素的逻辑,我们可以在下一个 step 中看到

42.4 BeanDefinitionParser

如果NamespaceHandler遇到已映射到特定 bean 定义解析器的类型的 XML 元素(在本例中为'dateformat'),则将使用BeanDefinitionParser。换句话说,BeanDefinitionParser负责解析 schema 中定义的一个不同的 top-level XML 元素。在解析器中,我们可以访问 XML 元素(以及它的子元素),以便我们可以解析自定义 XML 内容,如下面的示例所示:

package org.springframework.samples.xml;

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

import java.text.SimpleDateFormat;

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

}

我们使用 Spring-provided AbstractSingleBeanDefinitionParser来处理 creating 单个BeanDefinition的许多基本 grunt 工作。

我们为AbstractSingleBeanDefinitionParser超类提供了我们单个BeanDefinition所代表的类型。
在这个简单的例子中,这就是我们需要做的一切。我们的单个BeanDefinition的创建由AbstractSingleBeanDefinitionParser超类处理,bean 定义的唯一标识符的提取和设置也是如此。

42.5 注册处理程序和 schema

编码完成了!剩下要做的就是以某种方式使 Spring XML 解析基础设施知道我们的自定义元素;我们通过在两个特殊用途 properties files 中注册我们的自定义namespaceHandler和自定义 XSD 文件来完成此操作。这些 properties files 都放在 application 的'META-INF'目录中,并且对于 example,可以与 JAR 文件中的二进制 class 一起分发。 Spring XML 解析基础结构将通过 consuming 这些特殊的 properties files 自动获取您的新扩展,其格式如下所述。

42.5.1'META-INF/spring.handlers'

名为'spring.handlers'的 properties 文件包含 XML Schema URI 到名称空间处理程序 classes 的映射。因此,对于我们的示例,我们需要编写以下内容:

http\://www.mycompany.com/schema/myns=org.springframework.samples.xml.MyNamespaceHandler

(':'字符是 Java properties 格式的有效分隔符,因此 URI 中的':'字符需要使用 backslash.)进行转义

key-value 对的第一部分(key)是与您的自定义命名空间扩展相关联的 URI,并且需要 match 完全匹配自定义 XSD schema 中指定的'targetNamespace'属性的 value。

42.5.2'META-INF/spring.schemas'

名为'spring.schemas'的 properties 文件包含 XML Schema 位置(与 XML files 中的 schema 声明一起使用 schema 作为'xsi:schemaLocation'属性的一部分)到 classpath 资源的映射。需要此文件来防止 Spring 绝对必须使用需要 Internet 访问权限的默认EntityResolver才能检索 schema 文件。如果在此 properties 文件中指定映射,Spring 将在 classpath 上搜索 schema(在本例中为'org.springframework.samples.xml'包中的'myns.xsd'):

http\://www.mycompany.com/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd

这样做的结果是,我们鼓励您在 classpath 上的NamespaceHandlerBeanDefinitionParser classes 旁边部署 XSD file(s。

42.6 在 Spring XML configuration 中使用自定义扩展

使用您自己实现的自定义扩展与使用 Spring 直接提供的“自定义”extensions 之一没有什么不同。在下面找到使用 Spring XML configuration 文件中前面步骤中开发的自定义<dateformat/>元素的示例。

<?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"/>

    <bean id="jobDetailTemplate" abstract="true">
        <property name="dateFormat">
            <!-- as an inner bean -->
            <myns:dateformat pattern="HH:mm MM-dd-yyyy"/>
        </property>
    </bean>

</beans>

42.7 更实用的例子

在下面找到一些更加健全的自定义 XML extensions 示例。

42.7.1 在自定义标记内嵌套自定义标记

这个 example 说明了如何编写满足以下 configuration 目标所需的各种 artifact:

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

    <foo:component id="bionic-family" name="Bionic-1">
        <foo:component name="Mother-1">
            <foo:component name="Karate-1"/>
            <foo:component name="Sport-1"/>
        </foo:component>
        <foo:component name="Rock-1"/>
    </foo:component>

</beans>

上面的 configuration 实际上将自定义 extensions 嵌套在一起。由上述<foo:component/>元素实际配置的 class 是Component class(如下所示)。注意Component class 如何不公开'components' property 的 setter 方法;这使得使用 setter 注入为Component class 配置 bean 定义变得困难(或者更不可能)。

package com.foo;

import java.util.ArrayList;
import java.util.List;

public class Component {

    private String name;
    private List<Component> components = new ArrayList<Component> ();

    // mmm, there is no setter method for the 'components'
    public void addComponent(Component component) {
        this.components.add(component);
    }

    public List<Component> getComponents() {
        return components;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

此问题的典型解决方案是创建一个自定义FactoryBean,为'components' property 公开 setter property。

package com.foo;

import org.springframework.beans.factory.FactoryBean;

import java.util.List;

public class ComponentFactoryBean implements FactoryBean<Component> {

    private Component parent;
    private List<Component> children;

    public void setParent(Component parent) {
        this.parent = parent;
    }

    public void setChildren(List<Component> children) {
        this.children = children;
    }

    public Component getObject() throws Exception {
        if (this.children != null && this.children.size() > 0) {
            for (Component child : children) {
                this.parent.addComponent(child);
            }
        }
        return this.parent;
    }

    public Class<Component> getObjectType() {
        return Component.class;
    }

    public boolean isSingleton() {
        return true;
    }

}

这一切都非常好,并且确实很好地工作,但是向最终用户公开了很多 Spring 管道。我们要做的是编写一个自定义扩展,隐藏所有这些 Spring 管道。如果我们坚持前面描述的步骤,我们将从创建 XSD schema 开始,以定义自定义标记的结构。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://www.foo.com/schema/component"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.foo.com/schema/component"
        elementFormDefault="qualified"
        attributeFormDefault="unqualified">

    <xsd:element name="component">
        <xsd:complexType>
            <xsd:choice minOccurs="0" maxOccurs="unbounded">
                <xsd:element ref="component"/>
            </xsd:choice>
            <xsd:attribute name="id" type="xsd:ID"/>
            <xsd:attribute name="name" use="required" type="xsd:string"/>
        </xsd:complexType>
    </xsd:element>

</xsd:schema>

然后我们将创建一个自定义NamespaceHandler

package com.foo;

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

public class ComponentNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser());
    }

}

接下来是自定义BeanDefinitionParser。请记住,我们创建的是一个描述ComponentFactoryBeanBeanDefinition

package com.foo;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;

import java.util.List;

public class ComponentBeanDefinitionParser extends AbstractBeanDefinitionParser {

    protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
        return parseComponentElement(element);
    }

    private static AbstractBeanDefinition parseComponentElement(Element element) {
        BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean.class);
        factory.addPropertyValue("parent", parseComponent(element));

        List<Element> childElements = DomUtils.getChildElementsByTagName(element, "component");
        if (childElements != null && childElements.size() > 0) {
            parseChildComponents(childElements, factory);
        }

        return factory.getBeanDefinition();
    }

    private static BeanDefinition parseComponent(Element element) {
        BeanDefinitionBuilder component = BeanDefinitionBuilder.rootBeanDefinition(Component.class);
        component.addPropertyValue("name", element.getAttribute("name"));
        return component.getBeanDefinition();
    }

    private static void parseChildComponents(List<Element> childElements, BeanDefinitionBuilder factory) {
        ManagedList<BeanDefinition> children = new ManagedList<BeanDefinition>(childElements.size());
        for (Element element : childElements) {
            children.add(parseComponentElement(element));
        }
        factory.addPropertyValue("children", children);
    }

}

最后,需要在 Spring XML 基础结构中注册各种 artifact。

# in 'META-INF/spring.handlers'
http\://www.foo.com/schema/component=com.foo.ComponentNamespaceHandler
# in 'META-INF/spring.schemas'
http\://www.foo.com/schema/component/component.xsd=com/foo/component.xsd

42.7.2'普通'元素的自定义属性

编写自己的自定义解析器和关联的 artifacts 并不难,但有时这不是正确的做法。考虑需要向现有 bean 定义添加元数据的场景。在这种情况下,您当然不希望自己编写自己的整个自定义扩展程序;相反,您只想在现有的 bean 定义元素中添加其他属性。

通过另一个例子,假设您为服务 object 定义 bean 定义的服务 class 将(未知)访问集群JCache,并且您希望确保在内部急切地启动命名的 JCache 实例周围的 cluster:

<bean id="checkingAccountService" class="com.foo.DefaultCheckingAccountService"
        jcache:cache-name="checking.account">
    <!-- other dependencies here... -->
</bean>

我们要做的是在解析'jcache:cache-name'属性时创建另一个BeanDefinition;这个BeanDefinition然后将为我们初始化命名的 JCache。我们还将修改'checkingAccountService'的现有BeanDefinition,以便它依赖于这个新的 JCache-initializing BeanDefinition

package com.foo;

public class JCacheInitializer {

    private String name;

    public JCacheInitializer(String name) {
        this.name = name;
    }

    public void initialize() {
        // lots of JCache API calls to initialize the named cache...
    }

}

现在进入自定义扩展。首先,XSD schema 的创作描述了自定义属性(在这种情况下非常简单)。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://www.foo.com/schema/jcache"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.foo.com/schema/jcache"
        elementFormDefault="qualified">

    <xsd:attribute name="cache-name" type="xsd:string"/>

</xsd:schema>

接下来,关联NamespaceHandler

package com.foo;

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

public class JCacheNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        super.registerBeanDefinitionDecoratorForAttribute("cache-name",
            new JCacheInitializingBeanDefinitionDecorator());
    }

}

接下来,解析器。请注意,在这种情况下,因为我们要解析 XML 属性,所以我们写一个BeanDefinitionDecorator而不是BeanDefinitionParser

package com.foo;

import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.BeanDefinitionDecorator;
import org.springframework.beans.factory.xml.ParserContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class JCacheInitializingBeanDefinitionDecorator implements BeanDefinitionDecorator {

    private static final String[] EMPTY_STRING_ARRAY = new String[0];

    public BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder holder,
            ParserContext ctx) {
        String initializerBeanName = registerJCacheInitializer(source, ctx);
        createDependencyOnJCacheInitializer(holder, initializerBeanName);
        return holder;
    }

    private void createDependencyOnJCacheInitializer(BeanDefinitionHolder holder,
            String initializerBeanName) {
        AbstractBeanDefinition definition = ((AbstractBeanDefinition) holder.getBeanDefinition());
        String[] dependsOn = definition.getDependsOn();
        if (dependsOn == null) {
            dependsOn = new String[]{initializerBeanName};
        } else {
            List dependencies = new ArrayList(Arrays.asList(dependsOn));
            dependencies.add(initializerBeanName);
            dependsOn = (String[]) dependencies.toArray(EMPTY_STRING_ARRAY);
        }
        definition.setDependsOn(dependsOn);
    }

    private String registerJCacheInitializer(Node source, ParserContext ctx) {
        String cacheName = ((Attr) source).getValue();
        String beanName = cacheName + "-initializer";
        if (!ctx.getRegistry().containsBeanDefinition(beanName)) {
            BeanDefinitionBuilder initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer.class);
            initializer.addConstructorArg(cacheName);
            ctx.getRegistry().registerBeanDefinition(beanName, initializer.getBeanDefinition());
        }
        return beanName;
    }

}

最后,需要在 Spring XML 基础结构中注册各种 artifact。

# in 'META-INF/spring.handlers'
http\://www.foo.com/schema/jcache=com.foo.JCacheNamespaceHandler
# in 'META-INF/spring.schemas'
http\://www.foo.com/schema/jcache/jcache.xsd=com/foo/jcache.xsd

42.8 更多资源

以下链接指向有关 XML Schema 的更多资源以及本章中描述的可扩展 XML 支持。

  • XML Schema 第 1 部分:结构第二版

  • XML Schema 第 2 部分:数据类型第二版