跳到主要内容
版本:6.0.2

FlatFileItemWriter

DeepSeek V3 中英对照 FlatFileItemWriter FlatFileItemWriter

写入平面文件时,会遇到与从文件读取数据时相同的问题和挑战。一个步骤必须能够以事务处理的方式写入分隔格式或固定长度格式。

LineAggregator

正如需要 LineTokenizer 接口将数据项转换为 String 一样,文件写入也必须具备将多个字段聚合成单个字符串以写入文件的能力。在 Spring Batch 中,这一功能由 LineAggregator 实现,其接口定义如下:

public interface LineAggregator<T> {

public String aggregate(T item);

}

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

如果需要对字符串的创建进行直接控制,同时又需要 FlatFileItemWriter 的优势(如事务和重启支持),那么上述实现会很有用。

简化文件写入示例

现在,LineAggregator 接口及其最基本的实现 PassThroughLineAggregator 已经定义完毕,可以解释写入的基本流程了:

  1. 待写入的对象被传递给 LineAggregator 以获取一个 String

  2. 返回的 String 被写入到配置的文件中。

以下代码片段来自 FlatFileItemWriter,它表达了这一点:

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

在 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 传递给 FieldSetMapper,最终通过 ItemReader#read() 方法返回处理结果。

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

  1. 将待写入项传递给写入器。

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

  3. 将结果数组聚合成一行。

由于框架无法知晓需要从对象中提取哪些字段,因此必须编写 FieldExtractor 来完成将数据项转换为数组的任务,如下方接口定义所示:

public interface FieldExtractor<T> {

Object[] extract(T item);

}

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]);

该提取器实现仅有一个必需属性:待映射字段的名称。正如 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 接口的实现,并指定要使用的分隔符。

以下示例展示了如何在 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();
}

在前面的示例中,使用了本章先前介绍的 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();
}

固定宽度文件写入示例

分隔符并非平面文件格式的唯一类型。许多人倾向于为每列使用固定宽度来划分字段,这种方式通常被称为"固定宽度"。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();
}

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

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

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

底层实现基于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();
}

处理文件创建

FlatFileItemReader 与文件资源的关系非常简单。当读取器初始化时,如果文件存在则打开文件,如果不存在则抛出异常。文件写入则不那么简单。乍一看,FlatFileItemWriter 似乎应该遵循类似的直接约定:如果文件已存在则抛出异常,如果不存在则创建并开始写入。然而,重启 Job 的可能性会引发问题。在正常的重启场景中,约定恰恰相反:如果文件存在,则从最后已知的有效位置开始写入;如果不存在,则抛出异常。但如果此作业的文件名始终保持不变呢?在这种情况下,除非是重启场景,否则你会希望删除已存在的文件。鉴于这种可能性,FlatFileItemWriter 包含了一个属性 shouldDeleteIfExists。将此属性设置为 true 会在写入器打开时删除同名的现有文件。