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 文件直接部署在类路径上的 NamespaceHandlerBeanDefinitionParser 类旁边。

在 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。记住我们要创建一个描述 ComponentFactoryBeanBeanDefinition。以下示例显示了我们的自定义 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.handlersMETA-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.handlersMETA-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 设置。
  • ManagedListManagedMap:注意示例代码中的 ManagedList。在 Spring XML 解析期间,如果你需要处理内部嵌套的 Bean 或集合,必须使用这些 Managed* 集合类,否则 Spring 无法正确处理其中的占位符解析或生命周期管理。

4. 关于 spring.handlersspring.schemas

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

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

Based on Spring Framework.