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

  • XML

在 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 的大多数用户都有一个需要写出的域对象,因此必须将其转换为一行。在文件读取中,需要以下操作

  1. 从文件中读取一行。

  2. 将该行传递给 LineTokenizer#tokenize() 方法,以检索 FieldSet

  3. 将从标记化返回的 FieldSet 传递给 FieldSetMapper,并返回 ItemReader#read() 方法的结果。

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

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

  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

  • XML

以下示例显示了如何在 Java 中使用 FieldExtractor 和分隔符

Java 配置
@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 和分隔符

XML 配置
<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 自动创建 BeanWrapperFieldExtractorDelimitedLineAggregator,如下例所示

Java 配置
@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 中如下配置

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 中如下配置

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 的 Javadoc 中找到。

  • Java

  • XML

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

Java 配置
@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 会导致在写入器打开时删除具有相同名称的现有文件。