42. 可扩展的XML创作

42.1 简介

从版本2.0开始,Spring为基于模式的Spring XML格式扩展提供了一种机制,用于定义和配置bean。本节将详细介绍如何编写自己的自定义XML bean定义解析器以及将这些解析器集成到Spring IoC容器中。

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

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

  • Authoring 用于描述自定义元素的XML架构。

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

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

  • Registering 上面有Spring的文物(这也是一个简单的步骤)。

以下是对这些步骤的描述。例如,我们将创建一个XML扩展(一个自定义XML元素),允许我们以简单的方式配置 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>

基于模式的创建配置格式的方法允许与具有模式感知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遇到不同命名空间的属性或嵌套元素时调用。例如,使用 out-of-the-box scopes Spring 2.0 supports 来装饰一个或多个bean定义。我们首先突出一个简单的例子,不使用装饰,之后我们将在一个更高级的例子中展示装饰。

尽管完全可以为整个命名空间编写自己的 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());
    }

}

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

42.4 BeanDefinitionParser

如果 NamespaceHandler 遇到已映射到特定bean定义解析器的类型的XML元素,则将使用 BeanDefinitionParser (这是 'dateformat' 在这种情况下)。换句话说, 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 { 

    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提供的 AbstractSingleBeanDefinitionParser 来处理创建单个 BeanDefinition 的大量基本工作。

我们提供的 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 Schema URI到命名空间处理程序类的映射。因此,对于我们的示例,我们需要编写以下内容:

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模式位置(与XML文件中的模式声明一起使用,该模式使用模式作为 'xsi:schemaLocation' 属性的一部分)到类路径资源的映射。需要此文件来防止Spring绝对必须使用需要Internet访问权限的默认 EntityResolver 来检索模式文件。如果在此属性文件中指定映射,Spring将在类路径中搜索模式(在本例中为 'org.springframework.samples.xml' 包中的 'myns.xsd' ):

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

这样做的结果是,我们鼓励您在类路径上的 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 ,它公开 '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管道给最终用户。我们要做的是编写一个隐藏所有Spring管道的自定义扩展。如果我们坚持 the steps described previously ,我们将首先创建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中添加其他属性定义元素。

通过另一个例子,假设您正在为服务对象定义bean定义的服务类(它不知道)正在访问集群 JCache ,并且您希望确保命名的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支持。

Updated at: 5 months ago
41.2.12. beans架构Table of content43. spring JSP Tag Library
Comment
You are not logged in.

There are no comments.