《Spring 5 官方文档》39. 创建可扩展的XML

原文链接 译者: flystarfly

39. 创作可扩展的XML

39.1 介绍

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

为了便于使用感知模式的XML编辑器创建配置文件,
Spring的可扩展XML配置机制基于XML Schema。如果你不熟悉
Spring当前发行版本一起发布的XML扩展配置,请先阅读附录???

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

  • 创作
    一个XML模式来描述您的自定义元素。
  • 编写 一个自定义的NamespaceHandler 实现类(不用担心,这步骤很简单)。
  • 编写一个或多个 BeanDefinitionParser 实现类
    (这里完成实际工作)。
  • 注册 以上项目到Spring(这步骤也很简单).

以下是每一个步骤的说明。 打个比方, 我们可以创造一个XML扩展(自定义XML元素),允许我们以简单的方式配置
SimpleDateFormat (来自 java.text package) 类型的对象。 当我们搞定之后,
我们就能够像这样来以SimpleDateFormat类型的定义方式来定义bean

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

(不要担心,这个例子很简单,之后会有更详细的例子。 这个简单的例子目的是为了引导您完成所涉及的基本步骤。)

39.2 编写模式

从编写XML模式开始创建用于Springs IoC容器的XML配置扩展名,来描述扩展信息。 我们将使用下面这个模式配置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>

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

上述模式将用于配置 SimpleDateFormat 对象, 直接在
XML应用程序上下文文件使用 <myns:dateformat/> 元件.

<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] Note

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

39.3 Coding a NamespaceHandler

除了模式,我们需要一个NamespaceHandler来解析Spring解析配置文件时遇到的特定命名空间的所有元素。
NamespaceHandler 会在我们的例子中仔细解析 myns:dateformat
元件。

The NamespaceHandler 接口非常简单,它只有三种方法:

  • init() – 允许初始化 NamespaceHandler 并且在使用处理程序之前将被Spring调用。
  • BeanDefinition parse(Element, ParserContext) -当Spring遇到一个
    顶级元素(不嵌套在bean定义或不同的命名空间中)的时候调用。 此方法可以自己注册bean定义和/或返回一个bean定义。
  • BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext)
    当Spring遇到不同命名空间的属性或嵌套元素时调用。一个或多个bean定义的装饰用于比如out-of-the-box scopes Spring 2.0 supports这个例子。 我们先举了一个不使用装饰简单的例子,之后我们会在一个更高级的例子中用上装饰。

虽然可以自己编写NamespaceHandler 对于整个命名空间(并因此提供分析命名空间中每个元素的代码),
通常情况下,Spring XML配置文件中的每个顶级XML元素
产生一个唯一的bean定义(就像我们这样一个唯一的 <myns:dateformat/>
元件在一个唯一的SimpleDateFormatbean定义中产生)。 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 只包含解析单个自定义元素的逻辑, 接下来我们将会看到这些

39.4 BeanDefinitionParser

A BeanDefinitionParser 将被用来当NamespaceHandler遇到已经映射到特定bean定义解析器(在这种情况下为'dateformat')的类型的XML元素)。 换句话说, the 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 类来处理许多基础且繁重的工作,创造一个单一 BeanDefinition

2

我们提供AbstractSingleBeanDefinitionParser的超类来作为我们单一BeanDefinition 的代表

在这个简单的例子中, 以上就是我们所要做的一切。
AbstractSingleBeanDefinitionParser 超类来创造我们自己的单一的BeanDefinition, bean定义的唯一标识符的提取和设置也是如此。

39.5 Registering the handler and the schema

编码完成了! 另外还需要做的是以某种方式使Spring XML解析基础架构了解我们的自定义元素; 我们会在在两个专用属性文件中注册自定义的
namespaceHandler 以及自定义的XSD文件来做这些。 这些属性文件都被放置在应用程序中的“META-INF”目录中,并且可以,打个比方,在JAR文件中与二进制类一起分发。 Spring XML解析基础架构将通过使用这些特殊属性文件自动获取您的新扩展名,其格式如下。

39.5.1 ‘META-INF/spring.handlers’

这个被称作 'spring.handlers'的属性文件包含XML Schema URI到命名空间处理程序类的映射。 所以在我们的例子中,我们需要写以下内容:

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

(The ':'字符是Java属性格式的有效分隔符,所以
':'字符在URI中需要使用反斜杠进行转义。)

键值对的第一部分(键)是与您的自定义命名空间扩展相关联的URI,并且需要匹配 在您的自定义XSD模式中指定的'targetNamespace'属性的值。

39.5.2 ‘META-INF/spring.schemas’

属性文件被称为'spring.schemas'包含XML Schema位置的映射
(在XML文件中引用模式声明,使用模式作为'xsi:schemaLocation'属性的一部分) to classpath资源。 该文件被用与
Spring 能够非必须地使用需要Internet访问的默认 EntityResolver 来检索模式文件。如果您在此属性文件中指定映射,Spring将在类路径中搜索模式(在这种情况下的'myns.xsd''org.springframework.samples.xml'包中):

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

这样做的结果是鼓励您部署XSD文件在
NamespaceHandler BeanDefinitionParser类的类路径上。

39.6 Using a custom extension in your Spring XML configuration

使用您自己已经实现的自定义扩展与使用Spring提供的开箱即用的’custom’扩展之一没有什么不同。在下面的一个示例中,使用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>

39.7 Meatier examples

下面有一些比较简单的自定义XML扩展示例。

39.7.1 Nesting custom tags within custom tags

此示例说明如何编写满足以下配置目标所需的各种工件:

<?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 方法; 这使得为 Component类使用setter注入来配置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管道。 如果我们坚持之前描述的步骤, 我们将首先创建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。 记住,我们正在创建的是一个
BeanDefinition来描述一个ComponentFactoryBean

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

39.7.2 Custom attributes on ‘normal’ elements

编写自己的自定义解析器和相关的工件不是很难,但有时候会出一些错误。 考虑您需要向现有的bean定义添加元数据的场景。 在这种情况下,您绝对不需要自己去完成自己的整个定制扩展; 而您只需要为现有的bean定义元素添加一个附加属性。

我们顺便写了另外一个例子,假设您为服务对象定义一个bean定义的服务类(将为其未知)正在访问集群JCache,并且您希望确保命名的JCache实例在内部快速启动 周围群集:

<bean id="checkingAccountService" class="com.foo.DefaultCheckingAccountService"
		jcache:cache-name="checking.account">
	<!-- other dependencies here... -->
</bean>

What we are going to do here is create another BeanDefinition when the
'jcache:cache-name' attribute is parsed; this BeanDefinition will then initialize
the named JCache for us. We will also modify the existing BeanDefinition for the
'checkingAccountService' so that it will have a dependency on this new
JCache-initializing 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>

Next, the associated 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

39.8 Further Resources

查找下面的链接,了解有关XML模式的更多资源以及本章中描述的可扩展XML支持。

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/99680.html

(0)
上一篇 2021年8月21日
下一篇 2021年8月21日

相关推荐

发表回复

登录后才能评论