FlatFileItemReader
平面檔案是包含最多二維(表格)資料的任何型別的檔案。在 Spring Batch 框架中讀取平面檔案由 FlatFileItemReader 類提供便利,該類提供讀取和解析平面檔案的基本功能。FlatFileItemReader 的兩個最重要的必需依賴項是 Resource 和 LineMapper。LineMapper 介面將在下一節中進行更多探討。資源屬性表示 Spring Core Resource。有關如何建立此型別 bean 的文件可以在 Spring Framework,第 5 章。資源中找到。因此,本指南不會深入探討建立 Resource 物件的細節,除了展示以下簡單示例
Resource resource = new FileSystemResource("resources/trades.csv");
在複雜的批處理環境中,目錄結構通常由企業應用整合 (EAI) 基礎設施管理,其中為外部介面設定了接收區,用於將檔案從 FTP 位置移動到批處理位置,反之亦然。檔案移動實用程式超出了 Spring Batch 架構的範圍,但批處理作業流將檔案移動實用程式作為作業流中的步驟並不少見。批處理架構只需要知道如何定位要處理的檔案。Spring Batch 從這個起點開始將資料饋送到管道中。然而,Spring Integration 提供了許多此類服務。
FlatFileItemReader 中的其他屬性允許您進一步指定如何解釋資料,如下表所示
| 財產 | 型別 | 描述 |
|---|---|---|
comments |
String[] |
指定指示註釋行的行字首。 |
encoding |
字串 |
指定要使用的文字編碼。預設值為 |
lineMapper |
|
將 |
linesToSkip |
int |
檔案中頂部要忽略的行數。 |
recordSeparatorPolicy |
RecordSeparatorPolicy |
用於確定行尾在哪裡,並執行諸如在帶引號的字串中跨行尾繼續之類的操作。 |
resource |
|
要讀取的資源。 |
skippedLinesCallback |
LineCallbackHandler |
一個介面,將檔案中要跳過的行的原始行內容傳遞給它。如果 |
strict |
布林值 |
在嚴格模式下,如果輸入資源不存在,讀取器會在 |
LineMapper
與 RowMapper 類似,後者接受像 ResultSet 這樣的低階構造並返回一個 Object,平面檔案處理需要相同的構造將 String 行轉換為 Object,如以下介面定義所示
public interface LineMapper<T> {
T mapLine(String line, int lineNumber) throws Exception;
}
基本契約是,給定當前行及其關聯的行號,對映器應返回一個結果域物件。這與 RowMapper 類似,因為每行都與其行號關聯,就像 ResultSet 中的每行都與其行號關聯一樣。這允許行號與結果域物件相關聯,用於身份比較或提供更具資訊性的日誌記錄。然而,與 RowMapper 不同,LineMapper 獲得的是原始行,如上所述,這隻完成了一半。該行必須被標記化為 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:透過檢查模式來確定應該對特定行使用哪一個分詞器。
FieldSetMapper
FieldSetMapper 介面定義了一個方法 mapFieldSet,該方法接受一個 FieldSet 物件並將其內容對映到一個物件。此物件可以是自定義 DTO、域物件或陣列,具體取決於作業的需求。FieldSetMapper 與 LineTokenizer 結合使用,將資源中的一行資料轉換為所需型別的物件,如以下介面定義所示
public interface FieldSetMapper<T> {
T mapFieldSet(FieldSet fieldSet) throws BindException;
}
使用的模式與 JdbcTemplate 使用的 RowMapper 相同。
DefaultLineMapper
現在已經定義了讀取平面檔案的基本介面,很明顯需要三個基本步驟
-
從檔案中讀取一行。
-
將
String行傳遞給LineTokenizer#tokenize()方法以檢索FieldSet。 -
將從標記化返回的
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;
}
}
上述功能在預設實現中提供,而不是內建到讀取器本身(如框架的早期版本中所做),以允許使用者在控制解析過程方面具有更大的靈活性,特別是當需要訪問原始行時。
簡單分隔檔案讀取示例
以下示例說明了如何使用實際的領域場景讀取平面檔案。這個特定的批處理作業從以下檔案中讀取足球運動員
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.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 來簡化這一點,該 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.infrastructure.item.file.mapping.BeanWrapperFieldSetMapper">
<property name="prototypeBeanName" value="player" />
</bean>
<bean id="player"
class="org.springframework.batch.samples.domain.Player"
scope="prototype" />
對於 FieldSet 中的每個條目,對映器會在 Player 物件的新例項上查詢相應的 setter(因此需要原型作用域),其方式與 Spring 容器查詢與屬性名稱匹配的 setter 相同。FieldSet 中每個可用的欄位都被對映,並返回結果 Player 物件,無需任何程式碼。
固定長度檔案格式
到目前為止,只詳細討論了分隔檔案。然而,它們只代表檔案讀取圖景的一半。許多使用平面檔案的組織使用固定長度格式。以下是一個固定長度檔案的示例
UK21341EAH4121131.11customer1 UK21341EAH4221232.11customer2 UK21341EAH4321333.11customer3 UK21341EAH4421434.11customer4 UK21341EAH4521535.11customer5
雖然這看起來像一個大欄位,但它實際上代表 4 個不同的欄位
-
ISIN:被訂購商品的唯一識別符號 - 12 個字元長。
-
Quantity:被訂購商品的數量 - 3 個字元長。
-
Price:商品價格 - 5 個字元長。
-
Customer:訂購商品的客戶 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.infrastructure.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 接收正確的項。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,可以單獨用於標記化。
平面檔案也常常包含跨越多行的記錄。要處理這種情況,需要更復雜的策略。此常見模式的演示可以在 multiLineRecords 示例中找到。
平面檔案中的異常處理
在對行進行標記化時,許多情況下可能會丟擲異常。許多平面檔案不完善,包含格式不正確的記錄。許多使用者選擇跳過這些錯誤行,同時記錄問題、原始行和行號。這些日誌稍後可以手動檢查或由另一個批處理作業檢查。為此,Spring Batch 提供了一個用於處理解析異常的異常層次結構:FlatFileParseException 和 FlatFileFormatException。當嘗試讀取檔案時遇到任何錯誤時,FlatFileItemReader 會丟擲 FlatFileParseException。LineTokenizer 介面的實現會丟擲 FlatFileFormatException,表示在標記化時遇到的更具體的錯誤。
IncorrectTokenCountException
DelimitedLineTokenizer 和 FixedLengthTokenizer 都能夠指定可用於建立 FieldSet 的列名。但是,如果列名的數量與標記化行時找到的列數不匹配,則無法建立 FieldSet,並會丟擲 IncorrectTokenCountException,其中包含遇到的令牌數和預期令牌數,如以下示例所示
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());
}
由於分詞器配置了 4 個列名,但在檔案中只找到了 3 個標記,因此丟擲了 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());
}
上面分詞器配置的範圍是: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)。此設定告訴分詞器在對行進行分詞時不要強制執行行長度。現在已正確建立並返回 FieldSet。但是,它僅包含其餘值的空標記。