跳到主要内容
版本:7.0.3

XML模式设计

Hunyuan 7b 中英对照 XML Schema Authoring

从2.0版本开始,Spring引入了一种机制,允许在基本的Spring XML格式中添加基于模式的扩展,以便定义和配置Bean。本节将介绍如何编写自己的自定义XML Bean定义解析器,并将这些解析器集成到Spring IoC容器中。

为了便于使用支持模式识别的XML编辑器来编写配置文件,Spring的可扩展XML配置机制是基于XML Schema的。如果您不熟悉标准Spring发行版中包含的当前XML配置扩展功能,那么您应该首先阅读前面关于XML模式的部分。

要创建新的XML配置扩展:

  1. 作者 一个XML模式,用于描述您的自定义元素。

  2. 代码 一个自定义的NamespaceHandler实现。

  3. 代码 一个或多个BeanDefinitionParser实现(实际工作在这里完成)。

  4. 注册 将您的新工件注册到Spring中。

为了给出一个统一的示例,我们创建了一个XML扩展(一个自定义的XML元素),它允许我们配置SimpleDateFormat类型的对象(来自java.text包)。完成后,我们将能够如下定义SimpleDateFormat类型的bean:

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

(我们在这个附录的后面提供了更多详细的示例。这个第一个简单示例的目的是引导你了解创建自定义扩展的基本步骤。)

编写模式

要创建一个用于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.example/schema/myns"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
targetNamespace="http://www.mycompany.example/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"> // <1>
<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命名空间。

前面的模式允许我们直接在XML应用程序上下文文件中使用<myns:dateformat/>元素来配置SimpleDateFormat对象,如下例所示:

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

请注意,在我们创建了基础设施类之后,前面的XML代码片段与以下XML代码片段基本上是相同的:

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

前两段代码中的第二段会在容器中创建一个bean(其名称为dateFormat,类型为SimpleDateFormat),并设置了一些属性。

备注

基于模式的方法来创建配置格式,可以与具有模式感知的XML编辑器的IDE实现紧密集成。通过使用正确编写的模式,你可以利用自动完成功能,让用户从枚举中定义的多个配置选项中进行选择。

编写一个 NamespaceHandler

除了模式(schema)之外,我们还需要一个NamespaceHandler来解析Spring在解析配置文件时遇到的这个特定命名空间(namespace)中的所有元素。对于这个例子来说,NamespaceHandler应该负责解析myns:dateformat元素。

NamespaceHandler接口有三个方法:

  • init(): 用于初始化NamespaceHandler,在处理器被使用之前由Spring调用。

  • BeanDefinition parse(Element, ParserContext): 当Spring遇到顶级元素(不位于bean定义或不同命名空间内部时)时会被调用。此方法可以本身注册bean定义,返回一个bean定义,或者同时执行这两项操作。

  • BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext): 当Spring遇到属于不同命名空间的属性或嵌套元素时会被调用。一个或多个bean定义的装饰会用于(例如)Spring支持的Scope。我们首先给出一个不使用装饰的简单示例,之后会通过一个稍微复杂一些的示例来展示装饰的使用方法。

虽然你可以为整个命名空间编写自己的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能够负责其命名空间中所有自定义元素的解析协调工作,而将XML解析的实质性工作委托给BeanDefinitionParsers来完成。这意味着每个BeanDefinitionParser只包含解析单个自定义元素的逻辑,我们可以在接下来的步骤中看到这一点。

使用 BeanDefinitionParser

如果NamespaceHandler遇到一个已映射到特定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.addConstructorArgValue(pattern);

// However, this is an optional property
String lenient = element.getAttribute("lenient");
if (StringUtils.hasText(lenient)) {
bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
}
}
}
  • We use Spring-provided AbstractSingleBeanDefinitionParser to handle a lot of the basic work involved in creating a single BeanDefinition.

  • We provide the AbstractSingleBeanDefinitionParser superclass with the type that our BeanDefinition represents.

在这个简单的案例中,我们需要做的就是这些。我们单个BeanDefinition的创建由AbstractSingleBeanDefinitionParser超类来处理,同样,bean定义的唯一标识符的提取和设置也由该超类完成。

注册处理程序和模式

编码工作已经完成。接下来要做的就是让Spring XML解析框架能够识别我们的自定义元素。我们通过在两个专门的属性文件中注册我们的自定义namespaceHandler和自定义XSD文件来实现这一目标。这两个属性文件都放在应用程序的META-INF目录下,例如,可以与二进制类一起作为JAR文件的组成部分进行分发。Spring XML解析框架会自动识别到这个新的扩展名,因为它会读取这些特殊的属性文件,而这些文件的格式将在接下来的两个部分中详细说明。

编写 META-INF/springhandlers

名为springhandlers的属性文件包含了XML Schema URI与命名空间处理类之间的映射关系。以我们的示例来说,我们需要编写以下内容:

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

(冒号(:)字符在Java属性格式中是有效的分隔符,因此在URI中的冒号需要用反斜杠(\)进行转义。)

键值对的第一部分(即“key”)是与您的自定义命名空间扩展关联的URI,它需要与您的自定义XSD模式中指定的targetNamespace属性的值完全匹配。

编写 'META-INF/spring.schemas'

名为spring.schemas的属性文件包含了XML模式位置(与模式声明一起,在使用该模式的XML文件中通过xsi:schemaLocation属性引用)到类路径资源的映射。这个文件是必要的,可以防止Spring不得不使用默认的EntityResolver,而默认的EntityResolver需要互联网访问来检索模式文件。如果你在这个属性文件中指定了这种映射,Spring就会在类路径上搜索该模式(在本例中,是org.springframework.samples.xml包中的myns.xsd)。以下代码片段展示了我们需要添加的行,用于我们的自定义模式:

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

(请记住:: 这个字符必须进行转义。)

我们建议你将XSD文件(或多个文件)与NamespaceHandlerBeanDefinitionParser类一起部署在类路径上。

在您的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.example/schema/myns"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.mycompany.example/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"/> // <1>

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

</beans>
  • 我们的自定义Bean。

更详细的示例

本节提供了一些更详细的自定义XML扩展示例。

在自定义元素内部嵌套自定义元素

本节中提供的示例展示了如何编写满足以下配置目标所需的各种工件:

<?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.example/schema/component"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.foo.example/schema/component http://www.foo.example/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定义变得困难(或者更确切地说,是不可能的)。以下代码展示了 Component 类:

package com.foo;

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

public class Component {

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

// 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属性。以下代码展示了这样的自定义FactoryBean

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模式来定义我们自定义标签的结构,如下例所示:

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

<xsd:schema xmlns="http://www.foo.example/schema/component"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.foo.example/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。以下代码展示了我们的自定义 BeanDefinitionParser 实现:

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<>(childElements.size());
for (Element element : childElements) {
children.add(parseComponentElement(element));
}
factory.addPropertyValue("children", children);
}
}

最后,需要通过修改META-INF/springhandlersMETA-INF/spring.schemas文件,将这些不同的工件注册到Spring XML基础设施中,具体操作如下:

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

“普通”元素上的自定义属性

编写自己的自定义解析器及其相关组件并不困难。然而,有时这样做并非最佳选择。设想这样一种情况:你需要为已存在的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。我们也可以修改现有的BeanDefinition(用于“checkingAccountService”),使其依赖于这个新的、用于初始化JCache的BeanDefinition。以下代码展示了我们的JCacheInitializer

package com.foo;

public class JCacheInitializer {

private final 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.example/schema/jcache"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.foo.example/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。以下代码展示了我们的BeanDefinitionDecorator实现:

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

最后,我们需要通过修改META-INF/springHandlersMETA-INF/spring.schemas文件来将各种工件注册到Spring XML基础设施中,具体操作如下:

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