XML Schema Authoring

  • XSD 架构:描述自定义元素的结构和属性。

  • NamespaceHandler:处理自定义命名空间中的 XML 元素,并委托给 BeanDefinitionParsers。

  • BeanDefinitionParser:解析单个自定义 XML 元素并创建一个相应的 BeanDefinition。

使用这些组件,可以实现各种扩展场景,例如:

  • 创建嵌套的自定义元素。

  • 在普通元素上添加自定义属性。

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

Since version 2.0, Spring has featured a mechanism for adding schema-based extensions to the basic Spring XML format for defining and configuring beans. This section covers how to write your own custom XML bean definition parsers and integrate such parsers into the Spring IoC container.

为了便于编写使用感知模式的 XML 编辑器的配置文件,Spring 可扩展的 XML 配置机制基于 XML 模式。如果你不熟悉标配 Spring 发行版附带的 Spring 当前 XML 配置扩展,则应首先阅读 XML Schemas 一节中有关其的介绍。

To facilitate authoring configuration files that use a schema-aware XML editor, Spring’s extensible XML configuration mechanism is based on XML Schema. If you are not familiar with Spring’s current XML configuration extensions that come with the standard Spring distribution, you should first read the previous section on XML Schemas.

如要创建新的 XML 配置扩展:

To create new XML configuration extensions:

  1. Author an XML schema to describe your custom element(s).

  2. Code a custom NamespaceHandler implementation.

  3. Code one or more BeanDefinitionParser implementations (this is where the real work is done).

  4. Register your new artifacts with Spring.

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

For a unified example, we create an XML extension (a custom XML element) that lets us configure objects of the type SimpleDateFormat (from the java.text package). When we are done, we will be able to define bean definitions of type SimpleDateFormat as follows:

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

(我们稍后将在本附录中包含更加详细的示例。此第一个简单示例的目的是带你了解创建自定义扩展的基本步骤。)

(We include much more detailed examples follow later in this appendix. The intent of this first simple example is to walk you through the basic steps of making a custom extension.)

Authoring the Schema

创建用于与 Spring 的 IoC 容器一起使用的 XML 配置扩展首先从创作一个 XML Schema 以描述扩展开始。对于我们的示例,我们使用以下架构来配置 SimpleDateFormat 对象:

Creating an XML configuration extension for use with Spring’s IoC container starts with authoring an XML Schema to describe the extension. For our example, we use the following schema to configure SimpleDateFormat objects:

<!-- 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>
1 The indicated line contains an extension base for all identifiable tags (meaning they have an id attribute that we can use as the bean identifier in the container). We can use this attribute because we imported the Spring-provided beans namespace.

前面的架构让我们能够通过使用 myns:dateformat 元素直接在一个 XML 应用程序上下文文件配置 SimpleDateFormat 对象,如下面的示例所示:

The preceding schema lets us configure SimpleDateFormat objects directly in an XML application context file by using the <myns:dateformat/> element, as the following example shows:

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

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

Note that, after we have created the infrastructure classes, the preceding snippet of XML is essentially the same as the following XML snippet:

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

前面两个片段的第二个片段会在容器中创建(由简称为`SimpleDateFormat` 的名称 dateFormat 识别)一个 bean,其中设置了一些属性。

The second of the two preceding snippets creates a bean in the container (identified by the name dateFormat of type SimpleDateFormat) with a couple of properties set.

用于创建配置格式的基于架构的方法支持与具有识别架构的 XML 编辑器的 IDE 的紧密集成。通过使用经适当地创作的架构,您可以使用自动完成功能使用户在枚举中定义的不同配置选项之间进行选择。

The schema-based approach to creating configuration format allows for tight integration with an IDE that has a schema-aware XML editor. By using a properly authored schema, you can use autocompletion to let a user choose between several configuration options defined in the enumeration.

Coding a NamespaceHandler

除了架构之外,我们需要一个 NamespaceHandler 来解析 Spring 在解析配置文件时遇到的此特定命名空间的所有元素。对于此示例,NamespaceHandler 应当负责解析 myns:dateformat 元素。

In addition to the schema, we need a NamespaceHandler to parse all elements of this specific namespace that Spring encounters while parsing configuration files. For this example, the NamespaceHandler should take care of the parsing of the myns:dateformat element.

NamespaceHandler 接口具有三种方法:

The NamespaceHandler interface features three methods:

  • init(): Allows for initialization of the NamespaceHandler and is called by Spring before the handler is used.

  • BeanDefinition parse(Element, ParserContext): Called when Spring encounters a top-level element (not nested inside a bean definition or a different namespace). This method can itself register bean definitions, return a bean definition, or both.

  • BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext): Called when Spring encounters an attribute or nested element of a different namespace. The decoration of one or more bean definitions is used (for example) with the scopes that Spring supports. We start by highlighting a simple example, without using decoration, after which we show decoration in a somewhat more advanced example.

虽然你可以为整个命名空间编写自己的 NamespaceHandler(因而提供解析该命名空间中的每个元素的代码),但经常会出现 Spring XML 配置文件中的每个顶层 XML 元素而导致一个 bean 定义(例如我们的 myns:dateformat 元素导致了一个 SimpleDateFormat bean 定义)。Spring 具有支持此方案的许多便捷类。在以下示例中,我们使用 NamespaceHandlerSupport 类:

Although you can code your own NamespaceHandler for the entire namespace (and hence provide code that parses each and every element in the namespace), it is often the case that each top-level XML element in a Spring XML configuration file results in a single bean definition (as in our case, where a single <myns:dateformat/> element results in a single SimpleDateFormat bean definition). Spring features a number of convenience classes that support this scenario. In the following example, we use the NamespaceHandlerSupport class:

  • Java

  • Kotlin

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

public class MyNamespaceHandler extends NamespaceHandlerSupport {

	public void init() {
		registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
	}
}
import org.springframework.beans.factory.xml.NamespaceHandlerSupport

class MyNamespaceHandler : NamespaceHandlerSupport {

	override fun init() {
		registerBeanDefinitionParser("dateformat", SimpleDateFormatBeanDefinitionParser())
	}
}

你可能会注意到此类中实际上没有许多解析逻辑。事实上,NamespaceHandlerSupport 类具有一个内置的委托概念。它支持注册任意数量的 BeanDefinitionParser 实例,当它需要解析其命名空间中的一个元素时,它会委托给此类实例。此种清晰的分离方案让 NamespaceHandler 处理其命名空间中所有自定义元素的解析编排,同时委托给`BeanDefinitionParsers` 来执行 XML 解析的基本工作。这意味着每个 BeanDefinitionParser 仅包含用于解析单个自定义元素的逻辑,如我们在下一步中看到的。

You may notice that there is not actually a whole lot of parsing logic in this class. Indeed, the NamespaceHandlerSupport class has a built-in notion of delegation. It supports the registration of any number of BeanDefinitionParser instances, to which it delegates to when it needs to parse an element in its namespace. This clean separation of concerns lets a NamespaceHandler handle the orchestration of the parsing of all of the custom elements in its namespace while delegating to BeanDefinitionParsers to do the grunt work of the XML parsing. This means that each BeanDefinitionParser contains only the logic for parsing a single custom element, as we can see in the next step.

Using BeanDefinitionParser

如果 NamespaceHandler 遇到了已被映射到特定 bean 定义解析器(本例中为 dateformat)的类型的 XML 元素,则会用到 BeanDefinitionParser。换句话说,BeanDefinitionParser 负责解析架构中定义的某个不同的顶层 XML 元素。在解析器中,我们可以访问 XML 元素(因而也可以访问其子元素),以便能够解析我们的自定义 XML 内容,如你在以下示例中看到的:

A BeanDefinitionParser is used if the NamespaceHandler encounters an XML element of the type that has been mapped to the specific bean definition parser (dateformat in this case). In other words, the BeanDefinitionParser is responsible for parsing one distinct top-level XML element defined in the schema. In the parser, we' have access to the XML element (and thus to its subelements, too) so that we can parse our custom XML content, as you can see in the following example:

Java
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);

		// this however is an optional property
		String lenient = element.getAttribute("lenient");
		if (StringUtils.hasText(lenient)) {
			bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
		}
	}

}
1 We use the Spring-provided AbstractSingleBeanDefinitionParser to handle a lot of the basic grunt work of creating a single BeanDefinition.
2 We supply the AbstractSingleBeanDefinitionParser superclass with the type that our single BeanDefinition represents.
Kotlin
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

class SimpleDateFormatBeanDefinitionParser : AbstractSingleBeanDefinitionParser() { (1)

	override fun getBeanClass(element: Element): Class<*>? { (2)
		return SimpleDateFormat::class.java
	}

	override fun doParse(element: Element, bean: BeanDefinitionBuilder) {
		// this will never be null since the schema explicitly requires that a value be supplied
		val pattern = element.getAttribute("pattern")
		bean.addConstructorArgValue(pattern)

		// this however is an optional property
		val lenient = element.getAttribute("lenient")
		if (StringUtils.hasText(lenient)) {
			bean.addPropertyValue("lenient", java.lang.Boolean.valueOf(lenient))
		}
	}
}
3 We use the Spring-provided AbstractSingleBeanDefinitionParser to handle a lot of the basic grunt work of creating a single BeanDefinition.
4 We supply the AbstractSingleBeanDefinitionParser superclass with the type that our single BeanDefinition represents.

在此简单示例中,这是我们唯一需要执行的操作。创建我们的单个`BeanDefinition` 由父类 AbstractSingleBeanDefinitionParser 处理,bean 定义的唯一标识符的提取和设置也是如此。

In this simple case, this is all that we need to do. The creation of our single BeanDefinition is handled by the AbstractSingleBeanDefinitionParser superclass, as is the extraction and setting of the bean definition’s unique identifier.

Registering the Handler and the Schema

编码已完成。剩余的就是让 Spring XML 解析基础设施感知我们的自定义元素。我们通过在两个特殊用途属性文件中注册我们的自定义 namespaceHandler 和自定义 XSD 文件来执行此操作。这些属性文件都放置在你的应用程序中的 META-INF 目录中,并且可以例如随你 JAR 文件中的二进制类一起分发。Spring XML 解析基础设施通过使用这些特殊属性文件(它们的格式在下两节中详细介绍)自动选择你的新扩展。

The coding is finished. All that remains to be done is to make the Spring XML parsing infrastructure aware of our custom element. We do so by registering our custom namespaceHandler and custom XSD file in two special-purpose properties files. These properties files are both placed in a META-INF directory in your application and can, for example, be distributed alongside your binary classes in a JAR file. The Spring XML parsing infrastructure automatically picks up your new extension by consuming these special properties files, the formats of which are detailed in the next two sections.

Writing META-INF/spring.handlers

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

The properties file called spring.handlers contains a mapping of XML Schema URIs to namespace handler classes. For our example, we need to write the following:

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

: 字符是 Java 属性格式中的一个有效分隔符,因此 URI 中的`:` 字符需要使用反斜线转义。)

(The : character is a valid delimiter in the Java properties format, so : character in the URI needs to be escaped with a backslash.)

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

The first part (the key) of the key-value pair is the URI associated with your custom namespace extension and needs to exactly match exactly the value of the targetNamespace attribute, as specified in your custom XSD schema.

Writing 'META-INF/spring.schemas'

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

The properties file called spring.schemas contains a mapping of XML Schema locations (referred to, along with the schema declaration, in XML files that use the schema as part of the xsi:schemaLocation attribute) to classpath resources. This file is needed to prevent Spring from absolutely having to use a default EntityResolver that requires Internet access to retrieve the schema file. If you specify the mapping in this properties file, Spring searches for the schema (in this case, myns.xsd in the org.springframework.samples.xml package) on the classpath. The following snippet shows the line we need to add for our custom schema:

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

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

(Remember that the : character must be escaped.)

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

You are encouraged to deploy your XSD file (or files) right alongside the NamespaceHandler and BeanDefinitionParser classes on the classpath.

Using a Custom Extension in Your Spring XML Configuration

使用你自己实现的自定义扩展与使用 Spring 提供的“custom”扩展之一没有区别。以下示例在 Spring XML 配置文件中使用了前面步骤中开发的自定义 <dateformat/> 元素:

Using a custom extension that you yourself have implemented is no different from using one of the “custom” extensions that Spring provides. The following example uses the custom <dateformat/> element developed in the previous steps in a Spring XML configuration file:

<?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>
1 Our custom bean.

More Detailed Examples

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

This section presents some more detailed examples of custom XML extensions.

Nesting Custom Elements within Custom Elements

本节中给出的示例说明了如何编写要满足以下配置目标所需的各种神器:

The example presented in this section shows how you to write the various artifacts required to satisfy a target of the following configuration:

<?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 类:

The preceding configuration nests custom extensions within each other. The class that is actually configured by the <foo:component/> element is the Component class (shown in the next example). Notice how the Component class does not expose a setter method for the components property. This makes it hard (or rather impossible) to configure a bean definition for the Component class by using setter injection. The following listing shows the Component class:

  • Java

  • Kotlin

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;
	}
}
import java.util.ArrayList

class Component {

	var name: String? = null
	private val components = ArrayList<Component>()

	// there is no setter method for the 'components'
	fun addComponent(component: Component) {
		this.components.add(component)
	}

	fun getComponents(): List<Component> {
		return components
	}
}

此问题的典型解决方案是创建一个公开 components 属性的 setter 属性的自定义 FactoryBean。以下清单展示了这样的自定义 FactoryBean

The typical solution to this issue is to create a custom FactoryBean that exposes a setter property for the components property. The following listing shows such a custom FactoryBean:

  • Java

  • Kotlin

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;
	}
}
import org.springframework.beans.factory.FactoryBean
import org.springframework.stereotype.Component

class ComponentFactoryBean : FactoryBean<Component> {

	private var parent: Component? = null
	private var children: List<Component>? = null

	fun setParent(parent: Component) {
		this.parent = parent
	}

	fun setChildren(children: List<Component>) {
		this.children = children
	}

	override fun getObject(): Component? {
		if (this.children != null && this.children!!.isNotEmpty()) {
			for (child in children!!) {
				this.parent!!.addComponent(child)
			}
		}
		return this.parent
	}

	override fun getObjectType(): Class<Component>? {
		return Component::class.java
	}

	override fun isSingleton(): Boolean {
		return true
	}
}

它很好用,但会向终端用户暴露大量 Spring 管道。我们要做的是编写一个自定义扩展,将所有这些 Spring 管道隐藏起来。如果我们坚持使用 the steps described previously,那么我们首先要创建 XSD 模式,以此来定义自定义标记的结构,如下面清单所示:

This works nicely, but it exposes a lot of Spring plumbing to the end user. What we are going to do is write a custom extension that hides away all of this Spring plumbing. If we stick to the steps described previously, we start off by creating the XSD schema to define the structure of our custom tag, as the following listing shows:

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

the process described earlier一样,然后我们创建一个自定义 NamespaceHandler

Again following the process described earlier, we then create a custom NamespaceHandler:

  • Java

  • Kotlin

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

public class ComponentNamespaceHandler extends NamespaceHandlerSupport {

	public void init() {
		registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser());
	}
}
import org.springframework.beans.factory.xml.NamespaceHandlerSupport

class ComponentNamespaceHandler : NamespaceHandlerSupport() {

	override fun init() {
		registerBeanDefinitionParser("component", ComponentBeanDefinitionParser())
	}
}

接下来是自定义 BeanDefinitionParser。请记住,我们正在创建一个描述 ComponentFactoryBeanBeanDefinition。以下清单显示了我们的自定义 BeanDefinitionParser 实现:

Next up is the custom BeanDefinitionParser. Remember that we are creating a BeanDefinition that describes a ComponentFactoryBean. The following listing shows our custom BeanDefinitionParser implementation:

  • Java

  • Kotlin

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

class ComponentBeanDefinitionParser : AbstractBeanDefinitionParser() {

	override fun parseInternal(element: Element, parserContext: ParserContext): AbstractBeanDefinition? {
		return parseComponentElement(element)
	}

	private fun parseComponentElement(element: Element): AbstractBeanDefinition {
		val factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean::class.java)
		factory.addPropertyValue("parent", parseComponent(element))

		val childElements = DomUtils.getChildElementsByTagName(element, "component")
		if (childElements != null && childElements.size > 0) {
			parseChildComponents(childElements, factory)
		}

		return factory.getBeanDefinition()
	}

	private fun parseComponent(element: Element): BeanDefinition {
		val component = BeanDefinitionBuilder.rootBeanDefinition(Component::class.java)
		component.addPropertyValue("name", element.getAttribute("name"))
		return component.beanDefinition
	}

	private fun parseChildComponents(childElements: List<Element>, factory: BeanDefinitionBuilder) {
		val children = ManagedList<BeanDefinition>(childElements.size)
		for (element in childElements) {
			children.add(parseComponentElement(element))
		}
		factory.addPropertyValue("children", children)
	}
}

最后,需要通过修改 META-INF/spring.handlersMETA-INF/spring.schemas 文件,向 Spring XML 基础设施注册各种神器,如下所示:

Finally, the various artifacts need to be registered with the Spring XML infrastructure, by modifying the META-INF/spring.handlers and META-INF/spring.schemas files, as follows:

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

Custom Attributes on “Normal” Elements

编写自己的自定义解析器和相关神器并不难。但是,有时这样做并不是正确的选择。考虑一种场景,你需要向已存在的 Bean 定义添加元数据。在这种情况下,你肯定不想编写自己的整个自定义扩展名。相反,你只需要向现有的 Bean 定义元素添加一个附加属性。

Writing your own custom parser and the associated artifacts is not hard. However, it is sometimes not the right thing to do. Consider a scenario where you need to add metadata to already existing bean definitions. In this case, you certainly do not want to have to write your own entire custom extension. Rather, you merely want to add an additional attribute to the existing bean definition element.

通过另一个示例,假设你为服务对象定义了一个 Bean 定义,该对象(自己不知道)访问一个集群 https://www.jcp.org/en/jsr/detail?id=107 [JCache],并且你希望确保命名的 JCache 实例在周围集群中急切启动。以下清单显示了这样的定义:

By way of another example, suppose that you define a bean definition for a service object that (unknown to it) accesses a clustered JCache, and you want to ensure that the named JCache instance is eagerly started within the surrounding cluster. The following listing shows such a definition:

<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

We can then create another BeanDefinition when the ’jcache:cache-name'` attribute is parsed. This BeanDefinition then initializes the named JCache for us. We can also modify the existing BeanDefinition for the ’checkingAccountService'` so that it has a dependency on this new JCache-initializing BeanDefinition. The following listing shows our JCacheInitializer:

  • Java

  • Kotlin

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...
	}
}
class JCacheInitializer(private val name: String) {

	fun initialize() {
		// lots of JCache API calls to initialize the named cache...
	}
}

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

Now we can move onto the custom extension. First, we need to author the XSD schema that describes the custom attribute, as follows:

<?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,如下所示:

Next, we need to create the associated NamespaceHandler, as follows:

  • Java

  • Kotlin

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

public class JCacheNamespaceHandler extends NamespaceHandlerSupport {

	public void init() {
		super.registerBeanDefinitionDecoratorForAttribute("cache-name",
			new JCacheInitializingBeanDefinitionDecorator());
	}

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

class JCacheNamespaceHandler : NamespaceHandlerSupport() {

	override fun init() {
		super.registerBeanDefinitionDecoratorForAttribute("cache-name",
				JCacheInitializingBeanDefinitionDecorator())
	}

}

接下来,我们需要创建解析器。请注意,在这种情况下,因为我们要解析 XML 属性,所以我们编写 BeanDefinitionDecorator 而不是 BeanDefinitionParser。以下清单显示了我们的 BeanDefinitionDecorator 实现:

Next, we need to create the parser. Note that, in this case, because we are going to parse an XML attribute, we write a BeanDefinitionDecorator rather than a BeanDefinitionParser. The following listing shows our BeanDefinitionDecorator implementation:

  • Java

  • Kotlin

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

class JCacheInitializingBeanDefinitionDecorator : BeanDefinitionDecorator {

	override fun decorate(source: Node, holder: BeanDefinitionHolder,
						ctx: ParserContext): BeanDefinitionHolder {
		val initializerBeanName = registerJCacheInitializer(source, ctx)
		createDependencyOnJCacheInitializer(holder, initializerBeanName)
		return holder
	}

	private fun createDependencyOnJCacheInitializer(holder: BeanDefinitionHolder,
													initializerBeanName: String) {
		val definition = holder.beanDefinition as AbstractBeanDefinition
		var dependsOn = definition.dependsOn
		dependsOn = if (dependsOn == null) {
			arrayOf(initializerBeanName)
		} else {
			val dependencies = ArrayList(listOf(*dependsOn))
			dependencies.add(initializerBeanName)
			dependencies.toTypedArray()
		}
		definition.setDependsOn(*dependsOn)
	}

	private fun registerJCacheInitializer(source: Node, ctx: ParserContext): String {
		val cacheName = (source as Attr).value
		val beanName = "$cacheName-initializer"
		if (!ctx.registry.containsBeanDefinition(beanName)) {
			val initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer::class.java)
			initializer.addConstructorArg(cacheName)
			ctx.registry.registerBeanDefinition(beanName, initializer.getBeanDefinition())
		}
		return beanName
	}
}

最后,我们必需修改 META-INF/spring.handlersMETA-INF/spring.schemas 文件,以注册各种工件与 Spring XML 基础设施,如下所示:

Finally, we need to register the various artifacts with the Spring XML infrastructure by modifying the META-INF/spring.handlers and META-INF/spring.schemas files, as follows:

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