FlatFileItemWriter
寫入平面檔案與從檔案讀取具有相同的問題。一個步驟必須能夠以事務方式寫入分隔符或固定長度格式。
LineAggregator
正如 LineTokenizer 介面對於將項轉換為 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,可以解釋寫入的基本流程
-
要寫入的物件被傳遞給
LineAggregator以獲取String。 -
返回的
String被寫入到配置的檔案中。
FlatFileItemWriter 的以下摘錄用程式碼表達了這一點
public void write(T item) throws Exception {
write(lineAggregator.aggregate(item) + LINE_SEPARATOR);
}
-
Java
-
XML
在 Java 中,一個簡單的配置示例可能如下所示
@Bean
public FlatFileItemWriter itemWriter() {
return new FlatFileItemWriterBuilder<Foo>()
.name("itemWriter")
.resource(new FileSystemResource("target/test-outputs/output.txt"))
.lineAggregator(new PassThroughLineAggregator<>())
.build();
}
在 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 的大多數使用者都有一個需要寫入的域物件,因此必須將其轉換為一行。在檔案讀取中,需要以下內容
-
從檔案中讀取一行。
-
將該行傳遞給
LineTokenizer#tokenize()方法,以檢索FieldSet。 -
將標記化返回的
FieldSet傳遞給FieldSetMapper,返回ItemReader#read()方法的結果。
檔案寫入有類似但相反的步驟
-
將要寫入的項傳遞給寫入器。
-
將專案中的欄位轉換為陣列。
-
將生成的陣列聚合為一行。
由於框架無法知道物件中的哪些欄位需要寫入,因此必須編寫 FieldExtractor 來完成將項轉換為陣列的任務,如以下介面定義所示
public interface FieldExtractor<T> {
Object[] extract(T item);
}
FieldExtractor 介面的實現應該從提供的物件的欄位建立陣列,然後可以使用元素之間的分隔符或作為固定寬度行的一部分寫入。
PassThroughFieldExtractor
在許多情況下,需要寫入集合,例如陣列、Collection 或 FieldSet。“提取”其中一種集合型別的陣列非常簡單。為此,將集合轉換為陣列。因此,在這種情況下應使用 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]);
此提取器實現只有一個必需屬性:要對映的欄位名稱。正如 BeanWrapperFieldSetMapper 需要欄位名稱來將 FieldSet 中的欄位對映到所提供物件上的設定器一樣,BeanWrapperFieldExtractor 需要名稱來對映到獲取器以建立物件陣列。值得注意的是,名稱的順序決定了陣列中欄位的順序。
分隔檔案寫入示例
最基本的平面檔案格式是所有欄位都由分隔符分隔的格式。這可以透過使用 DelimitedLineAggregator 來實現。以下示例寫入一個表示客戶賬戶信用的簡單域物件
public class CustomerCredit {
private int id;
private String name;
private BigDecimal credit;
//getters and setters removed for clarity
}
由於正在使用域物件,因此必須提供 FieldExtractor 介面的實現,以及要使用的分隔符。
-
Java
-
XML
以下示例顯示瞭如何在 Java 中使用帶有分隔符的 FieldExtractor
@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
<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 中的名稱和信用欄位轉換為物件陣列,然後用逗號分隔每個欄位寫入。
-
Java
-
XML
也可以使用 FlatFileItemWriterBuilder.DelimitedBuilder 自動建立 BeanWrapperFieldExtractor 和 DelimitedLineAggregator,如以下示例所示
@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 中按如下方式配置
@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 中按如下方式配置
<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>
上述大部分示例應該看起來很熟悉。但是,格式屬性的值是新的。
-
Java
-
XML
以下示例顯示了 Java 中的格式屬性
...
FormatterLineAggregator<CustomerCredit> lineAggregator = new FormatterLineAggregator<>();
lineAggregator.setFormat("%-9s%-2.0f");
...
以下示例顯示了 XML 中的格式屬性
<property name="format" value="%-9s%-2.0f" />
底層實現是使用 Java 5 中新增的相同 Formatter 構建的。Java Formatter 基於 C 程式語言的 printf 功能。有關如何配置格式化程式的大多數詳細資訊可以在 Formatter 的 Javadoc 中找到。
-
Java
-
XML
也可以使用 FlatFileItemWriterBuilder.FormattedBuilder 自動建立 BeanWrapperFieldExtractor 和 FormatterLineAggregator,如以下示例所示
@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 與檔案資源的關係非常簡單。當讀取器初始化時,它會開啟檔案(如果存在),如果不存在則丟擲異常。檔案寫入並不是那麼簡單。乍一看,FlatFileItemWriter 似乎應該存在類似的簡單契約:如果檔案已經存在,則丟擲異常;如果不存在,則建立它並開始寫入。然而,潛在地重新啟動 Job 可能會導致問題。在正常的重新啟動場景中,契約是反向的:如果檔案存在,則從上次已知的好位置開始寫入;如果不存在,則丟擲異常。但是,如果此作業的檔名始終相同,會發生什麼?在這種情況下,除非是重新啟動,否則您將希望刪除檔案(如果存在)。由於這種可能性,FlatFileItemWriter 包含屬性 shouldDeleteIfExists。將此屬性設定為 true 會導致在開啟寫入器時刪除具有相同名稱的現有檔案。