当前版本仍在开发中,尚不被视为稳定版本。最新稳定版请使用 Spring Batch 文档 6.0.2!

FlatFileItemWriter

向平面文件写出数据时,需要解决的问题与从文件读取时基本相同。一个 step 必须能够以事务方式写出分隔型或定长型格式的数据。

LineAggregator

就像读取时需要 LineTokenizer 把一行文本拆分为结构化数据一样,文件写出时也需要一种机制把多个字段聚合为一个字符串, 以便写入文件。在 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 已经定义清楚了,因此可以说明文件写出的基本流程:

  1. 把待写出的对象传给 LineAggregator,得到一个 String

  2. 把返回的 String 写入已配置的文件。

下面这段来自 FlatFileItemWriter 的代码就体现了这一过程:

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

  • XML

在 Java 中,一个简单的配置示例如下:

Java Configuration
@Bean
public FlatFileItemWriter itemWriter() {
	return  new FlatFileItemWriterBuilder<Foo>()
           			.name("itemWriter")
           			.resource(new FileSystemResource("target/test-outputs/output.txt"))
           			.lineAggregator(new PassThroughLineAggregator<>())
           			.build();
}

在 XML 中,一个简单的配置示例如下:

XML Configuration
<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. 把待写出的 item 传给 writer。

  2. 把 item 上的字段转换为数组。

  3. 把这个数组聚合为一行文本。

由于框架无法自行知道对象中的哪些字段需要被写出,因此必须提供一个 FieldExtractor,负责把 item 转换成数组, 如下接口定义所示:

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

这个 extractor 实现只需要一个必需属性:要映射的字段名。就像 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 Configuration
@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 Configuration
<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 中的 namecredit 字段转换为对象数组,然后再用逗号把各字段分隔后写出。

  • Java

  • XML

也可以使用 FlatFileItemWriterBuilder.DelimitedBuilder 自动创建 BeanWrapperFieldExtractorDelimitedLineAggregator,如下例所示:

Java Configuration
@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 配置中没有与 FlatFileItemWriterBuilder 对应的等价写法。

定宽文件写出示例

分隔型并不是唯一的平面文件格式。很多场景更倾向于为每一列指定固定宽度,以此区分字段,这通常被称为“定宽”格式。 Spring Batch 通过 FormatterLineAggregator 支持这种文件写出方式。

  • Java

  • XML

继续使用上面介绍的 CustomerCredit 领域对象,在 Java 中可以这样配置:

Java Configuration
@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 Configuration
<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 的详细配置方式,大部分都可以在 Formatter 的 Javadoc 中找到。

  • Java

  • XML

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

Java Configuration
@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 打开时会删除同名的已有文件。