FlatFileItemReader
平面檔案是任何最多包含二維(表格)資料的檔案類型。在 Spring Batch 框架中讀取平面檔案,是由名為 FlatFileItemReader
的類別所促進,該類別提供讀取和解析平面檔案的基本功能。FlatFileItemReader
兩個最重要的必要依賴項是 Resource
和 LineMapper
。LineMapper
介面將在後續章節中更深入探討。resource 屬性代表 Spring Core Resource
。關於如何建立此類型 Bean 的文件,可以在 Spring Framework,第 5 章,資源 中找到。因此,本指南不會深入探討建立 Resource
物件的細節,僅展示以下簡單範例
Resource resource = new FileSystemResource("resources/trades.csv");
在複雜的批次環境中,目錄結構通常由企業應用程式整合 (EAI) 基礎架構管理,其中為外部介面建立放置區,用於將檔案從 FTP 位置移動到批次處理位置,反之亦然。檔案移動工具超出 Spring Batch 架構的範圍,但批次 Job 流程中包含檔案移動工具作為 Job 流程中的步驟並不罕見。批次架構只需要知道如何找到要處理的檔案即可。Spring Batch 從這個起點開始將資料饋送到管道的過程。然而,Spring Integration 提供了許多此類型的服務。
FlatFileItemReader
中的其他屬性可讓您進一步指定如何解譯您的資料,如下表所述
屬性 | 類型 | 描述 |
---|---|---|
comments |
String[] |
指定指示註解列的行前綴。 |
encoding |
String |
指定要使用的文字編碼。預設值為 |
lineMapper |
|
將 |
linesToSkip |
int |
檔案頂端要忽略的行數。 |
recordSeparatorPolicy |
RecordSeparatorPolicy |
用於判斷行尾位置,並執行諸如在引號字串內跨越多行結尾之類的操作。 |
resource |
|
要從中讀取的資源。 |
skippedLinesCallback |
LineCallbackHandler |
介面,將檔案中要跳過的行的原始行內容傳遞給它。如果 |
strict |
boolean |
在嚴格模式下,如果輸入資源不存在,讀取器會在 |
LineMapper
如同 RowMapper
接收諸如 ResultSet
之類的低階結構並傳回 Object
一樣,平面檔案處理也需要相同的結構來將 String
行轉換為 Object
,如下列介面定義所示
public interface LineMapper<T> {
T mapLine(String line, int lineNumber) throws Exception;
}
基本合約是,給定目前行及其關聯的行號,mapper 應傳回產生的領域物件。這與 RowMapper
類似,因為每一行都與其行號關聯,就像 ResultSet
中的每一列都與其列號綁定一樣。這允許將行號與產生的領域物件關聯,以進行身分比較或提供更豐富的日誌資訊。然而,與 RowMapper
不同,LineMapper
獲得的是原始行,如上所述,這只完成了一半的工作。該行必須被 Token 化為 FieldSet
,然後才能映射到物件,如本文檔稍後所述。
LineTokenizer
將輸入行轉換為 FieldSet
的抽象是必要的,因為可能有許多平面檔案資料格式需要轉換為 FieldSet
。在 Spring Batch 中,此介面是 LineTokenizer
public interface LineTokenizer {
FieldSet tokenize(String line);
}
LineTokenizer
的合約是,給定輸入行(理論上 String
可能包含多行),會傳回代表該行的 FieldSet
。然後,此 FieldSet
可以傳遞給 FieldSetMapper
。Spring Batch 包含以下 LineTokenizer
實作
-
DelimitedLineTokenizer
:用於記錄中欄位由分隔符號分隔的檔案。最常見的分隔符號是逗號,但也經常使用管道符號或分號。 -
FixedLengthTokenizer
:用於記錄中欄位均為「固定寬度」的檔案。每個記錄類型都必須定義每個欄位的寬度。 -
PatternMatchingCompositeLineTokenizer
:透過檢查模式,判斷應在特定行上使用哪個LineTokenizer
(從 Tokenizer 清單中)。
FieldSetMapper
FieldSetMapper
介面定義了一個單一方法 mapFieldSet
,它接收一個 FieldSet
物件,並將其內容映射到一個物件。此物件可以是自訂 DTO、領域物件或陣列,具體取決於 Job 的需求。FieldSetMapper
與 LineTokenizer
結合使用,以將資源中的資料行轉換為所需類型的物件,如下列介面定義所示
public interface FieldSetMapper<T> {
T mapFieldSet(FieldSet fieldSet) throws BindException;
}
使用的模式與 JdbcTemplate
使用的 RowMapper
相同。
DefaultLineMapper
既然已定義了讀取平面檔案的基本介面,那麼很明顯,需要三個基本步驟
-
從檔案讀取一行。
-
將
String
行傳遞到LineTokenizer#tokenize()
方法以檢索FieldSet
。 -
將從 Token 化傳回的
FieldSet
傳遞到FieldSetMapper
,從ItemReader#read()
方法傳回結果。
上述兩個介面代表兩個獨立的任務:將行轉換為 FieldSet
,以及將 FieldSet
映射到領域物件。由於 LineTokenizer
的輸入與 LineMapper
的輸入(一行)匹配,且 FieldSetMapper
的輸出與 LineMapper
的輸出匹配,因此提供了一個同時使用 LineTokenizer
和 FieldSetMapper
的預設實作。DefaultLineMapper
(如下列類別定義所示)代表大多數使用者需要的行為
public class DefaultLineMapper<T> implements LineMapper<>, InitializingBean {
private LineTokenizer tokenizer;
private FieldSetMapper<T> fieldSetMapper;
public T mapLine(String line, int lineNumber) throws Exception {
return fieldSetMapper.mapFieldSet(tokenizer.tokenize(line));
}
public void setLineTokenizer(LineTokenizer tokenizer) {
this.tokenizer = tokenizer;
}
public void setFieldSetMapper(FieldSetMapper<T> fieldSetMapper) {
this.fieldSetMapper = fieldSetMapper;
}
}
上述功能是在預設實作中提供的,而不是內建到讀取器本身(如同框架的先前版本中所做的那樣),以便讓使用者在控制解析過程方面具有更大的彈性,尤其是在需要存取原始行時。
簡單分隔檔案讀取範例
以下範例說明如何使用實際的領域情境讀取平面檔案。此特定批次 Job 從以下檔案讀取足球員
ID,lastName,firstName,position,birthYear,debutYear "AbduKa00,Abdul-Jabbar,Karim,rb,1974,1996", "AbduRa00,Abdullah,Rabih,rb,1975,1999", "AberWa00,Abercrombie,Walter,rb,1959,1982", "AbraDa00,Abramowicz,Danny,wr,1945,1967", "AdamBo00,Adams,Bob,te,1946,1969", "AdamCh00,Adams,Charlie,wr,1979,2003"
此檔案的內容映射到以下 Player
領域物件
public class Player implements Serializable {
private String ID;
private String lastName;
private String firstName;
private String position;
private int birthYear;
private int debutYear;
public String toString() {
return "PLAYER:ID=" + ID + ",Last Name=" + lastName +
",First Name=" + firstName + ",Position=" + position +
",Birth Year=" + birthYear + ",DebutYear=" +
debutYear;
}
// setters and getters...
}
為了將 FieldSet
映射到 Player
物件,需要定義一個傳回球員的 FieldSetMapper
,如下列範例所示
protected static class PlayerFieldSetMapper implements FieldSetMapper<Player> {
public Player mapFieldSet(FieldSet fieldSet) {
Player player = new Player();
player.setID(fieldSet.readString(0));
player.setLastName(fieldSet.readString(1));
player.setFirstName(fieldSet.readString(2));
player.setPosition(fieldSet.readString(3));
player.setBirthYear(fieldSet.readInt(4));
player.setDebutYear(fieldSet.readInt(5));
return player;
}
}
然後可以透過正確建構 FlatFileItemReader
並呼叫 read
來讀取檔案,如下列範例所示
FlatFileItemReader<Player> itemReader = new FlatFileItemReader<>();
itemReader.setResource(new FileSystemResource("resources/players.csv"));
DefaultLineMapper<Player> lineMapper = new DefaultLineMapper<>();
//DelimitedLineTokenizer defaults to comma as its delimiter
lineMapper.setLineTokenizer(new DelimitedLineTokenizer());
lineMapper.setFieldSetMapper(new PlayerFieldSetMapper());
itemReader.setLineMapper(lineMapper);
itemReader.open(new ExecutionContext());
Player player = itemReader.read();
每次呼叫 read
都會從檔案中的每一行傳回一個新的 Player
物件。當到達檔案末尾時,會傳回 null
。
依名稱映射欄位
DelimitedLineTokenizer
和 FixedLengthTokenizer
都允許一項額外功能,該功能在功能上與 JDBC ResultSet
類似。欄位的名稱可以注入到這些 LineTokenizer
實作中的任何一個,以提高映射函數的可讀性。首先,平面檔案中所有欄的欄名都會注入到 Tokenizer 中,如下列範例所示
tokenizer.setNames(new String[] {"ID", "lastName", "firstName", "position", "birthYear", "debutYear"});
FieldSetMapper
可以按如下方式使用此資訊
public class PlayerMapper implements FieldSetMapper<Player> {
public Player mapFieldSet(FieldSet fs) {
if (fs == null) {
return null;
}
Player player = new Player();
player.setID(fs.readString("ID"));
player.setLastName(fs.readString("lastName"));
player.setFirstName(fs.readString("firstName"));
player.setPosition(fs.readString("position"));
player.setDebutYear(fs.readInt("debutYear"));
player.setBirthYear(fs.readInt("birthYear"));
return player;
}
}
自動將 FieldSets 映射到領域物件
對於許多人來說,撰寫特定的 FieldSetMapper
與為 JdbcTemplate
撰寫特定的 RowMapper
一樣繁瑣。Spring Batch 透過提供 FieldSetMapper
來簡化此操作,該 FieldSetMapper
會自動映射欄位,方法是使用 JavaBean 規範將欄位名稱與物件上的 Setter 進行比對。
-
Java
-
XML
再次使用足球範例,BeanWrapperFieldSetMapper
設定在 Java 中看起來像以下程式碼片段
@Bean
public FieldSetMapper fieldSetMapper() {
BeanWrapperFieldSetMapper fieldSetMapper = new BeanWrapperFieldSetMapper();
fieldSetMapper.setPrototypeBeanName("player");
return fieldSetMapper;
}
@Bean
@Scope("prototype")
public Player player() {
return new Player();
}
再次使用足球範例,BeanWrapperFieldSetMapper
設定在 XML 中看起來像以下程式碼片段
<bean id="fieldSetMapper"
class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
<property name="prototypeBeanName" value="player" />
</bean>
<bean id="player"
class="org.springframework.batch.samples.domain.Player"
scope="prototype" />
對於 FieldSet
中的每個條目,mapper 會在新建立的 Player
物件 (因此需要 Prototype 範圍) 上尋找對應的 Setter,方式與 Spring 容器尋找與屬性名稱匹配的 Setter 相同。FieldSet
中的每個可用欄位都會被映射,並傳回產生的 Player
物件,而無需任何程式碼。
固定長度檔案格式
到目前為止,僅詳細討論了分隔檔案。然而,它們僅代表檔案讀取圖景的一半。許多使用平面檔案的組織都使用固定長度格式。以下是一個固定長度檔案範例
UK21341EAH4121131.11customer1 UK21341EAH4221232.11customer2 UK21341EAH4321333.11customer3 UK21341EAH4421434.11customer4 UK21341EAH4521535.11customer5
雖然這看起來像一個大型欄位,但它實際上代表 4 個不同的欄位
-
ISIN:訂購 Item 的唯一識別碼 - 長度為 12 個字元。
-
Quantity:訂購 Item 的數量 - 長度為 3 個字元。
-
Price:Item 的價格 - 長度為 5 個字元。
-
Customer:訂購 Item 的客戶 ID - 長度為 9 個字元。
在設定 FixedLengthLineTokenizer
時,必須以範圍的形式提供每個長度。
-
Java
-
XML
以下範例顯示如何在 Java 中為 FixedLengthLineTokenizer
定義範圍
@Bean
public FixedLengthTokenizer fixedLengthTokenizer() {
FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();
tokenizer.setNames("ISIN", "Quantity", "Price", "Customer");
tokenizer.setColumns(new Range(1, 12),
new Range(13, 15),
new Range(16, 20),
new Range(21, 29));
return tokenizer;
}
以下範例顯示如何在 XML 中為 FixedLengthLineTokenizer
定義範圍
<bean id="fixedLengthLineTokenizer"
class="org.springframework.batch.item.file.transform.FixedLengthTokenizer">
<property name="names" value="ISIN,Quantity,Price,Customer" />
<property name="columns" value="1-12, 13-15, 16-20, 21-29" />
</bean>
由於 FixedLengthLineTokenizer
使用與先前討論的相同的 LineTokenizer
介面,因此它傳回的 FieldSet
與使用分隔符號時相同。這允許使用相同的方法來處理其輸出,例如使用 BeanWrapperFieldSetMapper
。
支援上述範圍語法需要 Specialized Property Editor |
由於 FixedLengthLineTokenizer
使用與上述討論的相同的 LineTokenizer
介面,因此它傳回的 FieldSet
與使用分隔符號時相同。這讓可以使用相同的方法來處理其輸出,例如使用 BeanWrapperFieldSetMapper
。
單一檔案中的多種記錄類型
到目前為止,所有檔案讀取範例都為了簡單起見,做出了一個關鍵假設:檔案中的所有記錄都具有相同的格式。然而,情況可能並非總是如此。檔案可能具有不同格式的記錄,需要以不同的方式進行 Token 化並映射到不同的物件,這非常常見。以下檔案摘錄說明了這一點
USER;Smith;Peter;;T;20014539;F LINEA;1044391041ABC037.49G201XX1383.12H LINEB;2134776319DEF422.99M005LI
在此檔案中,我們有三種類型的記錄:「USER」、「LINEA」和「LINEB」。「USER」行對應於 User
物件。「LINEA」和「LINEB」都對應於 Line
物件,儘管「LINEA」比「LINEB」具有更多資訊。
ItemReader
個別讀取每一行,但我們必須指定不同的 LineTokenizer
和 FieldSetMapper
物件,以便 ItemWriter
接收正確的 Item。PatternMatchingCompositeLineMapper
透過允許將模式映射到 LineTokenizers
和模式映射到 FieldSetMappers
來簡化此操作。
-
Java
-
XML
@Bean
public PatternMatchingCompositeLineMapper orderFileLineMapper() {
PatternMatchingCompositeLineMapper lineMapper =
new PatternMatchingCompositeLineMapper();
Map<String, LineTokenizer> tokenizers = new HashMap<>(3);
tokenizers.put("USER*", userTokenizer());
tokenizers.put("LINEA*", lineATokenizer());
tokenizers.put("LINEB*", lineBTokenizer());
lineMapper.setTokenizers(tokenizers);
Map<String, FieldSetMapper> mappers = new HashMap<>(2);
mappers.put("USER*", userFieldSetMapper());
mappers.put("LINE*", lineFieldSetMapper());
lineMapper.setFieldSetMappers(mappers);
return lineMapper;
}
以下範例顯示如何在 XML 中為 FixedLengthLineTokenizer
定義範圍
<bean id="orderFileLineMapper"
class="org.spr...PatternMatchingCompositeLineMapper">
<property name="tokenizers">
<map>
<entry key="USER*" value-ref="userTokenizer" />
<entry key="LINEA*" value-ref="lineATokenizer" />
<entry key="LINEB*" value-ref="lineBTokenizer" />
</map>
</property>
<property name="fieldSetMappers">
<map>
<entry key="USER*" value-ref="userFieldSetMapper" />
<entry key="LINE*" value-ref="lineFieldSetMapper" />
</map>
</property>
</bean>
在此範例中,「LINEA」和「LINEB」具有單獨的 LineTokenizer
實例,但它們都使用相同的 FieldSetMapper
。
PatternMatchingCompositeLineMapper
使用 PatternMatcher#match
方法,以便為每一行選擇正確的委派。PatternMatcher
允許使用兩個具有特殊含義的萬用字元:問號 (「?」) 完全匹配一個字元,而星號 (「*」) 匹配零個或多個字元。請注意,在先前的設定中,所有模式都以星號結尾,使其有效地成為行的前綴。無論設定中的順序如何,PatternMatcher
始終匹配最特定的模式。因此,如果「LINE*」和「LINEA*」都列為模式,「LINEA」將匹配模式「LINEA*」,而「LINEB」將匹配模式「LINE*」。此外,單個星號 (「*」) 可以作為預設值,方法是匹配任何未被任何其他模式匹配的行。
-
Java
-
XML
以下範例顯示如何在 Java 中匹配任何未被任何其他模式匹配的行
...
tokenizers.put("*", defaultLineTokenizer());
...
以下範例顯示如何在 XML 中匹配任何未被任何其他模式匹配的行
<entry key="*" value-ref="defaultLineTokenizer" />
還有一個 PatternMatchingCompositeLineTokenizer
可以單獨用於 Token 化。
平面檔案也常見於包含跨越多行的記錄。為了處理這種情況,需要更複雜的策略。可以在 multiLineRecords
範例中找到此常見模式的示範。
平面檔案中的例外處理
在 Token 化行時,可能會擲回例外的情況有很多。許多平面檔案並不完美,並且包含格式不正確的記錄。許多使用者選擇跳過這些錯誤行,同時記錄問題、原始行和行號。這些日誌稍後可以手動或由另一個批次 Job 檢查。因此,Spring Batch 提供了一個例外層級結構,用於處理解析例外:FlatFileParseException
和 FlatFileFormatException
。當嘗試讀取檔案時遇到任何錯誤時,FlatFileItemReader
會擲回 FlatFileParseException
。FlatFileFormatException
由 LineTokenizer
介面的實作擲回,並指示在 Token 化時遇到的更具體的錯誤。
IncorrectTokenCountException
DelimitedLineTokenizer
和 FixedLengthLineTokenizer
都有能力指定可用於建立 FieldSet
的欄名。但是,如果欄名數量與 Token 化行時找到的欄數不符,則無法建立 FieldSet
,並且會擲回 IncorrectTokenCountException
,其中包含遇到的 Token 數量和預期的數量,如下列範例所示
tokenizer.setNames(new String[] {"A", "B", "C", "D"});
try {
tokenizer.tokenize("a,b,c");
}
catch (IncorrectTokenCountException e) {
assertEquals(4, e.getExpectedCount());
assertEquals(3, e.getActualCount());
}
由於 Tokenizer 設定了 4 個欄名,但在檔案中僅找到 3 個 Token,因此擲回了 IncorrectTokenCountException
。
IncorrectLineLengthException
以固定長度格式格式化的檔案在解析時有額外的要求,因為與分隔格式不同,每個欄都必須嚴格遵守其預定義的寬度。如果總行長度不等於此欄的最寬值,則會擲回例外,如下列範例所示
tokenizer.setColumns(new Range[] { new Range(1, 5),
new Range(6, 10),
new Range(11, 15) });
try {
tokenizer.tokenize("12345");
fail("Expected IncorrectLineLengthException");
}
catch (IncorrectLineLengthException ex) {
assertEquals(15, ex.getExpectedLength());
assertEquals(5, ex.getActualLength());
}
上面 Tokenizer 設定的範圍是:1-5、6-10 和 11-15。因此,行的總長度為 15。但是,在前面的範例中,傳入了長度為 5 的行,導致擲回 IncorrectLineLengthException
。在此處擲回例外,而不是僅映射第一個欄,可以讓行的處理更早失敗,並且包含比在嘗試讀取 FieldSetMapper
中的欄 2 時失敗時更多的資訊。但是,在某些情況下,行的長度並非始終恆定。因此,可以透過 'strict' 屬性關閉行長度的驗證,如下列範例所示
tokenizer.setColumns(new Range[] { new Range(1, 5), new Range(6, 10) });
tokenizer.setStrict(false);
FieldSet tokens = tokenizer.tokenize("12345");
assertEquals("12345", tokens.readString(0));
assertEquals("", tokens.readString(1));
前面的範例與之前的範例幾乎完全相同,只是呼叫了 tokenizer.setStrict(false)
。此設定告知 Tokenizer 在 Token 化行時不要強制執行行長度。現在已正確建立並傳回 FieldSet
。但是,它僅包含剩餘值的空 Token。