FlatFileItemWriter
写入平面文件与从文件读取面临相同的问题和挑战。Step 必须能够以事务方式写入分隔符或固定长度格式。
LineAggregator
正如需要 LineTokenizer
接口将 item 转换为 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
之后,可以解释基本的写入流程了
-
将要写入的对象传递给
LineAggregator
以获取一个String
。 -
将返回的
String
写入到配置的文件中。
FlatFileItemWriter
中的以下代码片段表达了这一点
public void write(T item) throws Exception {
write(lineAggregator.aggregate(item) + LINE_SEPARATOR);
}
-
Java
-
XML
在 Java 中,一个简单的配置示例如下
@Bean
public FlatFileItemWriter itemWriter() {
return new FlatFileItemWriterBuilder<Foo>()
.name("itemWriter")
.resource(new FileSystemResource("target/test-outputs/output.txt"))
.lineAggregator(new PassThroughLineAggregator<>())
.build();
}
在 XML 中,一个简单的配置示例如下
<bean id="itemWriter" class="org.spr...FlatFileItemWriter">
<property name="resource" value="file:target/test-outputs/output.txt" />
<property name="lineAggregator">
<bean class="org.spr...PassThroughLineAggregator"/>
</property>
</bean>
FieldExtractor
上述示例对于最基本的文件写入场景可能有用。但是,大多数 FlatFileItemWriter
用户都有需要写出的领域对象,因此必须将其转换为行。在文件读取中,需要完成以下步骤
-
从文件中读取一行。
-
将该行传递给
LineTokenizer#tokenize()
方法,以获取一个FieldSet
。 -
将分词(tokenize)返回的
FieldSet
传递给一个FieldSetMapper
,返回ItemReader#read()
方法的结果。
文件写入有相似但相反的步骤
-
将要写入的 item 传递给 writer。
-
将 item 上的字段转换为数组。
-
将结果数组聚合成一行。
因为框架无法知道对象中的哪些字段需要写出,所以必须编写一个 FieldExtractor
来完成将 item 转换为数组的任务,如下接口定义所示
public interface FieldExtractor<T> {
Object[] extract(T item);
}
FieldExtractor
接口的实现应该从提供的对象的字段创建一个数组,然后可以使用分隔符分隔元素或作为固定宽度行的一部分写出。
PassThroughFieldExtractor
在许多情况下,需要写出一个集合,例如数组、Collection
或 FieldSet
。从这些集合类型中“提取”数组非常简单。要做到这一点,将集合转换为数组即可。因此,在这种场景下应该使用 PassThroughFieldExtractor
。需要注意的是,如果传入的对象不是集合类型,则 PassThroughFieldExtractor
返回一个仅包含要提取的 item 的数组。
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]);
此 extractor 实现只有一个必需属性:要映射的字段名称。正如 BeanWrapperFieldSetMapper
需要字段名称将 FieldSet
上的字段映射到提供的对象上的 setter 一样,BeanWrapperFieldExtractor
需要名称将 getter 映射到用于创建对象数组的 getter。值得注意的是,名称的顺序决定了数组中字段的顺序。
分隔符文件写入示例
最基本的平面文件格式是所有字段都由分隔符分隔。这可以使用 DelimitedLineAggregator
来实现。以下示例写出一个表示客户账户贷记的简单领域对象
public class CustomerCredit {
private int id;
private String name;
private BigDecimal credit;
//getters and setters removed for clarity
}
由于使用了领域对象,因此必须提供 FieldExtractor
接口的实现,以及要使用的分隔符。
-
Java
-
XML
以下示例展示了如何在 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();
}
以下示例展示了如何在 XML 中使用带有分隔符的 FieldExtractor
<bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
<property name="resource" ref="outputResource" />
<property name="lineAggregator">
<bean class="org.spr...DelimitedLineAggregator">
<property name="delimiter" value=","/>
<property name="fieldExtractor">
<bean class="org.spr...BeanWrapperFieldExtractor">
<property name="names" value="name,credit"/>
</bean>
</property>
</bean>
</property>
</bean>
在前面的示例中,本章前面描述的 BeanWrapperFieldExtractor
用于将 CustomerCredit
中的 name 和 credit 字段转换为对象数组,然后将每个字段用逗号分隔写出。
-
Java
-
XML
也可以使用 FlatFileItemWriterBuilder.DelimitedBuilder
自动创建 BeanWrapperFieldExtractor
和 DelimitedLineAggregator
,如下例所示
@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();
}
没有使用 FlatFileItemWriterBuilder
的 XML 等效方式。
固定宽度文件写入示例
分隔符不是唯一的平面文件格式类型。许多人喜欢为每列设置固定宽度来界定字段,这通常称为“固定宽度”。Spring Batch 通过 FormatterLineAggregator
支持文件写入中的此功能。
-
Java
-
XML
使用上面描述的相同 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();
}
使用上面描述的相同 CustomerCredit
领域对象,可以在 XML 中如下配置
<bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
<property name="resource" ref="outputResource" />
<property name="lineAggregator">
<bean class="org.spr...FormatterLineAggregator">
<property name="fieldExtractor">
<bean class="org.spr...BeanWrapperFieldExtractor">
<property name="names" value="name,credit" />
</bean>
</property>
<property name="format" value="%-9s%-2.0f" />
</bean>
</property>
</bean>
前面的大部分示例应该看起来很熟悉。但是,format 属性的值是新的。
-
Java
-
XML
以下示例展示了 Java 中的 format 属性
...
FormatterLineAggregator<CustomerCredit> lineAggregator = new FormatterLineAggregator<>();
lineAggregator.setFormat("%-9s%-2.0f");
...
以下示例展示了 XML 中的 format 属性
<property name="format" value="%-9s%-2.0f" />
底层实现是使用作为 Java 5 一部分添加的 Formatter
构建的。Java Formatter
基于 C 编程语言的 printf
功能。有关如何配置 Formatter 的大多数详细信息可以在 Formatter 的 Javadoc 中找到。
-
Java
-
XML
也可以使用 FlatFileItemWriterBuilder.FormattedBuilder
自动创建 BeanWrapperFieldExtractor
和 FormatterLineAggregator
,如下例所示
@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
与文件资源的关系非常简单。当 reader 初始化时,它会打开文件(如果存在),如果不存在则抛出异常。文件写入就没有那么简单了。乍一看,FlatFileItemWriter
似乎应该存在一个类似的直接约定:如果文件已存在,则抛出异常;如果不存在,则创建并开始写入。然而,Job
的潜在重启可能会导致问题。在正常的重启场景中,约定是相反的:如果文件存在,则从最后一个已知良好位置开始写入;如果不存在,则抛出异常。但是,如果此 job 的文件名总是相同会发生什么呢?在这种情况下,您可能希望在文件存在时删除它,除非是重启。由于这种可能性,FlatFileItemWriter
包含 shouldDeleteIfExists
属性。将此属性设置为 true 会在 writer 打开时删除同名的现有文件。