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 流中的 Step 並不罕見。批處理架構只需知道如何找到待處理的檔案。Spring Batch 從這個起點開始將資料輸入管道。然而,Spring Integration 提供了許多此類服務。
FlatFileItemReader
中的其他屬性允許您進一步指定資料如何解釋,如下表所示
屬性 | 型別 | 描述 |
---|---|---|
comments |
String[] |
指定指示註釋行的行字首。 |
encoding |
String |
指定使用的文字編碼。預設值為 |
lineMapper |
|
將 |
linesToSkip |
int |
要忽略檔案頂部行數。 |
recordSeparatorPolicy |
RecordSeparatorPolicy |
用於確定行尾位置,並在引號字串內時執行跨行繼續等操作。 |
resource |
|
要讀取的資源。 |
skippedLinesCallback |
LineCallbackHandler |
將待跳過行的原始行內容傳遞給該介面。如果 |
strict |
boolean |
在嚴格模式下,如果輸入資源不存在,reader 會在 |
LineMapper
與 RowMapper
類似,RowMapper
接收底層構造(如 ResultSet
)並返回一個 Object
,平面檔案處理也需要相同的構造來將 String
行轉換為 Object
,如下介面定義所示
public interface LineMapper<T> {
T mapLine(String line, int lineNumber) throws Exception;
}
基本契約是,給定當前行及其關聯的行號,mapper 應返回一個結果域物件。這類似於 RowMapper
,每行都與其行號相關聯,就像 ResultSet
中的每行都與其行號相關聯一樣。這允許將行號與結果域物件相關聯,用於身份比較或更詳細的日誌記錄。然而,與 RowMapper
不同,LineMapper
接收原始行,正如上面討論的,這隻完成了一半的工作。該行必須被分詞(tokenize)成一個 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
應該用於特定行。
FieldSetMapper
FieldSetMapper
介面定義了一個方法 mapFieldSet
,該方法接收一個 FieldSet
物件,並將其內容對映到物件。根據 Job 的需求,該物件可以是自定義 DTO、域物件或陣列。FieldSetMapper
與 LineTokenizer
結合使用,將資源中的資料行轉換為所需型別的物件,如下介面定義所示
public interface FieldSetMapper<T> {
T mapFieldSet(FieldSet fieldSet) throws BindException;
}
使用的模式與 JdbcTemplate
使用的 RowMapper
相同。
DefaultLineMapper
既然已經定義了讀取平面檔案的基本介面,那麼顯然需要三個基本步驟
-
從檔案中讀取一行。
-
將
String
行傳遞給LineTokenizer#tokenize()
方法以檢索FieldSet
。 -
將 tokenizing 返回的
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;
}
}
上述功能在預設實現中提供,而不是內建在 reader 本身中(如框架早期版本中所做),以允許使用者在控制解析過程時具有更大的靈活性,特別是如果需要訪問原始行時。
簡單的分隔檔案讀取示例
以下示例說明了如何在實際域場景中讀取平面檔案。這個特定的批處理 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
物件,需要定義一個返回 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;
}
}
將 FieldSet 自動對映到域物件
對於許多人來說,編寫特定的 FieldSetMapper
與為 JdbcTemplate
編寫特定的 RowMapper
一樣麻煩。Spring Batch 透過提供一個 FieldSetMapper
使這變得更容易,該 mapper 使用 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
物件例項上查詢相應的 setter(因此需要原型範圍),就像 Spring 容器查詢匹配屬性名的 setter 一樣。FieldSet
中每個可用的欄位都被對映,然後返回結果 Player
物件,無需編寫程式碼。
固定長度檔案格式
到目前為止,只詳細討論了分隔檔案。然而,它們只代表檔案讀取圖景的一半。許多使用平面檔案的組織使用固定長度格式。以下是一個固定長度檔案示例
UK21341EAH4121131.11customer1 UK21341EAH4221232.11customer2 UK21341EAH4321333.11customer3 UK21341EAH4421434.11customer4 UK21341EAH4521535.11customer5
雖然這看起來像一個大欄位,但它實際上代表 4 個不同的欄位
-
ISIN:訂購商品的唯一識別符號 - 長度為 12 個字元。
-
數量:訂購商品的數量 - 長度為 3 個字元。
-
價格:商品價格 - 長度為 5 個字元。
-
客戶:訂購商品的客戶 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
。
為了支援前述的範圍語法,需要在 |
由於 FixedLengthLineTokenizer
使用與上面討論相同的 LineTokenizer
介面,它返回與使用分隔符時相同的 FieldSet
。這允許使用相同的方法來處理其輸出,例如使用 BeanWrapperFieldSetMapper
。
單個檔案中的多種記錄型別
到目前為止,所有檔案讀取示例為了簡單起見都做了一個關鍵假設:檔案中的所有記錄具有相同的格式。然而,情況並非總是如此。檔案可能包含具有不同格式、需要以不同方式分詞並對映到不同物件的記錄,這非常常見。以下檔案摘錄說明了這一點
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
透過允許配置模式到 LineTokenizer
的對映和模式到 FieldSetMapper
的對映,簡化了這一過程。
-
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
可用於僅進行分詞。
平面檔案還經常包含跨越多行的記錄。要處理這種情況,需要更復雜的策略。在 multiLineRecords
示例中可以找到這種常見模式的演示。
平面檔案中的異常處理
分詞一行時,可能會丟擲異常的場景很多。許多平面檔案不完美,包含格式錯誤的記錄。許多使用者選擇跳過這些錯誤行,同時記錄問題、原始行和行號。這些日誌稍後可以手動檢查或由另一個批處理 Job 處理。為此,Spring Batch 提供了一個用於處理解析異常的異常層次結構:FlatFileParseException
和 FlatFileFormatException
。當嘗試讀取檔案時遇到任何錯誤時,FlatFileItemReader
會丟擲 FlatFileParseException
。LineTokenizer
介面的實現會丟擲 FlatFileFormatException
,表示在分詞時遇到了更具體的錯誤。
IncorrectTokenCountException
DelimitedLineTokenizer
和 FixedLengthTokenizer
都具有指定列名以用於建立 FieldSet
的能力。但是,如果在分詞行時找到的列數與列名數不匹配,則無法建立 FieldSet
,並會丟擲 IncorrectTokenCountException
,其中包含遇到的 token 數和期望的 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 在分詞行時不要強制執行行長度。現在已正確建立並返回 FieldSet
。但是,它包含剩餘值的空 token。