6.6.3 FlatFileItemWriter

将数据写入到纯文本文件也必须解决和读取文件时一样的问题。 在事务中,一个 step 必须通过分隔符或采用固定长度的格式将数据写出去.

LineAggregator

LineTokenizer 接口的处理方式类似, 写入文件时也需要有某种方式将一条记录的多个字段组织拼接成单个 String,然后再将string写入文件. Spring Batch 对应的接口是 LineAggregator:

  1. public interface LineAggregator<T> {
  2. public String aggregate(T item);
  3. }

接口 LineAggregator 与 LineTokenizer 相互对应. LineTokenizer 接收 String ,处理后返回一个 FieldSet 对象, 而 LineAggregator 则是接收一条记录,返回对应的 String.

PassThroughLineAggregator

LineAggregator 接口最基础的实现类是 PassThroughLineAggregator, 这个简单实现仅仅是将接收到的对象调用 toString() 方法的值返回:

  1. public class PassThroughLineAggregator<T> implements LineAggregator<T> {
  2. public String aggregate(T item) {
  3. return item.toString();
  4. }
  5. }

上面的实现对于需要直接转换为string的时候是很管用的,但是 FlatFileItemWriter 的一些优势也是很有必要的,比如 事务,以及 支持重启特性等.

简单的文件写入示例

既然已经有了 LineAggregator 接口以及其最基础的实现, PassThroughLineAggregator, 那就可以解释基础的写出流程了:

  1. 将要写出的对象传递给 LineAggregator 以获取一个字符串(String).
  2. 将返回的 String 写入配置指定的文件中.

下面是 FlatFileItemWriter 中对应的代码:

  1. public void write(T item) throws Exception {
  2. write(lineAggregator.aggregate(item) + LINE_SEPARATOR);
  3. }

简单的配置如下所示:

  1. <bean id="itemWriter" class="org.spr...FlatFileItemWriter">
  2. <property name="resource" value="file:target/test-outputs/output.txt" />
  3. <property name="lineAggregator">
  4. <bean class="org.spr...PassThroughLineAggregator"/>
  5. </property>
  6. </bean>

属性提取器 FieldExtractor

上面的示例可以应对最基本的文件写入情景。但使用 FlatFileItemWriter 时可能更多地是需要将某个领域对象写到文件,因此必须转换到单行之中。 在读取文件时,有以下步骤:

  1. 从文件中读取一行.
  2. 将这一行字符串传递给 LineTokenizer#tokenize() 方法, 以获取 FieldSet 对象
  3. 将分词器返回的 FieldSet 传给一个 FieldSetMapper 映射器, 然后将 ItemReader#read() 方法得到的结果 return。

文件的写入也很类似, 但步骤正好相反:

  1. 将要写入的对象传递给 writer
  2. 将领域对象的属性域转换为数组
  3. 将结果数组合并(aggregate)为一行字符串

因为框架没办法知道需要将领域对象的哪些字段写入到文件中,所以就需要有一个 FieldExtractor 来将对象转换为数组:

  1. public interface FieldExtractor<T> {
  2. Object[] extract(T item);
  3. }

FieldExtractor 的实现类应该根据传入对象的属性创建一个数组, 稍后使用分隔符将各个元素写入文件,或者作为 field-width line 的一部分.

PassThroughFieldExtractor

在很多时候需要将一个集合(如 array、Collection, FieldSet等)写出到文件。 从集合中“提取”一个数组那真的是非常简单: 直接进行简单转换即可。 因此在这种场合 PassThroughFieldExtractor 就派上用场了。应该注意,如果传入的对象不是集合类型的, 那么 PassThroughFieldExtractor 将返回一个数组, 其中只包含提取的单个对象。

BeanWrapperFieldExtractor

与文件读取一节中所描述的 BeanWrapperFieldSetMapper 一样, 通常使用配置来指定如何将领域对象转换为一个对象数组是比较好的办法, 而不用自己写个方法来进行转换。BeanWrapperFieldExtractor 就提供了这类功能:

  1. BeanWrapperFieldExtractor<Name> extractor = new BeanWrapperFieldExtractor<Name>();
  2. extractor.setNames(new String[] { "first", "last", "born" });
  3. String first = "Alan";
  4. String last = "Turing";
  5. int born = 1912;
  6. Name n = new Name(first, last, born);
  7. Object[] values = extractor.extract(n);
  8. assertEquals(first, values[0]);
  9. assertEquals(last, values[1]);
  10. assertEquals(born, values[2]);

这个 extractor 实现只有一个必需的属性,就是 names, 里面用来存放要映射字段的名字。 就像 BeanWrapperFieldSetMapper 需要字段名称来将 FieldSet 中的 field 映射到对象的 setter 方法一样, BeanWrapperFieldExtractor 需要 names 映射 getter 方法来创建一个对象数组。值得注意的是, names的顺序决定了field在数组中的顺序。

分隔符文件(Delimited File)写入示例

最基础的平面文件格式是将所有字段用分隔符(delimiter)来进行分隔(separated)。这可以通过 DelimitedLineAggregator 来完成。下面的例子把一个表示客户信用额度的领域对象写出:

  1. public class CustomerCredit {
  2. private int id;
  3. private String name;
  4. private BigDecimal credit;
  5. //getters and setters removed for clarity
  6. }

因为使用到了领域对象,所以必须提供 FieldExtractor 接口的实现,当然也少不了要使用的分隔符:

  1. <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
  2. <property name="resource" ref="outputResource" />
  3. <property name="lineAggregator">
  4. <bean class="org.spr...DelimitedLineAggregator">
  5. <property name="delimiter" value=","/>
  6. <property name="fieldExtractor">
  7. <bean class="org.spr...BeanWrapperFieldExtractor">
  8. <property name="names" value="name,credit"/>
  9. </bean>
  10. </property>
  11. </bean>
  12. </property>
  13. </bean>

在这种情况下, 本章前面提到过的 BeanWrapperFieldExtractor 被用来将 CustomerCredit 中的 name 和 credit 字段转换为一个对象数组, 然后在各个字段之间用逗号分隔写入文件。

固定宽度的(Fixed Width)文件写入示例

平面文件的格式并不是只有采用分隔符这种类型。许多人喜欢对每个字段设置一定的宽度,这样就能区分各个字段了,这种做法通常被称为“固定宽度, fixed width”。 Spring Batch 通过 FormatterLineAggregator 支持这种文件的写入。使用上面描述的 CustomerCredit 领域对象, 则可以对它进行如下配置:

  1. <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
  2. <property name="resource" ref="outputResource" />
  3. <property name="lineAggregator">
  4. <bean class="org.spr...FormatterLineAggregator">
  5. <property name="fieldExtractor">
  6. <bean class="org.spr...BeanWrapperFieldExtractor">
  7. <property name="names" value="name,credit" />
  8. </bean>
  9. </property>
  10. <property name="format" value="%-9s%-2.0f" />
  11. </bean>
  12. </property>
  13. </bean>

上面的示例大部分看起来是一样的, 只有 format 属性的值不同:

  1. <property name="format" value="%-9s%-2.0f" />

底层实现采用 Java 5 提供的 Formatter 。Java的 Formatter (格式化) 基于C语言的 printf 函数功能。关于如何配置 formatter 请参考 Formatter 的javadoc.

处理文件创建(Handling File Creation)

FlatFileItemReader 与文件资源的关系很简单。在初始化 reader 时,如果文件存在则打开, 如果文件不存在那就抛出一个异常(exception)。

但是文件的写入就没那么简单了。乍一看可能会觉得跟 FlatFileItemWriter 一样简单直接粗暴: 如果文件存在则抛出异常, 如果不存在则创建文件并开始写入。

但是, 作业的重启有可能会有BUG。 在正常的重启情景中, 约定与前面所想的恰恰相反: 如果文件存在, 则从已知的最后一个正确位置开始写入, 如果不存在, 则抛出异常。

如果此作业(Job)的文件名每次都是一样的那怎么办? 这时候可能需要删除已存在的文件(重启则不删除)。 因为有这些可能性, FlatFileItemWriter 有一个属性 shouldDeleteIfExists。将这个属性设置为 true , 打开 writer 时会将已有的同名文件删除。