FlatFileItemWriter

寫入平面檔案與從檔案讀取資料必須克服相同的問題與狀況。一個 Step 必須能夠以交易方式寫入分隔或固定長度格式。

LineAggregator

正如需要 LineTokenizer 介面來取得 item 並將其轉換為 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. 將從 tokenizing 傳回的 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]);

此提取器實作只有一個必需的屬性:要對應的欄位名稱。正如 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 的大多數詳細資訊可以在 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 時刪除具有相同名稱的現有檔案。