FlatFileItemReader

フラットファイルは、最大 2 次元(表)データを含む任意の型のファイルです。Spring Batch フレームワークでのフラットファイルの読み取りは、FlatFileItemReader と呼ばれるクラスによって促進されます。このクラスは、フラットファイルの読み取りと解析のための基本的な機能を提供します。FlatFileItemReader の 2 つの最も重要な必須依存関係は、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 の他のプロパティでは、次の表で説明するように、データの解釈方法をさらに指定できます。

表 1: FlatFileItemReader プロパティ
プロパティ タイプ 説明

comments

String[]

コメント行を示す行プレフィックスを指定します。

encoding

String

使用するテキストエンコーディングを指定します。デフォルト値は UTF-8 です。

lineMapper

LineMapper

String を、アイテムを表す Object に変換します。

linesToSkip

int

ファイルの先頭で無視する行数。

recordSeparatorPolicy

RecordSeparatorPolicy

行末がどこにあるかを判断し、引用符で囲まれた文字列内にある場合、行末を越えて続行するなどの操作を行うために使用されます。

resource

Resource

読み取り元のリソース。

skippedLinesCallback

LineCallbackHandler

スキップするファイルの行の生の行コンテンツを渡すインターフェース。linesToSkip が 2 に設定されている場合、このインターフェースは 2 回呼び出されます。

strict

boolean

厳格モードでは、入力リソースが存在しない場合、リーダーは ExecutionContext で例外をスローします。それ以外の場合は、問題をログに記録して続行します。

LineMapper

ResultSet などの低レベルの構造体を取り、Object を返す RowMapper と同様に、フラットファイル処理では、String 行を Object に変換するために同じ構造体が必要です。次のインターフェース定義に示します。

public interface LineMapper<T> {

    T mapLine(String line, int lineNumber) throws Exception;

}

基本的な契約は、現在の行とそれが関連付けられている行番号を指定すると、マッパーが結果のドメインオブジェクトを返すことです。これは、ResultSet の各行が行番号に結び付けられているように、各行がその行番号に関連付けられているという点で、RowMapper に似ています。これにより、結果のドメインオブジェクトに行番号を結び付けて、ID の比較やより詳細なログを記録できます。ただし、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: パターンと照合することにより、特定の行でトークナイザーのリストのどの LineTokenizer を使用するかを決定します。

FieldSetMapper

FieldSetMapper インターフェースは、FieldSet オブジェクトを取り、その内容をオブジェクトにマップする単一のメソッド mapFieldSet を定義します。このオブジェクトは、ジョブのニーズに応じて、カスタム DTO、ドメインオブジェクト、配列になります。FieldSetMapper は LineTokenizer と組み合わせて使用され、次のインターフェース定義に示すように、リソースからのデータ行を目的の型のオブジェクトに変換します。

public interface FieldSetMapper<T> {

    T mapFieldSet(FieldSet fieldSet) throws BindException;

}

使用されるパターンは、JdbcTemplate が使用する RowMapper と同じです。

DefaultLineMapper

フラットファイルを読み取るための基本的なインターフェースが定義されたため、次の 3 つの基本的な手順が必要であることが明らかになります。

  1. ファイルから 1 行読み取ります。

  2. String 行を LineTokenizer#tokenize() メソッドに渡して、FieldSet を取得します。

  3. トークン化から返された FieldSet を FieldSetMapper に渡し、ItemReader#read() メソッドから結果を返します。

上記の 2 つのインターフェースは、2 つの個別のタスクを表します。行を 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 に似ている追加機能が 1 つあります。フィールドの名前をこれらの 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;
   }
}

FieldSets をドメインオブジェクトに自動マッピングする

多くの人にとって、特定の FieldSetMapper を作成する必要があることは、JdbcTemplate に対して特定の RowMapper を作成することと同じくらい面倒です。Spring Batch は、JavaBean 仕様を使用して、フィールド名をオブジェクトの setter と照合することにより、フィールドを自動的にマップする FieldSetMapper を提供することにより、これを容易にします。

  • 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.item.file.mapping.BeanWrapperFieldSetMapper">
    <property name="prototypeBeanName" value="player" />
</bean>

<bean id="player"
      class="org.springframework.batch.samples.domain.Player"
      scope="prototype" />

FieldSet の各エントリについて、マッパーは、Spring コンテナーがプロパティ名に一致する setter を検索するのと同じ方法で、Player オブジェクトの新しいインスタンスで対応する setter を検索します(このため、プロトタイプスコープが必要です)。FieldSet で使用可能な各フィールドがマップされ、結果の Player オブジェクトが返されます。コードは不要です。

固定長ファイル形式

これまでのところ、区切りファイルのみが詳細に議論されてきました。ただし、それらはファイル読み取りイメージの半分のみを表します。フラットファイルを使用する多くの組織は、固定長形式を使用しています。固定長ファイルの例は次のとおりです。

UK21341EAH4121131.11customer1
UK21341EAH4221232.11customer2
UK21341EAH4321333.11customer3
UK21341EAH4421434.11customer4
UK21341EAH4521535.11customer5

これは 1 つの大きなフィールドのように見えますが、実際には 4 つの異なるフィールドを表します。

  1. ISIN: 並べ替えるアイテムの一意の識別子 -12 文字。

  2. 量: 並べ替えるアイテムの数 -3 文字。

  3. 価格: 項目の価格 -5 文字の長さ。

  4. お客様: アイテムを並べ替える顧客の 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.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 の使用など、出力の処理に同じアプローチを使用できます。

範囲の前述の構文をサポートするには、専用のプロパティエディター RangeArrayPropertyEditor を ApplicationContext で構成する必要があります。ただし、この Bean は、バッチ名前空間が使用される ApplicationContext で自動的に宣言されます。

FixedLengthLineTokenizer は上で説明したのと同じ LineTokenizer インターフェースを使用するため、区切り文字が使用された場合と同じ FieldSet を返します。これにより、BeanWrapperFieldSetMapper を使用するなど、出力の処理に同じアプローチを使用できます。

単一ファイル内の複数のレコード型

ここまでのすべてのファイル読み取りの例は、単純化のためにすべて重要な前提を立てています。ファイル内のすべてのレコードは同じ形式を持っています。ただし、これは常にそうであるとは限りません。ファイルには、異なる形式のレコードがあり、異なるトークン化と異なるオブジェクトへのマッピングが必要になることがよくあります。ファイルからの次の抜粋はこれを示しています。

USER;Smith;Peter;;T;20014539;F
LINEA;1044391041ABC037.49G201XX1383.12H
LINEB;2134776319DEF422.99M005LI

このファイルには、3 つの型のレコード、"USER"、"LINEA"、"LINEB" があります。"USER" 行は、User オブジェクトに対応します。"LINEA" と "LINEB" は両方とも Line オブジェクトに対応していますが、"LINEA" は "LINEB" よりも多くの情報を持っています。

ItemReader は各行を個別に読み取りますが、ItemWriter が正しい項目を受け取るように、異なる LineTokenizer オブジェクトと FieldSetMapper オブジェクトを指定する必要があります。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 では、特別な意味を持つ 2 つのワイルドカード文字を使用できます。疑問符(" ? ")は正確に 1 文字に一致し、アスタリスク("*" )は 0 個以上の文字に一致します。上記の構成では、すべてのパターンがアスタリスクで終わり、効果的に行のプレフィックスになることに注意してください。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 は、解析例外を処理するための例外の階層 FlatFileParseException および FlatFileFormatException を提供します。FlatFileParseException は、ファイルの読み取り中にエラーが発生した場合に FlatFileItemReader によってスローされます。FlatFileFormatException は、LineTokenizer インターフェースの実装によってスローされ、トークン化中に発生したより具体的なエラーを示します。

IncorrectTokenCountException

DelimitedLineTokenizer と FixedLengthLineTokenizer の両方には、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 が正しく作成され、返されるようになりました。ただし、残りの値には空のトークンのみが含まれます。