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
的优点(如事务和重启支持),那么前面的实现很有用。
Simplified File Writing Example
现在已定义 LineAggregator
接口及其最基本的实现 PassThroughLineAggregator
,因此可以解释写入的基本流程:
-
要写入的对象传递给
LineAggregator
以获取String
。 -
返回的
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();
}
- XML
-
在 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 以创建对象数组。值得注意的是,名称的顺序决定了数组中字段的顺序。
Delimited File Writing Example
最基本的平面文件格式是其中所有字段都由定界符分隔的格式。这可以用 DelimitedLineAggregator
实现。以下示例写出了一个简单的表示为客户帐户贷项的域对象:
public class CustomerCredit {
private int id;
private String name;
private BigDecimal credit;
//getters and setters removed for clarity
}
因为正在使用域对象,所以必须提供 FieldExtractor
接口的实现,以及要使用的定界符。
- Java
-
以下示例演示了如何在 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
-
以下示例演示了如何在 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
中的名称和信用字段转换为对象数组,然后在每个字段之间用逗号将该数组写出。
- Java
-
你也可以使用
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();
}
- XML
-
在 XML 中没有使用
FlatFileItemWriterBuilder
的等效示例。
Fixed Width File Writing Example
定界并不是平面文件格式的唯一类型。许多人更喜欢对每列使用设定宽度来分隔字段,这通常称为“固定宽度”。Spring Batch 在文件写入中使用 FormatterLineAggregator
支持这样做。
- Java
-
使用上面描述的相同
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();
}
- XML
-
使用上面描述的相同
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>
前面的示例大部分应看起来很熟悉。但是,“格式”属性的值是新的。
- Java
-
以下示例演示了 Java 中的格式属性:
...
FormatterLineAggregator<CustomerCredit> lineAggregator = new FormatterLineAggregator<>();
lineAggregator.setFormat("%-9s%-2.0f");
...
- XML
-
以下示例演示了 XML 中的格式属性:
<property name="format" value="%-9s%-2.0f" />
底层实现使用添加为 Java 5 一部分的相同 `Formatter`构建。Java `Formatter`基于 C 编程语言的 `printf`功能。在 Formatter的 Javadoc 中可以找到有关如何配置格式化程序的大多数详细信息。
- Java
-
也可以使用
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();
}
- XML
Handling File Creation
FlatFileItemReader
与文件资源之间的关系非常简单。当读取器初始化时,它将打开文件(如果存在),并且如果文件不存在,它将引发异常。文件写入并不那么简单。乍一看,似乎 FlatFileItemWriter
应该存在类似的简单的契约:如果文件已存在,则引发异常;如果文件不存在,则创建文件并开始写入。但是,重新启动 Job
可能会导致问题。在正常重新启动场景中,契约相反:如果文件存在,则从最后已知的好位置开始向其中写入;如果文件不存在,则引发异常。但是,如果此作业的文件名始终相同,将会发生什么?在这种情况下,如果这不是重新启动,您将希望删除该文件(如果存在)。由于这种可能性,FlatFileItemWriter
包含属性 shouldDeleteIfExists
。将此属性设置为 true 将导致在打开写入器时,删除具有相同名称的现有文件。