当前版本仍在开发中,尚不被视为稳定版本。最新稳定版请使用 Spring Batch 文档 6.0.2!

XML ItemReader 与 ItemWriter

Spring Batch 提供了事务性基础设施,既可用于读取 XML 记录并将其映射为 Java 对象,也可用于把 Java 对象写出为 XML 记录。

流式 XML 处理的限制

这里使用 StAX API 来执行 I/O,因为其他标准 XML 解析 API 不适合批处理场景的要求。DOM 会一次性把整个输入加载进内存, 而 SAX 通过只允许用户提供回调函数来控制解析过程。

我们需要先理解 XML 输入和输出在 Spring Batch 中是如何工作的。首先,有几个概念与普通文件读写不同,但在 Spring Batch 的 XML 处理中是通用的。处理 XML 时,不再是对一行行记录(即 FieldSet 实例)进行分词,而是假定一个 XML 资源由一组 与单条记录对应的“片段”组成,如下图所示:

XML Input
图 1. XML 输入

在上面的场景中,trade 标签被定义为“根元素”。从 <trade></trade> 之间的全部内容都被视为一个“片段”。 Spring Batch 使用对象/XML 映射(OXM)把这些片段绑定到对象上。不过,Spring Batch 并不绑定于某一种特定的 XML 绑定技术。 常见做法是委托给 Spring OXM, 它为主流 OXM 技术提供了统一抽象。对 Spring OXM 的依赖是可选的;如果需要,你也可以选择实现 Spring Batch 专用接口。 OXM 与其所支持技术之间的关系如下图所示:

OXM Binding
图 2. OXM 绑定

在介绍完 OXM 以及如何使用 XML 片段表示记录之后,下面可以更具体地看一下 reader 和 writer。

StaxEventItemReader

StaxEventItemReader 的配置给出了一个处理 XML 输入流中记录的典型示例。先看下面这组可由 StaxEventItemReader 处理的 XML 记录:

<?xml version="1.0" encoding="UTF-8"?>
<records>
    <trade xmlns="https://springframework.org/batch/sample/io/oxm/domain">
        <isin>XYZ0001</isin>
        <quantity>5</quantity>
        <price>11.39</price>
        <customer>Customer1</customer>
    </trade>
    <trade xmlns="https://springframework.org/batch/sample/io/oxm/domain">
        <isin>XYZ0002</isin>
        <quantity>2</quantity>
        <price>72.99</price>
        <customer>Customer2c</customer>
    </trade>
    <trade xmlns="https://springframework.org/batch/sample/io/oxm/domain">
        <isin>XYZ0003</isin>
        <quantity>9</quantity>
        <price>99.99</price>
        <customer>Customer3</customer>
    </trade>
</records>

要处理这些 XML 记录,需要具备以下内容:

  • 根元素名称:构成待映射对象的那个片段的根元素名称。示例配置中对应的值是 trade

  • 资源:一个表示待读取文件的 Spring Resource。

  • Unmarshaller:由 Spring OXM 提供的反序列化组件,用于把 XML 片段映射为对象。

  • Java

  • XML

下面的示例展示了如何在 Java 中定义一个 StaxEventItemReader,它使用名为 trade 的根元素、 data/iosample/input/input.xml 资源,以及名为 tradeMarshaller 的 unmarshaller:

Java Configuration
@Bean
public StaxEventItemReader itemReader() {
	return new StaxEventItemReaderBuilder<Trade>()
			.name("itemReader")
			.resource(new FileSystemResource("org/springframework/batch/infrastructure/item/xml/domain/trades.xml"))
			.addFragmentRootElements("trade")
			.unmarshaller(tradeMarshaller())
			.build();

}

下面的示例展示了如何在 XML 中定义一个 StaxEventItemReader,它使用名为 trade 的根元素、 data/iosample/input/input.xml 资源,以及名为 tradeMarshaller 的 unmarshaller:

XML Configuration
<bean id="itemReader" class="org.springframework.batch.infrastructure.item.xml.StaxEventItemReader">
    <property name="fragmentRootElementName" value="trade" />
    <property name="resource" value="org/springframework/batch/infrastructure/item/xml/domain/trades.xml" />
    <property name="unmarshaller" ref="tradeMarshaller" />
</bean>

需要注意,在这个示例中我们选择使用 XStreamMarshaller。它接收一个以 map 形式传入的别名配置,其中第一组键值对 表示片段名称(也就是根元素)与要绑定的对象类型。随后,和 FieldSet 类似,映射到该对象类型字段的其他元素名称, 也通过 map 中的键值对进行描述。在配置文件中,可以使用 Spring 的配置工具来声明这些所需别名。

  • Java

  • XML

下面的示例展示了如何在 Java 中描述这些别名:

Java Configuration
@Bean
public XStreamMarshaller tradeMarshaller() {
	Map<String, Class> aliases = new HashMap<>();
	aliases.put("trade", Trade.class);
	aliases.put("price", BigDecimal.class);
	aliases.put("isin", String.class);
	aliases.put("customer", String.class);
	aliases.put("quantity", Long.class);

	XStreamMarshaller marshaller = new XStreamMarshaller();

	marshaller.setAliases(aliases);

	return marshaller;
}

下面的示例展示了如何在 XML 中描述这些别名:

XML Configuration
<bean id="tradeMarshaller"
      class="org.springframework.oxm.xstream.XStreamMarshaller">
    <property name="aliases">
        <util:map id="aliases">
            <entry key="trade"
                   value="org.springframework.batch.samples.domain.trade.Trade" />
            <entry key="price" value="java.math.BigDecimal" />
            <entry key="isin" value="java.lang.String" />
            <entry key="customer" value="java.lang.String" />
            <entry key="quantity" value="java.lang.Long" />
        </util:map>
    </property>
</bean>

在读取输入时,reader 会持续读取 XML 资源,直到识别出一个新片段即将开始。默认情况下,reader 通过匹配元素名称来判断 新片段是否开始。随后,reader 会基于这个片段构造一个独立的 XML 文档,并把该文档传给反序列化器 (通常是对 Spring OXM Unmarshaller 的一层封装),以便把 XML 映射为 Java 对象。

总结来说,这个过程大致等价于下面的 Java 代码,其中使用了 Spring 配置所提供的依赖注入:

StaxEventItemReader<Trade> xmlStaxEventItemReader = new StaxEventItemReader<>();
Resource resource = new ByteArrayResource(xmlResource.getBytes());

Map aliases = new HashMap();
aliases.put("trade","org.springframework.batch.samples.domain.trade.Trade");
aliases.put("price","java.math.BigDecimal");
aliases.put("customer","java.lang.String");
aliases.put("isin","java.lang.String");
aliases.put("quantity","java.lang.Long");
XStreamMarshaller unmarshaller = new XStreamMarshaller();
unmarshaller.setAliases(aliases);
xmlStaxEventItemReader.setUnmarshaller(unmarshaller);
xmlStaxEventItemReader.setResource(resource);
xmlStaxEventItemReader.setFragmentRootElementName("trade");
xmlStaxEventItemReader.open(new ExecutionContext());

boolean hasNext = true;

Trade trade = null;

while (hasNext) {
    trade = xmlStaxEventItemReader.read();
    if (trade == null) {
        hasNext = false;
    }
    else {
        System.out.println(trade);
    }
}

StaxEventItemWriter

输出过程与输入过程是对称的。StaxEventItemWriter 需要一个 Resource、一个 marshaller,以及一个 rootTagName。Java 对象会被传给 marshaller(通常是标准的 Spring OXM Marshaller),后者再借助一个自定义事件写入器 把内容写入 Resource。这个事件写入器会过滤掉 OXM 工具为每个片段生成的 StartDocumentEndDocument 事件。

  • Java

  • XML

下面的 Java 示例使用了 MarshallingEventWriterSerializer

Java Configuration
@Bean
public StaxEventItemWriter itemWriter(Resource outputResource) {
	return new StaxEventItemWriterBuilder<Trade>()
			.name("tradesWriter")
			.marshaller(tradeMarshaller())
			.resource(outputResource)
			.rootTagName("trade")
			.overwriteOutput(true)
			.build();

}

下面的 XML 示例使用了 MarshallingEventWriterSerializer

XML Configuration
<bean id="itemWriter" class="org.springframework.batch.infrastructure.item.xml.StaxEventItemWriter">
    <property name="resource" ref="outputResource" />
    <property name="marshaller" ref="tradeMarshaller" />
    <property name="rootTagName" value="trade" />
    <property name="overwriteOutput" value="true" />
</bean>

前面的配置设置了三个必需属性,并设置了可选属性 overwriteOutput=true。本章前面已经提到,这个属性用于指定是否允许覆盖已有文件。

  • Java

  • XML

下面的 Java 示例使用了与本章前面读取示例中相同的 marshaller:

Java Configuration
@Bean
public XStreamMarshaller customerCreditMarshaller() {
	XStreamMarshaller marshaller = new XStreamMarshaller();

	Map<String, Class> aliases = new HashMap<>();
	aliases.put("trade", Trade.class);
	aliases.put("price", BigDecimal.class);
	aliases.put("isin", String.class);
	aliases.put("customer", String.class);
	aliases.put("quantity", Long.class);

	marshaller.setAliases(aliases);

	return marshaller;
}

下面的 XML 示例使用了与本章前面读取示例中相同的 marshaller:

XML Configuration
<bean id="customerCreditMarshaller"
      class="org.springframework.oxm.xstream.XStreamMarshaller">
    <property name="aliases">
        <util:map id="aliases">
            <entry key="customer"
                   value="org.springframework.batch.samples.domain.trade.Trade" />
            <entry key="price" value="java.math.BigDecimal" />
            <entry key="isin" value="java.lang.String" />
            <entry key="customer" value="java.lang.String" />
            <entry key="quantity" value="java.lang.Long" />
        </util:map>
    </property>
</bean>

最后用一个 Java 示例来总结,下面的代码综合展示了前面讨论的要点,并演示了如何以编程方式设置这些必需属性:

FileSystemResource resource = new FileSystemResource("data/outputFile.xml")

Map aliases = new HashMap();
aliases.put("trade","org.springframework.batch.samples.domain.trade.Trade");
aliases.put("price","java.math.BigDecimal");
aliases.put("customer","java.lang.String");
aliases.put("isin","java.lang.String");
aliases.put("quantity","java.lang.Long");
Marshaller marshaller = new XStreamMarshaller();
marshaller.setAliases(aliases);

StaxEventItemWriter staxItemWriter =
	new StaxEventItemWriterBuilder<Trade>()
				.name("tradesWriter")
				.marshaller(marshaller)
				.resource(resource)
				.rootTagName("trade")
				.overwriteOutput(true)
				.build();

staxItemWriter.afterPropertiesSet();

ExecutionContext executionContext = new ExecutionContext();
staxItemWriter.open(executionContext);
Trade trade = new Trade();
trade.setPrice(11.39);
trade.setIsin("XYZ0001");
trade.setQuantity(5L);
trade.setCustomer("Customer1");
staxItemWriter.write(trade);