Appearance
Spring Framework XML Schema 自定义扩展详解 🚀
概述
Spring Framework 从 2.0 版本开始,提供了一套强大的机制来扩展基本的 Spring XML 配置格式。这个机制允许开发者创建自定义的 XML 元素和属性,让配置文件更加简洁、语义化,同时提供更好的 IDE 支持。
NOTE
本文将以 Kotlin 语言和 SpringBoot 为主要示例,深入探讨如何创建和使用自定义 XML Schema 扩展。
为什么需要自定义 XML Schema? 🤔
传统配置的痛点
在没有自定义扩展之前,我们需要这样配置一个 SimpleDateFormat
Bean:
xml
<bean id="dateFormat" class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-MM-dd HH:mm"/>
<property name="lenient" value="true"/>
</bean>
xml
<myns:dateformat id="dateFormat"
pattern="yyyy-MM-dd HH:mm"
lenient="true"/>
自定义扩展的优势
TIP
自定义 XML Schema 扩展带来以下好处:
- 简洁性:减少冗余的 XML 配置代码
- 语义化:配置更加直观,易于理解
- IDE 支持:提供自动补全和验证功能
- 可重用性:封装复杂的配置逻辑
- 类型安全:通过 Schema 验证确保配置正确性
创建自定义扩展的四个步骤 📋
步骤一:编写 XML Schema
首先,我们需要定义 XSD Schema 来描述自定义元素的结构:
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>
<!-- 继承 Spring 的 identifiedType,支持 id 属性 -->
<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>
IMPORTANT
通过继承 beans:identifiedType
,我们的自定义元素可以拥有 id
属性,这样就能在 Spring 容器中作为 Bean 的标识符使用。
步骤二:实现 NamespaceHandler
NamespaceHandler
负责处理特定命名空间下的所有元素:
kotlin
package org.springframework.samples.xml
import org.springframework.beans.factory.xml.NamespaceHandlerSupport
class MyNamespaceHandler : NamespaceHandlerSupport() {
override fun init() {
// 注册元素解析器:当遇到 "dateformat" 元素时,使用指定的解析器
registerBeanDefinitionParser("dateformat", SimpleDateFormatBeanDefinitionParser())
}
}
TIP
NamespaceHandlerSupport
提供了便利的委托机制,我们只需要注册具体的解析器,它会自动处理元素分发。
步骤三:实现 BeanDefinitionParser
这是核心逻辑所在,负责将 XML 元素转换为 Spring 的 BeanDefinition
:
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() {
// 指定要创建的 Bean 类型
override fun getBeanClass(element: Element): Class<*>? {
return SimpleDateFormat::class.java
}
// 解析 XML 元素的属性,设置到 BeanDefinition 中
override fun doParse(element: Element, bean: BeanDefinitionBuilder) {
// pattern 是必需属性,直接获取并设置为构造函数参数
val pattern = element.getAttribute("pattern")
bean.addConstructorArgValue(pattern)
// lenient 是可选属性,需要检查是否存在
val lenient = element.getAttribute("lenient")
if (StringUtils.hasText(lenient)) {
bean.addPropertyValue("lenient", java.lang.Boolean.valueOf(lenient))
}
}
}
NOTE
AbstractSingleBeanDefinitionParser
简化了单个 Bean 定义的创建过程,我们只需要:
- 指定 Bean 的类型(
getBeanClass
) - 解析属性并设置到 Bean 中(
doParse
)
步骤四:注册扩展
创建 META-INF/spring.handlers
properties
# 将命名空间 URI 映射到处理器类
http\://www.mycompany.example/schema/myns=org.springframework.samples.xml.MyNamespaceHandler
创建 META-INF/spring.schemas
properties
# 将 Schema 位置映射到类路径资源
http\://www.mycompany.example/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd
WARNING
注意在 properties 文件中,冒号(:
)字符需要用反斜杠(\
)转义。
使用自定义扩展 ✨
现在我们可以在 Spring 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: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"/>
<bean id="jobDetailTemplate" abstract="true">
<property name="dateFormat">
<!-- 作为内部 Bean -->
<myns:dateformat pattern="HH:mm MM-dd-yyyy"/>
</property>
</bean>
</beans>
高级示例:嵌套自定义元素 🔄
让我们看一个更复杂的例子,展示如何处理嵌套的自定义元素:
目标配置
xml
<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>
领域模型
kotlin
package com.foo
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 解决方案
由于 Component
类没有 components
的 setter 方法,我们需要创建一个 FactoryBean
:
ComponentFactoryBean 实现
kotlin
package com.foo
import org.springframework.beans.factory.FactoryBean
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
}
}
复杂的 BeanDefinitionParser
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
class ComponentBeanDefinitionParser : AbstractBeanDefinitionParser() {
override fun parseInternal(element: Element, parserContext: ParserContext): AbstractBeanDefinition? {
return parseComponentElement(element)
}
private fun parseComponentElement(element: Element): AbstractBeanDefinition {
// 创建 ComponentFactoryBean 而不是直接创建 Component
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.beanDefinition
}
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)) // [!code highlight] // 递归解析
}
factory.addPropertyValue("children", children)
}
}
自定义属性扩展 🎯
有时我们不需要创建全新的元素,只需要在现有元素上添加自定义属性:
使用场景
xml
<bean id="checkingAccountService" class="com.foo.DefaultCheckingAccountService"
jcache:cache-name="checking.account">
<!-- other dependencies here... -->
</bean>
BeanDefinitionDecorator 实现
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
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
}
}
注册属性装饰器
kotlin
class JCacheNamespaceHandler : NamespaceHandlerSupport() {
override fun init() {
// 注册属性装饰器而不是元素解析器
super.registerBeanDefinitionDecoratorForAttribute("cache-name",
JCacheInitializingBeanDefinitionDecorator())
}
}
实际应用场景 💼
1. 数据源配置简化
xml
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="user"/>
<property name="password" value="password"/>
<property name="maximumPoolSize" value="20"/>
<property name="minimumIdle" value="5"/>
</bean>
xml
<db:datasource id="dataSource"
url="jdbc:mysql://localhost:3306/mydb"
username="user"
password="password"
max-pool-size="20"
min-idle="5"/>
2. 缓存配置
xml
<cache:redis id="redisCache"
host="localhost"
port="6379"
database="0"
timeout="2000ms"/>
3. 消息队列配置
xml
<mq:rabbit-listener id="orderListener"
queue="order.queue"
handler="orderService.handleOrder"
concurrency="5"/>
最佳实践 🌟
1. Schema 设计原则
TIP
- 语义化命名:使用清晰、有意义的元素和属性名
- 合理分组:相关的配置项应该组织在一起
- 提供默认值:为可选属性提供合理的默认值
- 类型约束:使用适当的 XSD 类型约束
2. 解析器实现建议
kotlin
class MyBeanDefinitionParser : AbstractSingleBeanDefinitionParser() {
override fun getBeanClass(element: Element): Class<*>? {
return MyBean::class.java
}
override fun doParse(element: Element, bean: BeanDefinitionBuilder) {
// 1. 处理必需属性
val requiredAttr = element.getAttribute("required-attr")
if (!StringUtils.hasText(requiredAttr)) {
throw IllegalArgumentException("required-attr is mandatory")
}
bean.addPropertyValue("requiredProperty", requiredAttr)
// 2. 处理可选属性(提供默认值)
val optionalAttr = element.getAttribute("optional-attr")
bean.addPropertyValue("optionalProperty",
if (StringUtils.hasText(optionalAttr)) optionalAttr else "default-value")
// 3. 处理复杂类型
val childElements = DomUtils.getChildElementsByTagName(element, "child")
if (childElements.isNotEmpty()) {
parseChildElements(childElements, bean)
}
}
private fun parseChildElements(elements: List<Element>, bean: BeanDefinitionBuilder) {
// 处理子元素逻辑
}
}
3. 错误处理
WARNING
在解析过程中要提供清晰的错误信息:
kotlin
override fun doParse(element: Element, bean: BeanDefinitionBuilder) {
try {
val pattern = element.getAttribute("pattern")
if (!isValidPattern(pattern)) {
throw BeanDefinitionParsingException(
"Invalid date pattern: $pattern")
}
bean.addConstructorArgValue(pattern)
} catch (Exception e) {
throw BeanDefinitionParsingException(
"Failed to parse dateformat element", e)
}
}
总结 📝
Spring XML Schema 自定义扩展是一个强大的功能,它让我们能够:
- 简化配置:将复杂的 Bean 配置封装成简洁的自定义元素
- 提高可读性:使配置文件更加语义化和直观
- 增强 IDE 支持:通过 XSD Schema 提供自动补全和验证
- 促进重用:将常用的配置模式封装成可重用的组件
IMPORTANT
虽然现在 Spring Boot 的 Java Configuration 和注解配置更加流行,但了解 XML Schema 扩展机制仍然很有价值,特别是在需要处理复杂配置场景或维护遗留系统时。
通过掌握这套机制,我们可以创建出更加优雅、易用的配置方案,提升开发效率和代码质量。 🎉