42. 可扩展的 XML 创作

42.1 Introduction

从 2.0 版开始,Spring 引入了一种机制,该机制可对基于 Spring 的基本 XML 格式进行基于架构的扩展,以定义和配置 bean。本节专门介绍如何编写自己的自定义 XML Bean 定义解析器并将这些解析器集成到 Spring IoC 容器中。

为了便于使用支持模式的 XML 编辑器创作配置文件,Spring 的可扩展 XML 配置机制基于 XML Schema。如果您不熟悉标准 Spring 发行版随附的 Spring 当前的 XML 配置扩展,请首先阅读标题为???的附录。

可以通过执行以下(相对)简单的步骤来创建新的 XML 配置扩展:

  • Authoring XML 模式,用于描述您的自定义元素。

  • Coding自定义NamespaceHandler实现(这是一个简单的步骤,请放心)。

  • Coding个或多个BeanDefinitionParser个实现(这是完成实际工作的地方)。

  • 用 Spring Registering上面的工件(这也是一个简单的步骤)。

以下是对每个步骤的描述。对于该示例,我们将创建一个 XMLextensions(一个自定义 XML 元素),该 extensions 使我们可以轻松地配置SimpleDateFormat类型的对象(来自java.text包)。完成后,我们将能够定义SimpleDateFormat类型的 bean 定义,如下所示:

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

(不必担心这个示例非常简单;随后将有更详细的示例.此第一个简单示例的目的是引导您完成所涉及的基本步骤.)

42.2 编写架构

创建用于 Spring 的 IoC 容器的 XML 配置扩展首先要编写 XML Schema 来描述扩展。以下是用于配置SimpleDateFormat对象的架构。

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

(强调的行包含了所有可识别标签的扩展基础(意味着它们具有id属性,将用作容器中的 bean 标识符)。由于导入了 Spring 提供的'beans',因此我们可以使用此属性命名空间。)

上面的模式将用于直接使用<myns:dateformat/>元素在 XML 应用程序上下文文件中配置SimpleDateFormat对象。

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

请注意,在创建基础结构类之后,上面的 XML 代码段基本上与下面的 XML 代码段相同。换句话说,我们只是在容器中创建一个 Bean,以类型SimpleDateFormat的名称'dateFormat'标识,并设置了几个属性。

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

Note

创建配置格式的基于模式的方法允许与具有模式识别 XML 编辑器的 IDE 紧密集成。使用正确编写的架构,可以使用自动完成功能来让用户在枚举中定义的多个配置选项之间进行选择。

42.3 编码 NamespaceHandler

除了模式,我们还需要一个NamespaceHandler来解析 Spring 在解析配置文件时遇到的该特定名称空间的所有元素。在我们的情况下,NamespaceHandler应该负责myns:dateformat元素的解析。

NamespaceHandler接口非常简单,因为它仅具有三种方法:

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

  • BeanDefinition parse(Element, ParserContext)-在 Spring 遇到顶级元素(未嵌套在 bean 定义或其他命名空间中)时调用。此方法可以自己注册 bean 定义和/或返回 bean 定义。

  • BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext)-当 Spring 遇到另一个名称空间的属性或嵌套元素时调用。一个或多个 bean 定义的修饰例如与Spring 2.0 支持的现成作用域一起使用。我们将从突出显示一个简单的示例而不使用装饰开始,此后,我们将在一个更高级的示例中显示装饰。

尽管完全可以为整个名称空间编写自己的NamespaceHandler(从而提供解析该名称空间中每个元素的代码),但是通常情况下,Spring XML 配置文件中的每个顶级 XML 元素都会导致一个单一的 bean 定义(在我们的例子中,单个<myns:dateformat/>元素导致一个SimpleDateFormat bean 定义)。 Spring 提供了许多支持这种情况的便利类。在此示例中,我们将使用NamespaceHandlerSupport类:

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 会注意到,在此类中实际上没有很多解析逻辑。确实……NamespaceHandlerSupport类具有内置的委托概念。它支持注册任意数量的BeanDefinitionParser实例,当需要解析其命名空间中的元素时,将委托该实例注册。这种清晰的关注点分离使NamespaceHandler可以处理其命名空间中所有自定义元素的解析的编排,同时委派BeanDefinitionParsers来完成 XML 解析的繁重工作;这意味着每个BeanDefinitionParser将仅包含解析单个自定义元素的逻辑,正如我们在下一步中看到的那样

42.4 BeanDefinitionParser

如果NamespaceHandler遇到已 Map 到特定 bean 定义解析器(在这种情况下为'dateformat')的类型的 XML 元素,则将使用BeanDefinitionParser。换句话说,BeanDefinitionParser负责解析模式中定义的一个不同的顶级 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 { (1)

    protected Class getBeanClass(Element element) {
        return SimpleDateFormat.class; (2)
    }

    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) 我们使用 Spring 提供的AbstractSingleBeanDefinitionParser来处理创建* single * BeanDefinition的许多基本工作。
  • (2) 我们为AbstractSingleBeanDefinitionParser超类提供了我们的单个BeanDefinition将代表的类型。

在这种简单的情况下,这就是我们要做的全部。 BeanDefinition的创建由AbstractSingleBeanDefinitionParser超类处理,bean 定义的唯一标识符的提取和设置也是如此。

42.5 注册处理程序和架构

编码完成!剩下要做的就是以某种方式使 Spring XML 解析基础结构意识到我们的自定义元素。为此,我们在两个特殊用途的属性文件中注册了自定义namespaceHandler和自定义 XSD 文件。这些属性文件都放置在应用程序的'META-INF'目录中,并且可以例如与 JAR 文件中的二进制类一起分发。 Spring XML 解析基础结构将通过使用这些特殊的属性文件来自动选择您的新扩展,这些文件的格式在下面详细说明。

42.5.1 'META-INF/spring.handlers'

名为'spring.handlers'的属性文件包含 XML 模式 URI 到名称空间处理程序类的 Map。因此,对于我们的示例,我们需要编写以下内容:

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

(':'字符是 Java 属性格式的有效分隔符,因此 URI 中的':'字符需要用反斜杠转义.)

键值对的第一部分(键)是与您的自定义名称空间扩展关联的 URI,并且需要“完全匹配”自定义 XSD 架构中指定的'targetNamespace'属性的值。

42.5.2 'META-INF/spring.schemas'

名为'spring.schemas'的属性文件包含 XML 模式位置的 Map(在 XML 文件中使用模式作为'xsi:schemaLocation'属性的一部分,在 XML 文件中称为模式声明)到* classpath *资源。需要该文件来防止 Spring 绝对使用默认的EntityResolver,该默认EntityResolver需要 Internet 访问才能检索架构文件。如果在此属性文件中指定 Map,Spring 将在 Classpath(在本例中为'org.springframework.samples.xml'包中的'myns.xsd')上搜索架构:

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

这样做的结果是鼓励您在 Classpath 上的NamespaceHandlerBeanDefinitionParser类旁边部署 XSD 文件。

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

使用您自己实现的自定义扩展与使用 Spring 直接提供的“自定义”扩展之一没有什么不同。在下面找到一个在 Spring XML 配置文件中使用在先前步骤中开发的自定义<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 扩展的示例。

42.7.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: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>

上面的配置实际上将自定义扩展嵌套在彼此之间。上面的<foo:component/>元素实际配置的类是Component类(直接在下面显示)。请注意Component类如何公开'components'属性的 setter 方法;这使得很难(或者几乎不可能)使用 setter 注入为Component类配置 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,该自定义FactoryBean公开'components'属性的 setter 属性。

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 管道。我们要做的是编写一个自定义 extensions,以隐藏所有此 Spring 管道。如果我们坚持前面描述的步骤,那么我们将首先创建 XSD 模式以定义自定义标记的结构。

<?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 基础架构注册各种工件。

# 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“普通”元素上的自定义属性

编写您自己的自定义解析器和关联的工件并不难,但是有时候这样做是不正确的。考虑需要将元数据添加到已经存在的 bean 定义的场景。在这种情况下,您当然不需要离开并编写自己的整个自定义扩展。而是只想向现有的 bean 定义元素添加一个附加属性。

举另一个例子,假设您正在为将(不为人所知)访问集群JCache的服务对象定义 bean 定义的服务类,并且您想确保已在其中快速启动命名的 JCache 实例。周围的集群:

<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 初始化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 模式(在这种情况下非常容易)。

<?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 基础架构注册各种工件。

# 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 支持的更多资源的链接。