FlatFileItemWriter

写入平面文件具有与从文件中读取时必须克服的相同问题。必须能够以事务方式写入定界或固定长度格式。

Writing out to flat files has the same problems and issues that reading in from a file must overcome. A step must be able to write either delimited or fixed length formats in a transactional manner.

LineAggregator

正如 LineTokenizer 接口对于获取一项并将其转换为 String 是必需的,文件写入必须能够将多个字段聚合到一个字符串中以写入文件。在 Spring Batch 中,这是 LineAggregator,如下面的接口定义所示:

Just as the LineTokenizer interface is necessary to take an item and turn it into a String, file writing must have a way to aggregate multiple fields into a single string for writing to a file. In Spring Batch, this is the LineAggregator, shown in the following interface definition:

public interface LineAggregator<T> {

    public String aggregate(T item);

}

LineAggregatorLineTokenizer 的逻辑反义词。LineTokenizer 获取 String 并返回 FieldSet,而 LineAggregator 获取 item 并返回 String

The LineAggregator is the logical opposite of LineTokenizer. LineTokenizer takes a String and returns a FieldSet, whereas LineAggregator takes an item and returns a String.

PassThroughLineAggregator

LineAggregator 接口的最基本实现是 PassThroughLineAggregator,该实现假设该对象已经是字符串或其字符串表示形式可以接受写入,如下面的代码所示:

The most basic implementation of the LineAggregator interface is the PassThroughLineAggregator, which assumes that the object is already a string or that its string representation is acceptable for writing, as shown in the following code:

public class PassThroughLineAggregator<T> implements LineAggregator<T> {

    public String aggregate(T item) {
        return item.toString();
    }
}

如果需要直接控制创建字符串,但需要 FlatFileItemWriter 的优点(如事务和重启支持),那么前面的实现很有用。

The preceding implementation is useful if direct control of creating the string is required but the advantages of a FlatFileItemWriter, such as transaction and restart support, are necessary.

Simplified File Writing Example

现在已定义 LineAggregator 接口及其最基本的实现 PassThroughLineAggregator,因此可以解释写入的基本流程:

Now that the LineAggregator interface and its most basic implementation, PassThroughLineAggregator, have been defined, the basic flow of writing can be explained:

  1. The object to be written is passed to the LineAggregator in order to obtain a String.

  2. The returned String is written to the configured file.

以下来自 FlatFileItemWriter 的摘录以代码表示:

The following excerpt from the FlatFileItemWriter expresses this in code:

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

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

In Java, a simple example of configuration might look like the following:

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 中,简单的配置示例如下所示:

In XML, a simple example of configuration might look like the following:

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 的大多数用户都有需要写入后再转换成行的领域对象。在文件读取中,需要以下内容:

The preceding example may be useful for the most basic uses of a writing to a file. However, most users of the FlatFileItemWriter have a domain object that needs to be written out and, thus, must be converted into a line. In file reading, the following was required:

  1. Read one line from the file.

  2. Pass the line into the LineTokenizer#tokenize() method, in order to retrieve a FieldSet.

  3. Pass the FieldSet returned from tokenizing to a FieldSetMapper, returning the result from the ItemReader#read() method.

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

File writing has similar but inverse steps:

  1. Pass the item to be written to the writer.

  2. Convert the fields on the item into an array.

  3. Aggregate the resulting array into a line.

因为该框架无法知道需要从该对象中写入哪些字段,所以必须编写 FieldExtractor 来完成将项转换为数组的任务,如下面的接口定义所示:

Because there is no way for the framework to know which fields from the object need to be written out, a FieldExtractor must be written to accomplish the task of turning the item into an array, as shown in the following interface definition:

public interface FieldExtractor<T> {

    Object[] extract(T item);

}

FieldExtractor 接口的实现应从给定对象的字段创建一个数组,然后可以用元素之间的定界符或作为固定宽度的行的一部分将其写出。

Implementations of the FieldExtractor interface should create an array from the fields of the provided object, which can then be written out with a delimiter between the elements or as part of a fixed-width line.

PassThroughFieldExtractor

在很多情况下,需要写出集合,如数组、CollectionFieldSet。从这些集合类型中“抽取”数组非常简单。为此,将该集合转换为数组。因此,在此场景中应使用 PassThroughFieldExtractor。应注意,如果传递的对象不是集合类型,则 PassThroughFieldExtractor 返回仅包含要提取项的数组。

There are many cases where a collection, such as an array, Collection, or FieldSet, needs to be written out. "Extracting" an array from one of these collection types is very straightforward. To do so, convert the collection to an array. Therefore, the PassThroughFieldExtractor should be used in this scenario. It should be noted that, if the object passed in is not a type of collection, then the PassThroughFieldExtractor returns an array containing solely the item to be extracted.

BeanWrapperFieldExtractor

与文件读取部分中描述的 BeanWrapperFieldSetMapper 一样,通常最好配置如何将域对象转换为对象数组,而不是自己编写转换。BeanWrapperFieldExtractor 提供此功能,如下面的示例所示:

As with the BeanWrapperFieldSetMapper described in the file reading section, it is often preferable to configure how to convert a domain object to an object array, rather than writing the conversion yourself. The BeanWrapperFieldExtractor provides this functionality, as shown in the following example:

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 以创建对象数组。值得注意的是,名称的顺序决定了数组中字段的顺序。

This extractor implementation has only one required property: the names of the fields to map. Just as the BeanWrapperFieldSetMapper needs field names to map fields on the FieldSet to setters on the provided object, the BeanWrapperFieldExtractor needs names to map to getters for creating an object array. It is worth noting that the order of the names determines the order of the fields within the array.

Delimited File Writing Example

最基本的平面文件格式是其中所有字段都由定界符分隔的格式。这可以用 DelimitedLineAggregator 实现。以下示例写出了一个简单的表示为客户帐户贷项的域对象:

The most basic flat file format is one in which all fields are separated by a delimiter. This can be accomplished using a DelimitedLineAggregator. The following example writes out a simple domain object that represents a credit to a customer account:

public class CustomerCredit {

    private int id;
    private String name;
    private BigDecimal credit;

    //getters and setters removed for clarity
}

因为正在使用域对象,所以必须提供 FieldExtractor 接口的实现,以及要使用的定界符。

Because a domain object is being used, an implementation of the FieldExtractor interface must be provided, along with the delimiter to use.

Java

以下示例演示了如何在 Java 中使用带定界符的 FieldExtractor

The following example shows how to use the FieldExtractor with a delimiter in 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();

	DelimitedLineAggregator<CustomerCredit> lineAggregator = new DelimitedLineAggregator<>();
	lineAggregator.setDelimiter(",");
	lineAggregator.setFieldExtractor(fieldExtractor);

	return new FlatFileItemWriterBuilder<CustomerCredit>()
				.name("customerCreditWriter")
				.resource(outputResource)
				.lineAggregator(lineAggregator)
				.build();
}
XML

以下示例演示了如何在 XML 中使用带定界符的 FieldExtractor

The following example shows how to use the FieldExtractor with a delimiter in XML:

XML Configuration
<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 中的名称和信用字段转换为对象数组,然后在每个字段之间用逗号将该数组写出。

In the previous example, the BeanWrapperFieldExtractor described earlier in this chapter is used to turn the name and credit fields within CustomerCredit into an object array, which is then written out with commas between each field.

Java

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

It is also possible to use the FlatFileItemWriterBuilder.DelimitedBuilder to automatically create the BeanWrapperFieldExtractor and DelimitedLineAggregator as shown in the following example:

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

在 XML 中没有使用 FlatFileItemWriterBuilder 的等效示例。

There is no XML equivalent of using FlatFileItemWriterBuilder.

Fixed Width File Writing Example

定界并不是平面文件格式的唯一类型。许多人更喜欢对每列使用设定宽度来分隔字段,这通常称为“固定宽度”。Spring Batch 在文件写入中使用 FormatterLineAggregator 支持这样做。

Delimited is not the only type of flat file format. Many prefer to use a set width for each column to delineate between fields, which is usually referred to as 'fixed width'. Spring Batch supports this in file writing with the FormatterLineAggregator.

Java

使用上面描述的相同 CustomerCredit 域对象,可以在 Java 中对其配置如下:

Using the same CustomerCredit domain object described above, it can be configured as follows in 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();
}
XML

使用上面描述的相同 CustomerCredit 域对象,可以在 XML 中对其配置如下:

Using the same CustomerCredit domain object described above, it can be configured as follows in XML:

XML Configuration
<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>

前面的示例大部分应看起来很熟悉。但是,“格式”属性的值是新的。

Most of the preceding example should look familiar. However, the value of the format property is new.

Java

以下示例演示了 Java 中的格式属性:

The following example shows the format property in Java:

...
FormatterLineAggregator<CustomerCredit> lineAggregator = new FormatterLineAggregator<>();
lineAggregator.setFormat("%-9s%-2.0f");
...
XML

以下示例演示了 XML 中的格式属性:

The following example shows the format property in XML:

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

底层实现使用添加为 Java 5 一部分的相同 `Formatter`构建。Java `Formatter`基于 C 编程语言的 `printf`功能。在 Formatter的 Javadoc 中可以找到有关如何配置格式化程序的大多数详细信息。

The underlying implementation is built using the same Formatter added as part of Java 5. The Java Formatter is based on the printf functionality of the C programming language. Most details on how to configure a formatter can be found in the Javadoc of Formatter.

Java

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

It is also possible to use the FlatFileItemWriterBuilder.FormattedBuilder to automatically create the BeanWrapperFieldExtractor and FormatterLineAggregator as shown in following example:

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();
}
XML

Handling File Creation

FlatFileItemReader 与文件资源之间的关系非常简单。当读取器初始化时,它将打开文件(如果存在),并且如果文件不存在,它将引发异常。文件写入并不那么简单。乍一看,似乎 FlatFileItemWriter 应该存在类似的简单的契约:如果文件已存在,则引发异常;如果文件不存在,则创建文件并开始写入。但是,重新启动 Job 可能会导致问题。在正常重新启动场景中,契约相反:如果文件存在,则从最后已知的好位置开始向其中写入;如果文件不存在,则引发异常。但是,如果此作业的文件名始终相同,将会发生什么?在这种情况下,如果这不是重新启动,您将希望删除该文件(如果存在)。由于这种可能性,FlatFileItemWriter 包含属性 shouldDeleteIfExists。将此属性设置为 true 将导致在打开写入器时,删除具有相同名称的现有文件。

FlatFileItemReader has a very simple relationship with file resources. When the reader is initialized, it opens the file (if it exists), and throws an exception if it does not. File writing isn’t quite so simple. At first glance, it seems like a similar straightforward contract should exist for FlatFileItemWriter: If the file already exists, throw an exception, and, if it does not, create it and start writing. However, potentially restarting a Job can cause issues. In normal restart scenarios, the contract is reversed: If the file exists, start writing to it from the last known good position, and, if it does not, throw an exception. However, what happens if the file name for this job is always the same? In this case, you would want to delete the file if it exists, unless it’s a restart. Because of this possibility, the FlatFileItemWriter contains the property, shouldDeleteIfExists. Setting this property to true causes an existing file with the same name to be deleted when the writer is opened.