FlatFileItemReader

平面檔案是包含最多二維(表格)資料的任何型別的檔案。在 Spring Batch 框架中讀取平面檔案由 FlatFileItemReader 類提供便利,該類提供讀取和解析平面檔案的基本功能。FlatFileItemReader 的兩個最重要的必需依賴項是 ResourceLineMapperLineMapper 介面將在下一節中進行更多探討。資源屬性表示 Spring Core Resource。有關如何建立此型別 bean 的文件可以在 Spring Framework,第 5 章。資源中找到。因此,本指南不會深入探討建立 Resource 物件的細節,除了展示以下簡單示例

Resource resource = new FileSystemResource("resources/trades.csv");

在複雜的批處理環境中,目錄結構通常由企業應用整合 (EAI) 基礎設施管理,其中為外部介面設定了接收區,用於將檔案從 FTP 位置移動到批處理位置,反之亦然。檔案移動實用程式超出了 Spring Batch 架構的範圍,但批處理作業流將檔案移動實用程式作為作業流中的步驟並不少見。批處理架構只需要知道如何定位要處理的檔案。Spring Batch 從這個起點開始將資料饋送到管道中。然而,Spring Integration 提供了許多此類服務。

FlatFileItemReader 中的其他屬性允許您進一步指定如何解釋資料,如下表所示

表 1. FlatFileItemReader 屬性
財產 型別 描述

comments

String[]

指定指示註釋行的行字首。

encoding

字串

指定要使用的文字編碼。預設值為 UTF-8

lineMapper

LineMapper

String 轉換為表示項的 Object

linesToSkip

int

檔案中頂部要忽略的行數。

recordSeparatorPolicy

RecordSeparatorPolicy

用於確定行尾在哪裡,並執行諸如在帶引號的字串中跨行尾繼續之類的操作。

resource

資源

要讀取的資源。

skippedLinesCallback

LineCallbackHandler

一個介面,將檔案中要跳過的行的原始行內容傳遞給它。如果 linesToSkip 設定為 2,則此介面將被呼叫兩次。

strict

布林值

在嚴格模式下,如果輸入資源不存在,讀取器會在 ExecutionContext 上丟擲異常。否則,它會記錄問題並繼續。

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、域物件或陣列,具體取決於作業的需求。FieldSetMapperLineTokenizer 結合使用,將資源中的一行資料轉換為所需型別的物件,如以下介面定義所示

public interface FieldSetMapper<T> {

    T mapFieldSet(FieldSet fieldSet) throws BindException;

}

使用的模式與 JdbcTemplate 使用的 RowMapper 相同。

DefaultLineMapper

現在已經定義了讀取平面檔案的基本介面,很明顯需要三個基本步驟

  1. 從檔案中讀取一行。

  2. String 行傳遞給 LineTokenizer#tokenize() 方法以檢索 FieldSet

  3. 將從標記化返回的 FieldSet 傳遞給 FieldSetMapper,從 ItemReader#read() 方法返回結果。

上面描述的兩個介面代表兩個獨立的任務:將行轉換為 FieldSet 和將 FieldSet 對映到域物件。由於 LineTokenizer 的輸入與 LineMapper 的輸入(一行)匹配,並且 FieldSetMapper 的輸出與 LineMapper 的輸出匹配,因此提供了一個使用 LineTokenizerFieldSetMapper 的預設實現。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

按名稱對映欄位

DelimitedLineTokenizerFixedLengthTokenizer 都允許一個附加功能,其功能類似於 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 中如下所示

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 中如下所示

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 個不同的欄位

  1. ISIN:被訂購商品的唯一識別符號 - 12 個字元長。

  2. Quantity:被訂購商品的數量 - 3 個字元長。

  3. Price:商品價格 - 5 個字元長。

  4. Customer:訂購商品的客戶 ID - 9 個字元長。

在配置 FixedLengthLineTokenizer 時,必須以範圍的形式提供這些長度。

  • Java

  • XML

以下示例演示瞭如何在 Java 中為 FixedLengthLineTokenizer 定義範圍

Java 配置
@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 定義範圍

XML 配置
<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

支援上述範圍語法要求在 ApplicationContext 中配置一個專門的屬性編輯器 RangeArrayPropertyEditor。但是,當使用批處理名稱空間時,此 bean 會自動在 ApplicationContext 中宣告。

因為 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 單獨讀取每一行,但我們必須指定不同的 LineTokenizerFieldSetMapper 物件,以便 ItemWriter 接收正確的項。PatternMatchingCompositeLineMapper 透過允許配置模式到 LineTokenizers 的對映以及模式到 FieldSetMappers 的對映來簡化這一點。

  • Java

  • XML

Java 配置
@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 定義範圍

XML 配置
<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 中匹配未被任何其他模式匹配的行

Java 配置
...
tokenizers.put("*", defaultLineTokenizer());
...

以下示例演示瞭如何在 XML 中匹配未被任何其他模式匹配的行

XML 配置
<entry key="*" value-ref="defaultLineTokenizer" />

還有一個 PatternMatchingCompositeLineTokenizer,可以單獨用於標記化。

平面檔案也常常包含跨越多行的記錄。要處理這種情況,需要更復雜的策略。此常見模式的演示可以在 multiLineRecords 示例中找到。

平面檔案中的異常處理

在對行進行標記化時,許多情況下可能會丟擲異常。許多平面檔案不完善,包含格式不正確的記錄。許多使用者選擇跳過這些錯誤行,同時記錄問題、原始行和行號。這些日誌稍後可以手動檢查或由另一個批處理作業檢查。為此,Spring Batch 提供了一個用於處理解析異常的異常層次結構:FlatFileParseExceptionFlatFileFormatException。當嘗試讀取檔案時遇到任何錯誤時,FlatFileItemReader 會丟擲 FlatFileParseExceptionLineTokenizer 介面的實現會丟擲 FlatFileFormatException,表示在標記化時遇到的更具體的錯誤。

IncorrectTokenCountException

DelimitedLineTokenizerFixedLengthTokenizer 都能夠指定可用於建立 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。但是,它僅包含其餘值的空標記。

© . This site is unofficial and not affiliated with VMware.