jaxb_自定义Spring命名空间使JAXB变得更容易

jaxb

jaxb

首先,让我大声说一下: Spring不再是XML繁重的了。 事实上,如今,您可以使用大量注释, Java配置Spring Boot来编写几乎没有XML或根本没有XML的Spring应用程序。 认真地停止谈论Spring和XML,这已成为过去。

话虽这么说,您仍然可能出于以下几个原因而使用XML:您受困于旧代码库,出于其他原因选择了XML,或者将Spring用作某些框架/平台的基础。 最后一种情况实际上很常见,例如Mule ESBActiveMQ在下面使用Spring来连接它们的依赖项。 而且,Spring XML是他们配置框架的方式。 但是,使用纯Spring <bean/>来配置消息代理或企业服务总线会很麻烦且冗长。 幸运的是,Spring支持编写可嵌入标准Spring配置文件中的自定义名称空间。 这些自定义的XML代码片段在运行时已进行了预处理,并且可以以简洁明快的外观(在XML允许的范围内)一次注册许多bean定义。 从某种意义上说,自定义名称空间就像在运行时扩展为多个bean定义的宏。

为了让您了解我们的目标,请想象一个具有多个业务实体的标准“企业”应用程序。 对于每个实体,我们定义三个几乎相同的bean:存储库,服务和控制器。 它们始终以相似的方式进行布线,只是细节有所不同。 首先,我们的Spring XML如下所示(我正在粘贴带有缩略图的屏幕截图,以免引起您的注意,它巨大且huge肿):

XML文件

这是一个“分层”的体系结构,因此我们将称为onion的自定义名称空间-因为洋葱具有层次性-并且因为以这种方式设计的系统使我哭泣。 在本文结尾,您将学习如何将这堆XML折叠成:

<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
       xmlns="http://nurkiewicz.blogspot.com/spring/onion/spring-onion.xsd"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
           http://nurkiewicz.blogspot.com/spring/onion/spring-onion.xsd http://nurkiewicz.blogspot.com/spring/onion/spring-onion.xsd">
 
    <b:bean id="convertersFactory" class="com.blogspot.nurkiewicz.onion.ConvertersFactory"/>
 
    <converter format="html"/>
    <converter format="json"/>
    <converter format="error" lenient="false"/>
 
    <entity class="Foo" converters="json, error">
        <page response="404" dest="not-found"/>
        <page response="503" dest="error"/>
    </entity>
 
    <entity class="Bar" converters="json, html, error">
        <page response="400" dest="bad-request"/>
        <page response="500" dest="internal"/>
    </entity>
 
    <entity class="Buzz" converters="json, html">
        <page response="502" dest="bad-gateway"/>
    </entity>
 
</b:beans>

仔细观察,仍然是该框架可以完全理解的Spring XML文件-您将学习如何实现。 您可以为每个顶级定制XML标记运行任意代码,例如,一次出现的<entity/>寄存器存储库,服务和控制器Bean定义全部一次。 要实现的第一件事是为我们的名称空间编写自定义XML模式。 这并不难,并且将允许IntelliJ IDEA以XML显示代码完成:

<?xml version="1.0" encoding="UTF-8"?>
<schema
        xmlns:tns="http://nurkiewicz.blogspot.com/spring/onion/spring-onion.xsd"
        xmlns="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://nurkiewicz.blogspot.com/spring/onion/spring-onion.xsd"
        elementFormDefault="qualified"
        attributeFormDefault="unqualified">
 
    <element name="entity">
        <complexType>
            <sequence>
                <element name="page" type="tns:Page" minOccurs="0" maxOccurs="unbounded"/>
            </sequence>
            <attribute name="class" type="string" use="required"/>
            <attribute name="converters" type="string"/>
        </complexType>
    </element>
 
    <complexType name="Page">
        <attribute name="response" type="int" use="required"/>
        <attribute name="dest" type="string" use="required"/>
    </complexType>
 
    <element name="converter">
        <complexType>
            <attribute name="format" type="string" use="required"/>
            <attribute name="lenient" type="boolean" default="true"/>
        </complexType>
    </element>
 
</schema>

模式完成后,我们必须使用两个文件在Spring中进行注册:

/META-INF/spring.schemas

http\://nurkiewicz.blogspot.com/spring/onion/spring-onion.xsd=/com/blogspot/nurkiewicz/onion/ns/spring-onion.xsd

/META-INF/spring.handlers

http\://nurkiewicz.blogspot.com/spring/onion/spring-onion.xsd=com.blogspot.nurkiewicz.onion.ns.OnionNamespaceHandler

一个将模式URL映射到本地的模式位置,另一个则指向所谓的名称空间处理程序。 此类非常简单–它告诉如何处理Spring配置文件中遇到的该命名空间中的每个顶级自定义XML标签:

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
 
public class OnionNamespaceHandler extends NamespaceHandlerSupport {
    public void init() {
        registerBeanDefinitionParser("entity", new EntityBeanDefinitionParser());
        registerBeanDefinitionParser("converter", new ConverterBeanDefinitionParser());
    }
}

因此,当Spring找到XML的<converter format="html"/>片段时,它知道需要使用我们的ConverterBeanDefinitionParser 。 请记住,如果我们的自定义标签有子级(例如<entity/> ),则仅对顶级标签调用bean定义解析器。 如何解析和处理孩子取决于我们自己。 好的,因此假设单个<converter/>标签可以创建以下两个bean:

<bean id="htmlConverter" class="com.blogspot.nurkiewicz.onion.Converter" factory-bean="convertersFactory" factory-method="build">
    <constructor-arg value="html.xml"/>
    <constructor-arg value="true"/>
    <property name="reader" ref="htmlReader"/>
</bean>
<bean id="htmlReader" class="com.blogspot.nurkiewicz.onion.ReaderFactoryBean">
    <property name="format" value="html"/>
</bean>

bean定义解析器的职责是以编程方式注册XML定义的bean定义。 我不会详细介绍API,但是将其与上面的XML代码段进行比较,它们相互之间非常接近:

import org.w3c.dom.Element;
 
public class ConverterBeanDefinitionParser extends AbstractBeanDefinitionParser {
 
    @Override
    protected AbstractBeanDefinition parseInternal(Element converterElement, ParserContext parserContext) {
        final String format = converterElement.getAttribute("format");
        final String lenientStr = converterElement.getAttribute("lenient");
        final boolean lenient = lenientStr != null? Boolean.valueOf(lenientStr) : true;
        final BeanDefinitionRegistry registry = parserContext.getRegistry();
 
        final AbstractBeanDefinition converterBeanDef = converterBeanDef(format, lenient);
        registry.registerBeanDefinition(format + "Converter", converterBeanDef);
 
        final AbstractBeanDefinition readerBeanDef = readerBeanDef(format);
        registry.registerBeanDefinition(format + "Reader", readerBeanDef);
 
        return null;
    }
 
    private AbstractBeanDefinition readerBeanDef(String format) {
        return BeanDefinitionBuilder.
                    rootBeanDefinition(ReaderFactoryBean.class).
                    addPropertyValue("format", format).
                    getBeanDefinition();
    }
 
    private AbstractBeanDefinition converterBeanDef(String format, boolean lenient) {
        AbstractBeanDefinition converterBeanDef = BeanDefinitionBuilder.
                rootBeanDefinition(Converter.class.getName()).
                addConstructorArgValue(format + ".xml").
                addConstructorArgValue(lenient).
                addPropertyReference("reader", format + "Reader").
                getBeanDefinition();
        converterBeanDef.setFactoryBeanName("convertersFactory");
        converterBeanDef.setFactoryMethodName("build");
        return converterBeanDef;
    }
}

您是否看到parseInternal()如何接收表示<converter/>标签的XML Element ,提取属性并注册bean定义? 由您决定在AbstractBeanDefinitionParser实现中定义多少个bean。 请记住,我们在这里几乎没有构建配置,还没有实例化。 一旦XML文件被完全解析并且所有bean定义解析器被触发,Spring将开始引导我们的应用程序。 要记住的一件事是最后返回null 。 API期望您返回单个bean定义。 但是,无需限制自己, null就可以了。

我们支持的第二个自定义标签是<entity/> ,它一次注册三个bean。 这很相似,因此没有那么有趣,请参阅EntityBeanDefinitionParser完整源代码。 可以找到的一个重要的实现细节是ManagedList的用法。 文档模糊地提到了它,但是它非常有价值。 如果要定义一个知道其ID的要注入的bean列表,简单的List<String>是不够的,则必须明确告诉Spring您的意思是一个bean引用列表:

List<BeanMetadataElement> converterRefs = new ManagedList<>();
for (String converterName : converters) {
    converterRefs.add(new RuntimeBeanReference(converterName));
}
return BeanDefinitionBuilder.
        rootBeanDefinition("com.blogspot.nurkiewicz.FooService").
        addPropertyValue("converters", converterRefs).
        getBeanDefinition();

使用JAXB简化bean定义解析器

好的,因此,现在您应该熟悉自定义Spring名称空间以及它们如何为您提供帮助。 但是,它们要求您使用原始XML DOM API解析自定义标签,因此级别很低。 但是我的队友发现,既然我们已经有了XSD模式文件,为什么不使用JAXB来处理XML解析呢? 首先,我们要求Maven在构建过程中生成表示XML类型和元素的Java bean:

<build>
    <plugins>
        <plugin>
            <groupId>org.jvnet.jaxb2.maven2</groupId>
            <artifactId>maven-jaxb22-plugin</artifactId>
            <version>0.8.3</version>
            <executions>
                <execution>
                    <id>xjc</id>
                    <goals>
                        <goal>generate</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <schemaDirectory>src/main/resources/com/blogspot/nurkiewicz/onion/ns</schemaDirectory>
                <generatePackage>com.blogspot.nurkiewicz.onion.ns.xml</generatePackage>
            </configuration>
        </plugin>
    </plugins>
</build>

/target/generated-sources/xjc您会发现几个Java文件。 我喜欢生成的JAXB模型具有一些通用前缀,例如Xml ,可以通过在spring-onion.xsd旁边放置自定义bindings.xjb文件轻松实现:

<bindings version="1.0"
              xmlns="http://java.sun.com/xml/ns/jaxb"
              xmlns:xs="http://www.w3.org/2001/XMLSchema"
              extensionBindingPrefixes="xjc">
 
    <bindings schemaLocation="spring-onion.xsd" node="/xs:schema">
        <schemaBindings>
            <nameXmlTransform>
                <typeName prefix="Xml"/>
                <anonymousTypeName prefix="Xml"/>
                <elementName prefix="Xml"/>
            </nameXmlTransform>
        </schemaBindings>
    </bindings>
 
</bindings>

它如何改变我们的自定义bean定义解析器? 以前我们有这个:

final String clazz = entityElement.getAttribute("class");
//...
final NodeList pageNodes = entityElement.getElementsByTagNameNS(NS, "page");
for (int i = 0; i < pageNodes.getLength(); ++i) {  //...

现在我们简单地遍历Java bean:

final XmlEntity entity = JaxbHelper.unmarshal(entityElement);
final String clazz = entity.getClazz();
//...
for (XmlPage page : entity.getPage()) {  //...

JaxbHelper只是一个简单的工具,可从外部隐藏检查的异常和JAXB机制:

public class JaxbHelper {
 
    private static final Unmarshaller unmarshaller = create();
 
    private static Unmarshaller create() {
        try {
            return JAXBContext.newInstance("com.blogspot.nurkiewicz.onion.ns.xml").createUnmarshaller();
        } catch (JAXBException e) {
            throw Throwables.propagate(e);
        }
    }
 
    public static <T> T unmarshal(Element elem) {
        try {
            return (T) unmarshaller.unmarshal(elem);
        } catch (JAXBException e) {
            throw Throwables.propagate(e);
        }
 
    }
 
}

几句话作为总结。 首先,我不鼓励您为每个实体自动生成存储库/服务/控制器Bean定义。 实际上,这是一种不好的做法,但是我们所有人都熟悉该领域,因此我认为这将是一个很好的例子。 其次,更重要的是,自定义XML名称空间是一个功能强大的工具,当所有其他东西(即抽象bean工厂bean和Java配置)失败时,应将其用作最后的手段。 通常,您会希望在Spring顶部构建的框架或工具中使用这种功能。 在这种情况下,请在GitHub上查看完整的源代码

翻译自: https://www.javacodegeeks.com/2014/03/custom-spring-namespaces-made-easier-with-jaxb.html

jaxb

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值