https://docs.spring.io/spring-framework/reference/core/appendix/xml-custom.html
从 2.0 版本开始,Spring 提供了一种机制,可以将基于 schema 的扩展添加到用于定义和配置 bean 的基本 Spring XML 格式中。本节将介绍如何编写自定义的 XML bean 定义解析器,并将这些解析器集成到 Spring IoC 容器中。
为了便于使用支持模式的XML编辑器编写配置文件,Spring的可扩展XML配置机制基于XML Schema。
要创建新的 XML 配置扩展:
- 编写一个 XML schema 来描述你的自定义元素。
- 编写一个自定义的
NamespaceHandler
实现。 - 编写一个或多个
BeanDefinitionParser
实现(这是实际工作的地方)。 - 将你的新组件注册到 Spring 中。
为了统一示例,我们创建了一个XML扩展(一个自定义XML元素),它允许我们配置java.text
包中类型为SimpleDateFormat
的对象。完成后,我们将能够按如下方式定义类型为SimpleDateFormat
的bean定义:
<myns:dateformat id="dateFormat"
pattern="yyyy-MM-dd HH:mm"
lenient="true"/>
编写Schema
为Spring的IoC容器创建XML配置扩展首先需要编写一个XML Schema来描述扩展。在我们的示例中,我们使用以下模式来配置SimpleDateFormat
对象:
<!-- 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">
<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>
<xsd:extension base="beans:identifiedType"
>指示的行包含所有可识别标签(意味着它们具有我们可以用作容器中bean标识符的id
属性)的扩展基础。我们可以使用这个属性,因为我们导入了Spring提供的beans命名空间。
前面的模式允许我们直接在XML应用上下文文件中通过使用<myns:dateformat/>
元素来配置SimpleDateFormat
对象,如下例所示:
<myns:dateformat id="dateFormat"
pattern="yyyy-MM-dd HH:mm"
lenient="true"/>
创建了基础设施类之后,前面的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
),并设置了几个属性。
基于模式的配置格式创建方法允许与具有模式感知XML编辑器的IDE紧密集成。通过使用正确编写的模式,你可以使用自动完成功能让用户在枚举中定义的几个配置选项之间进行选择。
编写NamespaceHandler
除了模式之外,我们还需要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
类:
package org.springframework.samples.xml;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class MyNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
}
}
这个类实际上并没有太多的解析逻辑。确实,NamespaceHandlerSupport
类内置了委托的概念。它支持注册任意数量的BeanDefinitionParser
实例,当需要解析其命名空间中的元素时,它会委托给这些实例。这种清晰的关注点分离让NamespaceHandler
处理其命名空间中所有自定义元素解析的协调工作,同时将XML解析的繁重工作委托给BeanDefinitionParser
。这意味着每个BeanDefinitionParser
只包含解析单个自定义元素的逻辑。
使用BeanDefinitionParser
当NamespaceHandler
遇到映射到特定bean定义解析器(在这个例子中是dateformat
)类型的XML元素时,会使用BeanDefinitionParser
。换句话说,BeanDefinitionParser
负责解析模式中定义的一个独特的顶级XML元素。在解析器中,我们可以访问XML元素(因此也可以访问其子元素),以便我们可以解析我们的自定义XML内容:
package org.springframework.samples.xml;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;
import java.text.SimpleDateFormat;
public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { (1)
protected Class getBeanClass(Element element) {
return SimpleDateFormat.class; (2)
}
protected void doParse(Element element, BeanDefinitionBuilder bean) {
// this will never be null since the schema explicitly requires that a value be supplied
String pattern = element.getAttribute("pattern");
bean.addConstructorArgValue(pattern);
// this however is an optional property
String lenient = element.getAttribute("lenient");
if (StringUtils.hasText(lenient)) {
bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
}
}
}
在这个简单的情况下,这就是我们需要做的全部。我们的单个BeanDefinition
的创建是由AbstractSingleBeanDefinitionParser
超类处理的,就像bean定义的唯一标识符的提取和设置一样。
注册Handler 和Schema
编码已经完成。剩下的就是让Spring XML解析基础设施知道我们的自定义元素。我们通过在两个专用properties 文件中注册我们的自定义namespaceHandler
和自定义XSD文件来实现这一点。这些properties 文件都放置在你的应用程序的META-INF
目录中,并且可以与你的二进制类一起分布在JAR文件中。Spring XML解析基础设施通过使用这些特殊的properties 文件自动得到的新扩展,这些文件的格式在接下来的两节中有详细说明。
编写META-INF/spring.handlers文件
名为spring.handlers
的属性文件包含XML Schema URI到命名空间处理器类的映射。对于我们的示例,我们需要写入以下内容:
http\://www.mycompany.example/schema/myns=org.springframework.samples.xml.MyNamespaceHandler
(:
字符在Java属性格式中是一个有效的分隔符,所以URI中的:
字符需要用反斜杠转义。)
键值对的第一部分(键)是与你的自定义命名空间扩展关联的URI,需要与你的自定义XSD模式中指定的targetNamespace
属性的值完全匹配。
编写’META-INF/spring.schemas’
名为spring.schemas
的属性文件包含XML Schema位置(在xsi:schemaLocation
属性中使用模式作为部分的XML文件中,与模式声明一起引用)到类路径资源的映射。这个文件是必需的,以防止Spring绝对必须使用默认的EntityResolver
,这需要互联网访问来检索模式文件。如果你在此属性文件中指定映射,Spring将在类路径上搜索模式(在这种情况下,是org.springframework.samples.xml
包中的myns.xsd
)。以下片段显示了我们需要为自定义模式添加的行:
http\://www.mycompany.example/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd
(记住,必须转义冒号字符。)
建议将XSD文件与NamespaceHandler
和BeanDefinitionParser
类一起部署在类路径上。
在你的Spring XML配置中使用自定义扩展
使用你自己实现的自定义扩展与使用Spring提供的“自定义”扩展没有区别。以下示例在Spring 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">
<!-- as a top-level bean -->
<myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>
<bean id="jobDetailTemplate" abstract="true">
<property name="dateFormat">
<!-- as an inner bean -->
<myns:dateformat pattern="HH:mm MM-dd-yyyy"/>
</property>
</bean>
</beans>
更详细的示例
在自定义元素中嵌套自定义元素
本节中的示例展示了如何编写满足以下配置目标所需的各种工件:
<?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
类:
package com.foo;
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;
}
}
解决这个问题的典型解决方案是创建一个自定义的FactoryBean
,它为components
属性公开了一个setter属性。以下列表显示了这样一个自定义FactoryBean
:
package com.foo;
import org.springframework.beans.factory.FactoryBean;
import java.util.List;
public class ComponentFactoryBean implements FactoryBean<Component> {
private Component parent;
private List<Component> children;
public void setParent(Component parent) {
this.parent = parent;
}
public void setChildren(List<Component> children) {
this.children = children;
}
public Component getObject() throws Exception {
if (this.children != null && this.children.size() > 0) {
for (Component child : children) {
this.parent.addComponent(child);
}
}
return this.parent;
}
public Class<Component> getObjectType() {
return Component.class;
}
public boolean isSingleton() {
return true;
}
}
这样做很好,但是它向最终用户暴露了很多Spring的内部结构。我们要做的就是编写一个自定义扩展,隐藏所有这些Spring的内部结构。如果我们遵循前面描述的步骤,我们从创建XSD模式开始,以定义我们自定义标签的结构,如下所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.foo.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
:
package com.foo;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class ComponentNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser());
}
}
接下来是自定义BeanDefinitionParser
。记住,我们正在创建一个描述ComponentFactoryBean
的BeanDefinition
。以下列表显示了我们的自定义BeanDefinitionParser
实现:
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);
}
}
最后,需要通过修改META-INF/spring.handlers
和META-INF/spring.schemas
文件,将各种工件注册到Spring XML基础设施中,如下所示:
# 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
在“普通”元素上自定义属性
考虑一个场景,你需要为已存在的bean定义添加元数据。在这种情况下,你当然不想编写自己的整个自定义扩展。相反,只是想在现有的bean定义元素中添加一个额外的属性。
通过另一个例子,假设你为一个服务对象定义了一个bean定义(它不知道),该服务对象访问一个集群化的JCache,并且你希望确保命名的JCache实例在周围的集群中被急切地启动。以下列表显示了这样一个定义:
<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
:
package com.foo;
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...
}
}
现在我们可以继续进行自定义扩展。首先,我们需要编写描述自定义属性的XSD模式,如下所示:
<?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
,如下所示:
package com.foo;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class JCacheNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
super.registerBeanDefinitionDecoratorForAttribute("cache-name",
new JCacheInitializingBeanDefinitionDecorator());
}
}
接下来,我们需要创建解析器。注意,在这种情况下,因为我们要解析XML属性,所以我们编写一个BeanDefinitionDecorator
而不是BeanDefinitionParser
。以下列表显示了我们的BeanDefinitionDecorator
实现:
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;
}
}
最后,我们需要通过修改META-INF/spring.handlers
和META-INF/spring.schemas
文件,将各种工件注册到Spring XML基础设施中,如下所示:
# 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