XML Schema 编写
自 2.0 版本以来,Infra 引入了一种机制,用于向定义和配置 bean 的基本 Infra XML 格式添加基于模式的扩展。 本节介绍如何编写自己的自定义 XML bean 定义解析器,并将这些解析器集成到 Infra IoC 容器中。
为了便于编写使用感知模式的 XML 编辑器的配置文件,Infra 的可扩展 XML 配置机制基于 XML Schema。 如果您不熟悉标准 Infra 发行版附带的当前 XML 配置扩展,您应该首先阅读上一节 XML Schemas。
要创建新的 XML 配置扩展:
作为一个统一的示例,我们创建一个 XML 扩展(一个自定义 XML 元素),让我们配置 SimpleDateFormat 类型(来自 java.text 包)的对象。
完成后,我们将能够如下定义 SimpleDateFormat 类型的 bean 定义:
<myns:dateformat id="dateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>
(本附录稍后将包含更详细的示例。这第一个简单示例的目的是引导您完成制作自定义扩展的基本步骤。)
编写 Schema
创建用于 Infra IoC 容器的 XML 配置扩展首先要编写一个 XML Schema 来描述该扩展。
对于我们的示例,我们使用以下模式来配置 SimpleDateFormat 对象:
myns.xsd (在 infra/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 | 指示的行包含所有可识别标签的扩展基(意味着它们具有我们可以用作容器中 bean 标识符的 id 属性)。
我们可以使用此属性,因为我们要导入 Infra 提供的 beans 命名空间。 |
前面的模式允许我们使用 <myns:dateformat/> 元素直接在 XML 应用程序上下文文件中配置 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 来解析 Infra 在解析配置文件时遇到的此特定命名空间的所有元素。
对于此示例,NamespaceHandler 应负责解析 myns:dateformat 元素。
NamespaceHandler 接口具有三个方法:
-
init():允许初始化NamespaceHandler,并在使用处理程序之前由 Infra 调用。 -
BeanDefinition parse(Element, ParserContext):当 Infra 遇到顶级元素(未嵌套在 bean 定义或不同命名空间内)时调用。 此方法本身可以注册 bean 定义,返回 bean 定义,或两者兼而有之。 -
BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext):当 Infra 遇到不同命名空间的属性或嵌套元素时调用。 一个或多个 bean 定义的装饰用于(例如)Infra 支持的作用域。 我们首先重点介绍一个简单的示例,不使用装饰,之后我们在一个稍微高级的示例中展示装饰。
虽然您可以为整个命名空间编写自己的 NamespaceHandler(从而提供解析命名空间中每个元素的代码),
但通常情况下,Infra XML 配置文件中的每个顶级 XML 元素都会产生一个 bean 定义(就像我们的例子一样,其中单个 <myns:dateformat/> 元素产生单个 SimpleDateFormat bean 定义)。
Infra 具有许多支持此场景的便利类。在以下示例中,我们使用 NamespaceHandlerSupport 类:
-
Java
package infra.samples.xml;
import infra.beans.factory.xml.NamespaceHandlerSupport;
public class MyNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
}
}
您可能会注意到此类中实际上没有很多解析逻辑。
实际上,NamespaceHandlerSupport 类具有内置的委托概念。
它支持注册任意数量的 BeanDefinitionParser 实例,当它需要解析其命名空间中的元素时,它会委托给这些实例。
这种关注点的清晰分离让 NamespaceHandler 处理其命名空间中所有自定义元素的解析编排,同时委托给 BeanDefinitionParsers 来完成 XML 解析的繁重工作。
这意味着每个 BeanDefinitionParser 仅包含解析单个自定义元素的逻辑,如下一步所示。
使用 BeanDefinitionParser
如果 NamespaceHandler 遇到已映射到特定 bean 定义解析器(在本例中为 dateformat)类型的 XML 元素,则使用 BeanDefinitionParser。
换句话说,BeanDefinitionParser 负责解析模式中定义的一个不同的顶级 XML 元素。
在解析器中,我们可以访问 XML 元素(从而也可以访问其子元素),以便我们可以解析我们的自定义 XML 内容,如下例所示:
-
Java
package infra.samples.xml;
import infra.beans.factory.support.BeanDefinitionBuilder;
import infra.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import infra.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) {
// 这永远不会为空,因为模式明确要求提供值
String pattern = element.getAttribute("pattern");
bean.addConstructorArgValue(pattern);
// 然而这是一个可选属性
String lenient = element.getAttribute("lenient");
if (StringUtils.hasText(lenient)) {
bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
}
}
}
| 1 | 我们使用 Infra 提供的 AbstractSingleBeanDefinitionParser 来处理创建单个 BeanDefinition 的许多基本繁重工作。 |
| 2 | 我们向 AbstractSingleBeanDefinitionParser 超类提供我们的单个 BeanDefinition 所代表的类型。 |
在这个简单的例子中,这就是我们需要做的全部工作。
我们的单个 BeanDefinition 的创建由 AbstractSingleBeanDefinitionParser 超类处理,bean 定义的唯一标识符的提取和设置也是如此。
注册 Handler 和 Schema
编码完成。剩下的就是让 Infra XML 解析基础设施知道我们的自定义元素。
我们通过在两个专用属性文件中注册我们的自定义 namespaceHandler 和自定义 XSD 文件来实现这一点。
这两个属性文件都放置在应用程序的 META-INF 目录中,例如,可以与 JAR 文件中的二进制类一起分发。
Infra XML 解析基础设施通过使用这些特殊属性文件自动获取您的新扩展,其格式在接下来的两节中详细说明。
编写 META-INF/spring.handlers
名为 spring.handlers 的属性文件包含 XML Schema URI 到命名空间处理程序类的映射。
对于我们的示例,我们需要编写以下内容:
http\://www.mycompany.example/schema/myns=infra.samples.xml.MyNamespaceHandler
(: 字符是 Java 属性格式中的有效分隔符,因此 URI 中的 : 字符需要用反斜杠转义。)
键值对的第一部分(键)是与您的自定义命名空间扩展关联的 URI,并且需要与您的自定义 XSD 模式中指定的 targetNamespace 属性的值完全匹配。
编写 'META-INF/spring.schemas'
名为 spring.schemas 的属性文件包含 XML Schema 位置(在将模式用作 xsi:schemaLocation 属性一部分的 XML 文件中引用,连同模式声明一起)到类路径资源的映射。
此文件用于防止 Infra 绝对必须使用默认的 EntityResolver,该解析器需要 Internet 访问才能检索模式文件。
如果您在此属性文件中指定映射,Infra 将在类路径上搜索模式(在本例中为 infra.samples.xml 包中的 myns.xsd)。
以下代码段显示了我们需要为自定义模式添加的行:
http\://www.mycompany.example/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd
(请记住,必须转义 : 字符。)
鼓励您将 XSD 文件与类路径上的 NamespaceHandler 和 BeanDefinitionParser 类一起部署。
在您的 Infra XML 配置中使用自定义扩展
使用您自己实现的自定义扩展与使用 Infra 提供的“自定义”扩展没有什么不同。
以下示例在 Infra XML 配置文件使用了在前面的步骤中开发的自定义 <dateformat/> 元素:
<?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 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;
}
}
此问题的典型解决方案是创建一个自定义 FactoryBean,该 FactoryBean 公开 components 属性的 setter 属性。
以下清单显示了这样一个自定义 FactoryBean:
-
Java
package com.foo;
import infra.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;
}
}
这很好用,但它向最终用户公开了很多 Infra 管道。我们要做的就是编写一个自定义扩展来隐藏所有这些 Infra 管道。 如果我们坚持 前面描述的步骤,我们首先创建 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:
-
Java
package com.foo;
import infra.beans.factory.xml.NamespaceHandlerSupport;
public class ComponentNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser());
}
}
接下来是自定义 BeanDefinitionParser。请记住,我们正在创建一个描述 ComponentFactoryBean 的 BeanDefinition。
以下清单显示了我们的自定义 BeanDefinitionParser 实现:
-
Java
package com.foo;
import infra.beans.factory.config.BeanDefinition;
import infra.beans.factory.support.AbstractBeanDefinition;
import infra.beans.factory.support.BeanDefinitionBuilder;
import infra.beans.factory.support.ManagedList;
import infra.beans.factory.xml.AbstractBeanDefinitionParser;
import infra.beans.factory.xml.ParserContext;
import infra.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);
}
}
最后,需要向 Infra XML 基础设施注册各种工件,方法是修改 META-INF/spring.handlers 和 META-INF/spring.schemas 文件,如下所示:
# 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:
-
Java
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,如下所示:
-
Java
package com.foo;
import infra.beans.factory.xml.NamespaceHandlerSupport;
public class JCacheNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
super.registerBeanDefinitionDecoratorForAttribute("cache-name",
new JCacheInitializingBeanDefinitionDecorator());
}
}
接下来,我们需要创建解析器。请注意,在这种情况下,因为我们要解析 XML 属性,所以我们编写一个 BeanDefinitionDecorator 而不是 BeanDefinitionParser。
以下清单显示了我们的 BeanDefinitionDecorator 实现:
-
Java
package com.foo;
import infra.beans.factory.config.BeanDefinitionHolder;
import infra.beans.factory.support.AbstractBeanDefinition;
import infra.beans.factory.support.BeanDefinitionBuilder;
import infra.beans.factory.xml.BeanDefinitionDecorator;
import infra.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 文件向 Infra 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