跳到主要内容

XML Schema 编写

ChatGPT-4o-mini 中英对照 XML Schema Authoring

自 2.0 版本以来,Spring 提供了一种机制,可以向基本的 Spring XML 格式添加基于模式的扩展,以定义和配置 bean。本节将介绍如何编写自定义的 XML bean 定义解析器,并将这些解析器集成到 Spring IoC 容器中。

为了方便使用支持模式的 XML 编辑器编写配置文件,Spring 的可扩展 XML 配置机制基于 XML Schema。如果您不熟悉与标准 Spring 发行版一起提供的 Spring 当前 XML 配置扩展,您应该首先阅读上一节关于 XML Schemas 的内容。

要创建新的 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"/>
xml

(我们在本附录后面包含了更详细的示例。这个简单示例的目的是引导您了解制作自定义扩展的基本步骤。)

编写模式

为 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>
xml
  • 指定的行包含所有可识别标签的扩展基础(意味着它们具有一个 id 属性,我们可以将其用作容器中的 bean 标识符)。我们可以使用这个属性,因为我们导入了 Spring 提供的 beans 命名空间。

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

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

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

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

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

备注

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

编写一个 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 支持的作用域 一起使用。我们首先突出一个简单的例子,不使用装饰,然后在稍微更高级的例子中展示装饰。

虽然您可以为整个命名空间编写自己的 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());
}
}
java

您可能会注意到,这个类中实际上没有很多解析逻辑。确实,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) {
// 这将永远不会为 null,因为 schema 明确要求提供一个值
String pattern = element.getAttribute("pattern");
bean.addConstructorArgValue(pattern);

// 然而这是一个可选属性
String lenient = element.getAttribute("lenient");
if (StringUtils.hasText(lenient)) {
bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
}
}

}
java
  • 我们使用 Spring 提供的 AbstractSingleBeanDefinitionParser 来处理创建单个 BeanDefinition 的许多基本工作。

  • 我们向 AbstractSingleBeanDefinitionParser 超类提供我们单个 BeanDefinition 所代表的类型。

在这个简单的案例中,这就是我们需要做的全部。我们单个 BeanDefinition 的创建由 AbstractSingleBeanDefinitionParser 超类处理,正如 bean 定义的唯一标识符的提取和设置。

注册处理程序和模式

编码已经完成。剩下的工作就是让 Spring XML 解析基础设施意识到我们的自定义元素。我们通过在两个特殊用途的属性文件中注册我们的自定义 namespaceHandler 和自定义 XSD 文件来实现这一点。这些属性文件都放置在应用程序的 META-INF 目录中,可以例如与您的二进制类一起分发到 JAR 文件中。Spring XML 解析基础设施会通过读取这些特殊属性文件自动识别您的新扩展,下一节将详细介绍这些文件的格式。

编写 META-INF/spring.handlers

名为 spring.handlers 的属性文件包含 XML Schema URI 到命名空间处理器类的映射。对于我们的示例,我们需要编写以下内容:

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

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

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

写入 'META-INF/spring.schemas'

名为 spring.schemas 的属性文件包含 XML Schema 位置的映射(在使用该模式的 XML 文件中,通过 xsi:schemaLocation 属性引用该模式声明)。此文件是必需的,以防止 Spring 必须使用一个默认的 EntityResolver,该解析器需要 Internet 访问来检索模式文件。如果您在此属性文件中指定映射,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>
xml
  • 我们的自定义 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>
xml

前面的配置将自定义扩展嵌套在一起。实际上由 <foo:component/> 元素配置的类是 Component 类(在下一个示例中显示)。请注意,Component 类没有为 components 属性公开一个设置方法。这使得通过使用设置器注入来配置 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;
}
}
java

解决此问题的典型方案是创建一个自定义 FactoryBean,该 FactoryBeancomponents 属性公开一个 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;
}
}
java

这很好用,但它向最终用户暴露了很多 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>
xml

再次按照 之前描述的过程,我们创建一个自定义 NamespaceHandler

package com.foo;

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

public class ComponentNamespaceHandler extends NamespaceHandlerSupport {

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

接下来是自定义的 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);
}
}
java

最后,必须通过修改 META-INF/spring.handlersMETA-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>
xml

我们可以在解析 'jcache:cache-name' 属性时创建另一个 BeanDefinition。这个 BeanDefinition 然后为我们初始化命名的 JCache。我们还可以修改现有的 'checkingAccountService'BeanDefinition,使其依赖于这个新的 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...
}
}
java

现在我们可以进入自定义扩展部分。首先,我们需要编写描述自定义属性的 XSD 架构,如下所示:

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

接下来,我们需要创建相关的 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());
}

}
java

接下来,我们需要创建解析器。请注意,在这种情况下,由于我们要解析一个 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;
}
}
java

最后,我们需要通过修改 META-INF/spring.handlersMETA-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