SpringBatch从入门到精通-6.2 读和写处理-实战2【掘金日新计划】

399 阅读8分钟

\

持续创作,加速成长,6月更文活动来啦!| 掘金·日新计划

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情

SpringBatch从入门到精通-1【掘金日新计划】

SpringBatch从入门到精通-2-StepScope作用域和用法【掘金日新计划】

SpringBatch从入门到精通-3-并行处理【掘金日新计划】

SpringBatch从入门到精通-3.2-并行处理-远程分区【掘金日新计划】

SpringBatch从入门到精通-3.3-并行处理-远程分区(消息聚合)【掘金日新计划】

SpringBatch从入门到精通-4 监控和指标【掘金日新计划】

SpringBatch从入门到精通-4.2 监控和指标-原理【掘金日新计划】

SpringBatch从入门到精通-5 数据源配置相关【掘金日新计划】

SpringBatch从入门到精通-5.2 数据源配置相关-原理【掘金日新计划】

SpringBatch从入门到精通-6 读和写处理【掘金日新计划】

SpringBatch从入门到精通-6.1 读和写处理-实战【掘金日新计划】

FlatFileItemWriter

写入FlatFile具有相同的问题和从文件读取必须克服的问题。步骤必须能够以事务方式编写定界或固定长度格式。

LineAggregator

正如LineTokenizer接口对于获取一个项目并将其转换为 是必需的 String,文件写入必须有一种方法将多个字段聚合为单个字符串以写入文件。在 Spring Batch 中,这是LineAggregator,如下面的接口定义所示:

public interface LineAggregator<T> {
​
    public String aggregate(T item);
​
}

是的LineAggregator逻辑反义词LineTokenizer。LineTokenizer接受 String并返回 FieldSet,而LineAggregator接受 item并返回 String。

PassThroughLineAggregator

LineAggregator接口的 最基本实现是PassThroughLineAggregator,它假设对象已经是一个字符串或者它的字符串表示可以被写入,如下面的代码所示:

public class PassThroughLineAggregator<T> implements LineAggregator<T> {
​
    public String aggregate(T item) {
        return item.toString();
    }
}

FlatFileItemWriter如果需要直接控制创建字符串但需要 (例如事务和重新启动支持)的优点,则上述实现很有用。

简化文件写入示例

既然已经定义了LineAggregator接口及其最基本的实现, PassThroughLineAggregator就可以解释基本的编写流程了:

  1. 将要写入的对象传递给LineAggregator以获取 String.
  2. 返回String的内容写入配置文件。

以下摘录FlatFileItemWriter在代码中表达了这一点:

public void write(T item) throws Exception {
    write(lineAggregator.aggregate(item) + LINE_SEPARATOR);
}

在 Java 中,一个简单的配置示例可能如下所示:

Java 配置

@Bean
public FlatFileItemWriter itemWriter() {
    return  new FlatFileItemWriterBuilder<Foo>()
                    .name("itemWriter")
                    .resource(new FileSystemResource("target/test-outputs/output.txt"))
                    .lineAggregator(new PassThroughLineAggregator<>())
                    .build();
}
FieldExtractor

前面的示例对于写入文件的最基本用途可能很有用。但是,大多数用户FlatFileItemWriter都有一个需要写出的域对象,因此必须将其转换为一行。在文件读取中,需要以下内容:

  1. 从文件中读取一行。
  2. 将该行传递给LineTokenizer#tokenize()方法,以便检索 FieldSet.
  3. 将标记化返回的值传递FieldSet给 a FieldSetMapper,从方法返回结果ItemReader#read()。

文件写入有类似但相反的步骤:

  1. 将要写入的项目传递给编写器。
  2. 将项目上的字段转换为数组。
  3. 将结果数组聚合成一行。

因为框架无法知道需要写出对象中的哪些字段,所以FieldExtractor必须来完成将项变成数组的任务,如下面的接口定义所示:

public interface FieldExtractor<T> {
​
    Object[] extract(T item);
​
}

接口的实现FieldExtractor应该从提供的对象的字段创建一个数组,然后可以用元素之间的分隔符或作为固定宽度行的一部分写出。

PassThroughFieldExtractor

在许多情况下,需要写出一个集合,例如一个数组、Collection或。FieldSet从其中一种集合类型中“提取”一个数组非常简单。为此,请将集合转换为数组。因此, PassThroughFieldExtractor应该在这种情况下使用。需要注意的是,如果传入的对象不是集合类型,则PassThroughFieldExtractor 返回一个仅包含要提取的项的数组。

BeanWrapperFieldExtractor

与BeanWrapperFieldSetMapper文件读取部分中的描述一样,通常最好配置如何将域对象转换为对象数组,而不是自己编写转换。提供BeanWrapperFieldExtractor此功能,如下例所示:

BeanWrapperFieldExtractor<Name> extractor = new BeanWrapperFieldExtractor<>();
extractor.setNames(new String[] { "first", "last", "born" });
​
String first = "Alan";
String last = "Turing";
int born = 1912;
​
Name n = new Name(first, last, born);
Object[] values = extractor.extract(n);
​
assertEquals(first, values[0]);
assertEquals(last, values[1]);
assertEquals(born, values[2]);

这个提取器实现只有一个必需的属性:要映射的字段的名称。正如BeanWrapperFieldSetMapper需要将字段名称映射 FieldSet到所提供对象的 setter 上的字段一样,BeanWrapperFieldExtractor需要将名称映射到 getter 以创建对象数组。值得注意的是,名称的顺序决定了数组中字段的顺序。

分隔文件写入示例

最基本的平面文件格式是所有字段都由分隔符分隔的格式。这可以使用DelimitedLineAggregator. 以下示例写出一个简单的域对象,该对象表示对客户帐户的信用:

public class CustomerCredit {
​
    private int id;
    private String name;
    private BigDecimal credit;
​
    //getters and setters removed for clarity
}

因为正在使用域对象,所以FieldExtractor 必须提供接口的实现以及要使用的分隔符。

以下示例显示了如何FieldExtractor在 Java 中使用带分隔符:

Java 配置

@Bean
public FlatFileItemWriter<CustomerCredit> itemWriter(Resource outputResource) throws Exception {
    BeanWrapperFieldExtractor<CustomerCredit> fieldExtractor = new BeanWrapperFieldExtractor<>();
    fieldExtractor.setNames(new String[] {"name", "credit"});
    fieldExtractor.afterPropertiesSet();
​
    DelimitedLineAggregator<CustomerCredit> lineAggregator = new DelimitedLineAggregator<>();
    lineAggregator.setDelimiter(",");
    lineAggregator.setFieldExtractor(fieldExtractor);
​
    return new FlatFileItemWriterBuilder<CustomerCredit>()
                .name("customerCreditWriter")
                .resource(outputResource)
                .lineAggregator(lineAggregator)
                .build();
}

在前面的示例BeanWrapperFieldExtractor中,本章前面描述的用于将其中的名称和信用字段CustomerCredit转换为对象数组,然后在每个字段之间用逗号写出。

也可以使用FlatFileItemWriterBuilder.DelimitedBuilder自动创建BeanWrapperFieldExtractorand DelimitedLineAggregator ,如下例所示:

Java 配置

@Bean
public FlatFileItemWriter<CustomerCredit> itemWriter(Resource outputResource) throws Exception {
    return new FlatFileItemWriterBuilder<CustomerCredit>()
                .name("customerCreditWriter")
                .resource(outputResource)
                .delimited()
                .delimiter("|")
                .names(new String[] {"name", "credit"})
                .build();
}
固定宽度文件写入示例

分隔不是唯一的平面文件格式类型。许多人更喜欢为每列使用设置的宽度来划分字段之间的界限,这通常称为“固定宽度”。Spring Batch 在使用FormatterLineAggregator.

Java 配置

@Bean
public FlatFileItemWriter<CustomerCredit> itemWriter(Resource outputResource) throws Exception {
    BeanWrapperFieldExtractor<CustomerCredit> fieldExtractor = new BeanWrapperFieldExtractor<>();
    fieldExtractor.setNames(new String[] {"name", "credit"});
    fieldExtractor.afterPropertiesSet();
​
    FormatterLineAggregator<CustomerCredit> lineAggregator = new FormatterLineAggregator<>();
    lineAggregator.setFormat("%-9s%-2.0f");
    lineAggregator.setFieldExtractor(fieldExtractor);
​
    return new FlatFileItemWriterBuilder<CustomerCredit>()
                .name("customerCreditWriter")
                .resource(outputResource)
                .lineAggregator(lineAggregator)
                .build();
}

前面的大多数示例应该看起来很熟悉。但是,格式属性的值是新的。

以下示例显示了 Java 中的格式属性:

...
FormatterLineAggregator<CustomerCredit> lineAggregator = new FormatterLineAggregator<>();
lineAggregator.setFormat("%-9s%-2.0f");
...

底层实现是使用 Formatter作为 Java 的一部分添加的相同内容构建的。

也可以使用FlatFileItemWriterBuilder.FormattedBuilder自动创建BeanWrapperFieldExtractorand FormatterLineAggregator ,如下例所示:

Java 配置

@Bean
public FlatFileItemWriter<CustomerCredit> itemWriter(Resource outputResource) throws Exception {
    return new FlatFileItemWriterBuilder<CustomerCredit>()
                .name("customerCreditWriter")
                .resource(outputResource)
                .formatted()
                .format("%-9s%-2.0f")
                .names(new String[] {"name", "credit"})
                .build();
}
处理文件创建

FlatFileItemReader与文件资源有非常简单的关系。初始化阅读器时,它会打开文件(如果存在),如果不存在则抛出异常。文件写入并不是那么简单。乍一看,似乎应该存在类似的简单契约FlatFileItemWriter:如果文件已经存在,则抛出异常,如果不存在,则创建它并开始编写。但是,可能重新启动 Job可能会导致问题。在正常的重启场景中,合约是相反的:如果文件存在,则从最后一个已知的正确位置开始写入,如果不存在,则抛出异常。但是,如果此作业的文件名始终相同,会发生什么情况?在这种情况下,如果文件存在,您可能希望删除该文件,除非它是重新启动。由于这种可能性,FlatFileItemWriter 包含属性,shouldDeleteIfExists。将此属性设置为 true 会导致在打开编写器时删除同名的现有文件。

XML 项读取器和写入器

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 输入

图 1. XML 输入

“交易”标签在上述场景中被定义为“根元素”。 和 之间的所有内容都被视为一个“片段”。Spring Batch 使用 Object/XML Mapping (OXM) 将片段绑定到对象。但是,Spring Batch 不依赖于任何特定的 XML 绑定技术。典型用途是委托给 Spring OXM,它为最流行的 OXM 技术提供统一的抽象。对 Spring OXM 的依赖是可选的,如果需要,您可以选择实现 Spring Batch 特定的接口。与 OXM 支持的技术的关系如下图所示:

OXM 绑定

图 2. OXM 绑定

通过对 OXM 的介绍以及如何使用 XML 片段来表示记录,我们现在可以更仔细地检查读取器和写入器。

StaxEventItemReader

该StaxEventItemReader配置提供了用于处理来自 XML 输入流的记录的典型设置。首先,考虑StaxEventItemReadercan 处理的以下 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 记录,需要以下内容:

  • 根元素名称:构成要映射的对象的片段的根元素的名称。示例配置通过贸易价值证明了这一点。
  • Resource:表示要读取的文件的 Spring Resource。
  • Unmarshaller:Spring OXM 提供的一种解组工具,用于将 XML 片段映射到对象。

以下示例显示如何定义StaxEventItemReader与名为 的根元素trade、 的资源和在 Java 中data/iosample/input/input.xml调用的解组器一起使用的 :tradeMarshaller

Java 配置

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

请注意,在此示例中,我们选择使用 an XStreamMarshaller,它接受作为映射传入的别名,其中第一个键和值是片段的名称(即根元素)和要绑定的对象类型。然后,与 a 类似FieldSet,映射到对象类型中的字段的其他元素的名称在映射中被描述为键/值对。在配置文件中,我们可以使用 Spring 配置实用程序来描述所需的别名。

以下示例显示了如何在 Java 中描述别名:

Java 配置

@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 文档,并将文档传递给反序列化器(通常是 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.sample.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、一个编组器和一个rootTagName. Resource一个 Java 对象被传递给一个编组器(通常是一个标准的 Spring OXM 编组器),该编组器使用一个自定义事件编写器来写入 a ,该编写器过滤OXM 工具为每个片段生成的事件StartDocument。EndDocument

以下 Java 示例使用MarshallingEventWriterSerializer:

Java 配置

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

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

下面的 Java 示例使用与本章前面的阅读示例中使用的相同的编组器:

Java 配置

@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;
}

总结一个 Java 示例,以下代码说明了所有讨论的要点,演示了所需属性的编程设置:

FileSystemResource resource = new FileSystemResource("data/outputFile.xml")
​
Map aliases = new HashMap();
aliases.put("trade","org.springframework.batch.sample.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);

JSON 项目读取器和写入器

Spring Batch 提供对以下格式的 JSON 资源的读写支持:

[  {    "isin": "123",    "quantity": 1,    "price": 1.2,    "customer": "foo"  },  {    "isin": "456",    "quantity": 2,    "price": 1.4,    "customer": "bar"  }]

假设 JSON 资源是对应于各个项目的 JSON 对象数组。Spring Batch 不绑定到任何特定的 JSON 库。

JsonItemReader

将JsonItemReaderJSON 解析和绑定委托给 org.springframework.batch.item.json.JsonObjectReader接口的实现。此接口旨在通过使用流式 API 以块读取 JSON 对象来实现。目前提供了两种实现:

  • jackson通过org.springframework.batch.item.json.JacksonJsonObjectReader
  • Gson通过org.springframework.batch.item.json.GsonJsonObjectReader

为了能够处理 JSON 记录,需要以下内容:

  • Resource:代表要读取的 JSON 文件的 Spring 资源。
  • JsonObjectReader: 一个 JSON 对象读取器,用于解析 JSON 对象并将其绑定到项目

下面的例子展示了如何定义一个JsonItemReader与之前的 JSON 资源一起工作的org/springframework/batch/item/json/trades.jsona 和一个 JsonObjectReader基于 Jackson 的 a:

@Bean
public JsonItemReader<Trade> jsonItemReader() {
   return new JsonItemReaderBuilder<Trade>()
                 .jsonObjectReader(new JacksonJsonObjectReader<>(Trade.class))
                 .resource(new ClassPathResource("trades.json"))
                 .name("tradeJsonItemReader")
                 .build();
}

JsonFileItemWriter

将JsonFileItemWriter项目编组委托给 org.springframework.batch.item.json.JsonObjectMarshaller接口。该接口的约定是获取一个对象并将其编组为 JSON String。目前提供了两种实现:

  • jackson通过org.springframework.batch.item.json.JacksonJsonObjectMarshaller
  • gson通过org.springframework.batch.item.json.GsonJsonObjectMarshaller

为了能够写入 JSON 记录,需要以下内容:

  • ResourceResource:代表要写入的 JSON 文件的 Spring
  • JsonObjectMarshaller:将对象编组为 JSON 格式的 JSON 对象编组器

以下示例显示了如何定义 a JsonFileItemWriter:

@Bean
public JsonFileItemWriter<Trade> jsonFileItemWriter() {
   return new JsonFileItemWriterBuilder<Trade>()
                 .jsonObjectMarshaller(new JacksonJsonObjectMarshaller<>())
                 .resource(new ClassPathResource("trades.json"))
                 .name("tradeJsonFileItemWriter")
                 .build();
}

代码位置: github.com/jackssybin/…