跳到主要内容

FlatFileItemWriter

QWen Plus 中英对照 FlatFileItemWriter FlatFileItemWriter

将数据写入平面文件存在与从文件读取数据时必须克服的相同问题和挑战。一个步骤必须能够以事务方式写入要么是定界格式要么是固定长度格式的数据。

LineAggregator

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

public interface LineAggregator<T> {

public String aggregate(T item);

}
java

LineAggregatorLineTokenizer 的逻辑对立面。LineTokenizer 接收一个 String 并返回一个 FieldSet,而 LineAggregator 接收一个 item 并返回一个 String

PassThroughLineAggregator

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

public class PassThroughLineAggregator<T> implements LineAggregator<T> {

public String aggregate(T item) {
return item.toString();
}
}
java

前面的实现方式在需要直接控制字符串创建的同时,又需要 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();
}
java

FieldExtractor

前面的例子可能对最基本的文件写入操作有用。然而,大多数 FlatFileItemWriter 的用户都有一个需要写入的领域对象,因此必须将其转换为一行。在文件读取中,需要以下内容:

  1. 从文件中读取一行。

  2. 将该行传递给 LineTokenizer#tokenize() 方法,以获取一个 FieldSet

  3. 将从分词过程中返回的 FieldSet 传递给 FieldSetMapper,并返回 ItemReader#read() 方法的结果。

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

  1. 将要写入的项传递给 writer。

  2. 将项上的字段转换为数组。

  3. 将生成的数组聚合为一行。

因为框架无法知道对象中的哪些字段需要被写入,因此必须编写一个 FieldExtractor 来完成将项目转换为数组的任务,如下所示的接口定义:

public interface FieldExtractor<T> {

Object[] extract(T item);

}
java

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

PassThroughFieldExtractor

在许多情况下,需要写出一个集合,例如数组、CollectionFieldSet。从这些集合类型中“提取”一个数组是非常直接的。要做到这一点,可以将集合转换为数组。因此,在这种情况下应使用 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]);
java

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

分隔文件写入示例

最基础的平面文件格式是所有字段都通过分隔符分开的格式。这可以通过使用 DelimitedLineAggregator 来实现。以下示例写入了一个简单的领域对象,该对象表示对客户账户的信用额:

public class CustomerCredit {

private int id;
private String name;
private BigDecimal credit;

//getters and setters removed for clarity
}
java

由于使用了域对象,因此必须提供 FieldExtractor 接口的实现以及要使用的分隔符。

下面的示例展示了如何在 Java 中使用带有分隔符的 FieldExtractor

@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();
}
java

在前面的例子中,本章前面描述的 BeanWrapperFieldExtractor 用于将 CustomerCredit 中的 name 和 credit 字段转换为对象数组,然后以每个字段之间用逗号分隔的方式写出。

还可以使用 FlatFileItemWriterBuilder.DelimitedBuilder 自动创建 BeanWrapperFieldExtractorDelimitedLineAggregator,如下例所示:

@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();
}
java

固定宽度文件写入示例

定界符并不是平面文件格式的唯一类型。许多人更喜欢为每一列设置固定宽度来区分字段,这通常被称为“固定宽度”。Spring Batch 在文件写入时通过 FormatterLineAggregator 支持这种格式。

使用上面描述的相同的 CustomerCredit 领域对象,可以在 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

前面示例的大部分内容应该看起来很熟悉。但是,format 属性的值是新的。

下面的示例展示了 Java 中的 format 属性:

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

底层实现是使用与 Java 5 一起添加的相同 Formatter 构建的。Java Formatter 基于 C 编程语言的 printf 功能。有关如何配置格式化程序的大多数详细信息可以在 Formatter 的 Javadoc 中找到。

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

@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();
}
java

处理文件创建

FlatFileItemReader 与文件资源之间的关系非常简单。当读取器初始化时,它会打开文件(如果文件存在),如果文件不存在则抛出异常。然而,文件写入并不是那么简单。乍一看,FlatFileItemWriter 应该也有一个类似的简单约定:如果文件已经存在,则抛出异常;如果文件不存在,则创建文件并开始写入。但是,潜在的作业重启可能会引发问题。在正常的重启场景中,约定是相反的:如果文件存在,则从上次已知的良好位置继续写入;如果文件不存在,则抛出异常。然而,如果此作业的文件名始终相同,会发生什么呢?在这种情况下,您可能希望在文件存在时删除它,除非这是一个重启操作。由于这种可能性的存在,FlatFileItemWriter 包含了一个名为 shouldDeleteIfExists 的属性。将此属性设置为 true 会导致在写入器打开时删除具有相同名称的现有文件。