Skip to content

XML Schema 授权 (XML Schema Authoring) ​

自 2.0 版本起,Spring 提供了一种机制,可以在基本的 Spring XML 格式之上添加基于 Schema 的扩展,用于定义和配置 Bean。本节涵盖了如何编写你自己的自定义 XML Bean 定义解析器,并将这些解析器集成到 Spring IoC 容器中。

为了方便使用支持 Schema 的 XML 编辑器编写配置文件,Spring 的可扩展 XML 配置机制基于 XML Schema。如果你还不熟悉 Spring 标准发行版附带的当前 XML 配置扩展,你应该先阅读前一节 XML Schema 概览。

要创建新的 XML 配置扩展:

  1. 授权 一个 XML Schema 来描述你的自定义元素。
  2. 编码 一个自定义的 NamespaceHandler 实现。
  3. 编码 一个或多个 BeanDefinitionParser 实现(这是真正工作的核心)。
  4. 注册 你的新产物到 Spring 中。

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

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

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

授权 Schema ​

创建一个供 Spring IoC 容器使用的 XML 配置扩展,首先要授权一个 XML Schema 来描述该扩展。在该示例中,我们使用以下 Schema 来配置 SimpleDateFormat 对象:

xml
<!-- myns.xsd (位于 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. 该行包含所有可标识标签的扩展基类(意味着它们具有一个 id 属性,我们可以将其用作容器中的 Bean 标识符)。我们可以使用这个属性是因为我们导入了 Spring 提供的 beans 命名空间。

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

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

上述两个片段中的第二个在容器中创建了一个 Bean(其名称为 dateFormat,类型为 SimpleDateFormat),并设置了两个属性。

注意

基于 Schema 的配置创建方式允许与拥有架构感知(Schema-aware)XML 编辑器的 IDE 紧密集成。通过使用正确编写的 Schema,你可以利用自动完成功能,让用户在枚举定义的多个配置项中进行选择。

编码 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 支持的作用域。我们先从一个不使用装饰的简单例子开始,之后会在稍微高级一点的例子中展示装饰功能。

虽然你可以为整个命名空间编写自己的 NamespaceHandler(从而提供解析该命名空间中每一个元素的代码),但通常情况下,Spring XML 配置文件中的每个顶层 XML 元素都会产生一个 Bean 定义(如本例所示,单个 <myns:dateformat/> 元素产生一个 SimpleDateFormat Bean 定义)。Spring 提供了一些支持这种方案的便捷类。在以下示例中,我们使用 NamespaceHandlerSupport 类:

java
package org.springframework.samples.xml;

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

public class MyNamespaceHandler extends NamespaceHandlerSupport {

	public void init() {
		registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
	}
}
kotlin
package org.springframework.samples.xml

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

class MyNamespaceHandler : NamespaceHandlerSupport() {

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

你可能会注意到,这个类中其实并没有太多的解析逻辑。实际上,NamespaceHandlerSupport 类内置了委派机制。它支持注册任意数量的 BeanDefinitionParser 实例,并在需要解析其命名空间中的元素时进行委派。这种清晰的职责分离让 NamespaceHandler 负责编排其命名空间中所有自定义元素的解析,而将 XML 解析的繁重工作委派给 BeanDefinitionParsers。这意味着每个 BeanDefinitionParser 只包含解析单个自定义元素的逻辑,正如我们在下一步中看到的那样。

使用 BeanDefinitionParser ​

如果 NamespaceHandler 遇到了映射到特定 Bean 定义解析器(在本例中为 dateformat)类型的 XML 元素,就会使用 BeanDefinitionParser。换句话说,BeanDefinitionParser 负责解析 Schema 中定义的一个独特的顶层 XML 元素。在解析器中,我们可以访问 XML 元素(从而也可以访问其子元素),以便解析我们的自定义 XML 内容,如下例所示:

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

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

class SimpleDateFormatBeanDefinitionParser : AbstractSingleBeanDefinitionParser() { (1)

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

	override fun doParse(element: Element, bean: BeanDefinitionBuilder) {
		// 这里永远不会为 null,因为 Schema 明确要求提供一个值
		val pattern = element.getAttribute("pattern")
		bean.addConstructorArgValue(pattern)

		// 然而这是一个可选属性
		val lenient = element.getAttribute("lenient")
		if (StringUtils.hasText(lenient)) {
			bean.addPropertyValue("lenient", java.lang.Boolean.valueOf(lenient))
		}
	}
}
  1. 我们使用 Spring 提供的 AbstractSingleBeanDefinitionParser 来处理创建单个 BeanDefinition 的大量基础性工作。
  2. 我们向 AbstractSingleBeanDefinitionParser 父类提供我们的单个 BeanDefinition 所代表的类型。

在这个简单的案例中,这就是我们需要做的全部工作。单个 BeanDefinition 的创建由 AbstractSingleBeanDefinitionParser 父类处理,同时也处理了 Bean 定义唯一标识符的提取和设置。

注册 Handler 和 Schema ​

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

编写 META-INF/spring.handlers ​

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

properties
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。如果你在属性文件中指定了映射,Spring 将在类路径上搜索架构(在本例中为 org.springframework.samples.xml 包中的 myns.xsd)。以下片段显示了我们需要为自定义架构添加的行:

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

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

建议你将 XSD 文件直接部署在类路径上的 NamespaceHandler 和 BeanDefinitionParser 类旁边。

在 Spring XML 配置中使用自定义扩展 ​

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

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: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">

	<!-- 作为一个顶层 Bean -->
	<myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/> <!-- (1) -->

	<bean id="jobDetailTemplate" abstract="true">
		<property name="dateFormat">
			<!-- 作为一个内部 Bean -->
			<myns:dateformat pattern="HH:mm MM-dd-yyyy"/>
		</property>
	</bean>

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

更详细的示例 ​

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

在自定义元素中嵌套自定义元素 ​

本节中介绍的示例展示了如何编写满足以下配置目标的各种产物:

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

java
package com.foo;

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

public class Component {

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

	// 没有针对 'components' 的 setter 方法
	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;
	}
}
kotlin
package com.foo

import java.util.ArrayList

class Component {

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

	// 没有针对 'components' 的 setter 方法
	fun addComponent(component: Component) {
		this.components.add(component)
	}

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

解决此问题的典型方法是创建一个自定义 FactoryBean,它为 components 属性暴露一个 setter 属性。以下列表显示了这样一个自定义 FactoryBean:

java
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;
	}
}
kotlin
package com.foo

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 内部细节。如果我们坚持使用前面描述的步骤,我们首先创建 XSD Schema 来定义自定义标签的结构,如下表所示:

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

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

java
package com.foo;

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

public class ComponentNamespaceHandler extends NamespaceHandlerSupport {

	public void init() {
		registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser());
	}
}
kotlin
package com.foo

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

class ComponentNamespaceHandler : NamespaceHandlerSupport() {

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

接下来是自定义 BeanDefinitionParser。记住我们要创建一个描述 ComponentFactoryBean 的 BeanDefinition。以下示例显示了我们的自定义 BeanDefinitionParser 实现:

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

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.handlers 和 META-INF/spring.schemas 文件向 Spring XML 基础设施注册各种产物,如下所示:

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

“常规”元素上的自定义属性 ​

编写你自己的自定义解析器和相关产物并不难。然而,有时候这并不是正确的做法。考虑这样一种场景:你需要向已经存在的 Bean 定义添加元数据。在这种情况下,你肯定不想编写整个自定义扩展。相反,你仅仅想向现有的 Bean 定义元素添加一个额外的属性。

再举一个例子,假设你为一个服务对象定义了一个 Bean 定义,该服务对象(它自身并不知道)访问一个集群化的 JCache,并且你想确保命名的 JCache 实例在周围的集群内被及早启动。以下示例显示了这样一个定义:

xml
<bean id="checkingAccountService" class="com.foo.DefaultCheckingAccountService"
		jcache:cache-name="checking.account">
	<!-- 这里配置其他依赖项... -->
</bean>

然后我们可以在解析 'jcache:cache-name' 属性时创建另一个 BeanDefinition。这个 BeanDefinition 之后会为我们初始化命名的 JCache。我们还可以修改现有的 'checkingAccountService' 的 BeanDefinition,使其依赖于这个新的 JCache 初始化 BeanDefinition。以下示例显示了我们的 JCacheInitializer:

java
package com.foo;

public class JCacheInitializer {

	private final String name;

	public JCacheInitializer(String name) {
		this.name = name;
	}

	public void initialize() {
		// 大量的 JCache API 调用用以初始化命名的缓存...
	}
}
kotlin
package com.foo

class JCacheInitializer(private val name: String) {

	fun initialize() {
		// 大量的 JCache API 调用用以初始化命名的缓存...
	}
}

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

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

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

java
package com.foo;

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

public class JCacheNamespaceHandler extends NamespaceHandlerSupport {

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

}
kotlin
package com.foo

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

class JCacheNamespaceHandler : NamespaceHandlerSupport() {

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

}

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

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

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.handlers 和 META-INF/spring.schemas 文件向 Spring XML 基础设施注册各种产物,如下所示:

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

补充教学 ​

1. 为什么还要学 XML 授权? ​

在 Spring Boot 统治的今天,很多开发者已经不再编写 XML 配置文件了。但是,掌握 XML 授权依然有其核心价值:

  • 深层理解 Spring IoC:通过编写 BeanDefinitionParser,你可以从最底层看到 Spring 是如何将 XML 标签转换为 BeanDefinition(Bean 的描述蓝图)的。这种理解可以直接应用到 ImportBeanDefinitionRegistrar 等高级配置方式上。
  • 维护大型企业级遗留系统:很多成熟的基础设施依赖自定义 XML 命名空间(如 Spring Security、Dubbo)。
  • 开发通用 SDK/库:如果你在开发一个非 Spring Boot 环境下的工具类库,提供一套语义化的 XML 配置方案会极大提升用户体验。

2. BeanDefinitionParser vs BeanDefinitionDecorator ​

  • BeanDefinitionParser:负责将一个 XML 元素转换成一个全新的 BeanDefinition。它通常用于 <foo:server ... /> 这种独立的定义。
  • BeanDefinitionDecorator:顾名思义,它不产生新的核心 Bean,而是在已有的 Bean 定义上“加料”。比如文中提到的 jcache:cache-name 属性,它不仅创建了一个辅助的初始化 Bean,还巧妙地利用了 depends-on 机制修改了主 Bean 的依赖链。

3. 代码中的关键工具 ​

  • BeanDefinitionBuilder:它是 Spring 提供的流式 API,用于构建 BeanDefinition。使用它可以避免直接操作复杂的 POJO 设置。
  • ManagedList 和 ManagedMap:注意示例代码中的 ManagedList。在 Spring XML 解析期间,如果你需要处理内部嵌套的 Bean 或集合,必须使用这些 Managed* 集合类,否则 Spring 无法正确处理其中的占位符解析或生命周期管理。

4. 关于 spring.handlers 和 spring.schemas ​

这两个文件是 Java SPI (Service Provider Interface) 的 Spring 变体。Spring 在启动时会扫描所有 Jar 包中的这些路径。

  • 技巧:如果你在自定义扩展中遇到了 XSD 校验失败,首先检查 spring.schemas 中的 URI 是否与 XML 文件中的 xsi:schemaLocation 完全一致(包括 http 与 https 的区别)。

Based on Spring Framework.