FlatFileItemWriter

寫入平面檔案與從檔案讀取面臨相同的問題和挑戰。Step 必須能夠以事務方式寫入分隔符或固定長度格式。

LineAggregator

正如需要 LineTokenizer 介面將 item 轉換為 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 的優勢(例如事務和重啟支援),則上述實現非常有用。

簡化檔案寫入示例

在定義了 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. 將分詞(tokenize)返回的 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 返回一個僅包含要提取的 item 的陣列。

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 對映到用於建立物件陣列的 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 的大多數詳細資訊可以在 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 與檔案資源的關係非常簡單。當 reader 初始化時,它會開啟檔案(如果存在),如果不存在則丟擲異常。檔案寫入就沒有那麼簡單了。乍一看,FlatFileItemWriter 似乎應該存在一個類似的直接約定:如果檔案已存在,則丟擲異常;如果不存在,則建立並開始寫入。然而,Job 的潛在重啟可能會導致問題。在正常的重啟場景中,約定是相反的:如果檔案存在,則從最後一個已知良好位置開始寫入;如果不存在,則丟擲異常。但是,如果此 job 的檔名總是相同會發生什麼呢?在這種情況下,您可能希望在檔案存在時刪除它,除非是重啟。由於這種可能性,FlatFileItemWriter 包含 shouldDeleteIfExists 屬性。將此屬性設定為 true 會在 writer 開啟時刪除同名的現有檔案。