FlatFileItemWriter
将数据写入平面文件存在与从文件读取时相同的问题和挑战。一个步骤必须能够以事务方式写入分隔格式或固定长度格式。
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 已经定义,现在可以解释编写的基本流程:
-
要写入的对象被传递给
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。 -
将分词返回的
FieldSet传递给FieldSetMapper,并返回ItemReader#read()方法的结果。
文件写入具有相似但相反的步骤:
-
将待写入的项传递给写入器。
-
将项目中的字段转换为数组。
-
将结果数组聚合成一行。
由于框架无法知道对象中的哪些字段需要被写出,因此必须写入一个 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 接口的实现,以及要使用的分隔符。
-
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.infrastructure.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.infrastructure.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 的 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 与文件资源有着非常简单的关系。当读取器初始化时,它会打开文件(如果文件存在),如果文件不存在则抛出异常。文件写入并不那么简单。乍一看,似乎应该为 FlatFileItemWriter 存在一个类似的简单约定:如果文件已存在,则抛出异常;如果不存在,则创建文件并开始写入。然而,
潜在地重启一个 Job 可能会导致问题。在正常的重启场景中,约定是相反的:如果文件存在,则从最后一个已知良好位置开始写入;如果文件不存在,则抛出异常。然而,如果此作业的文件名始终相同会发生什么?在这种情况下,除非是重启,否则您希望在文件存在时将其删除。由于这种可能性,FlatFileItemWriter 包含属性 shouldDeleteIfExists。将此属性设置为 true 会导致在打开写入器时删除具有相同名称的现有文件。