9.2创建XML Schemas

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

为了便于创作使用模式感知XML编辑器的配置文件,Spring的可扩展XML配置机制基于XML Schema。如果你不熟悉Spring标准Spring发行版附带的当前XML配置扩展,则应首先阅读标题为[xsd-config]的附录。

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

(我们在本附录后面会包含更详细的示例。第一个简单示例的目的是引导你完成制作自定义扩展的基本步骤。)

9.2.1 编写 Schema

创建用于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>

前面的模式允许我们使用<myns:dateformat />元素直接在XML应用程序上下文文件中配置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-HH-dd HH:mm"/>
<property name="lenient" value="true"/>
</bean>

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

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

9.2.2 编写NamespaceHandler

除了schema之外,我们还需要一个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支持的范围一起使用。我们首先展示一个简单的例子,不使用decoration,之后我们在一个更高级的例子中展示decoration。

虽然你可以为整个命名空间编写自己的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仅包含用于解析单个自定义元素的逻辑,我们可以在下一步中看到。

9.2.3 使用BeanDefinitionParser

如果NamespaceHandler遇到已映射到特定bean定义解析器的类型的XML元素(在本例中为dateformat),则使用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 {
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));
}
}
}

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

9.2.4 注册Handler和Schema

编码完成。剩下要做的就是让Spring XML解析基础架构了解我们的自定义元素。我们通过在两个专用属性文件中注册我们的自定义namespaceHandler和自定义XSD文件来实现。这些属性文件都放在应用程序的META-INF目录中,例如,可以与JAR文件中的二进制类一起分发。 Spring XML解析基础结构通过使用这些特殊属性文件自动获取新扩展,其格式将在接下来的两节中详细介绍。

编写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属性的值完全匹配。

写'META-INF/spring.schemas'

名为spring.schemas的属性文件包含XML Schema位置的映射(在模式声明中,在使用该模式作为xsi:schemaLocation属性的一部分的XML文件中引用)到类路径资源。需要此文件来防止Spring绝对必须使用需要Internet访问权限的默认EntityResolver来检索模式文件。如果在此属性文件中指定映射,Spring将在类路径中搜索模式(在本例中为org.springframework.samples.xml包中的myns.xsd)。以下代码段显示了我们需要为自定义架构添加的行:

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

(请记住:必须转义:字符。)

建议你在类路径上与NamespaceHandler和BeanDefinitionParser类一起部署XSD文件(或多个文件)。

9.2.5 使用Spring XML Configuration的自定义扩展

使用自己实现的自定义扩展与使用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 https://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>

9.2.6 更多例子

本节介绍自定义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.com/schema/component"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://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定义很困难(或者更不可能)。 以下清单显示了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> ();
// 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属性。 以下清单显示了这样的自定义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.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。 请记住,我们正在创建一个描述ComponentFactoryBean的BeanDefinition。 以下清单显示了我们的自定义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<BeanDefinition>(childElements.size());
for (Element element : childElements) {
children.add(parseComponentElement(element));
}
factory.addPropertyValue("children", children);
}
}

最后,需要通过修改META-INF/spring.handlers和META-INF/spring.schemas文件,在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

自定义Normal Elements的属性

编写自己的自定义解析器和相关工件并不难。 但是,有时这不是正确的做法。 考虑需要向现有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。 以下清单显示了我们的JCacheInitializer:

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。 以下清单显示了我们的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/spring.handlers和META-INF /spring.schemas文件来注册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