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
の他のプロパティでは、次の表で説明するように、データの解釈方法をさらに指定できます。
プロパティ | タイプ | 説明 |
---|---|---|
comments | String[] | コメント行を示す行プレフィックスを指定します。 |
encoding | String | 使用するテキストエンコーディングを指定します。デフォルト値は |
lineMapper |
|
|
linesToSkip | int | ファイルの先頭で無視する行数。 |
recordSeparatorPolicy | RecordSeparatorPolicy | 行末がどこにあるかを判断し、引用符で囲まれた文字列内にある場合、行末を越えて続行するなどの操作を行うために使用されます。 |
resource |
| 読み取り元のリソース。 |
skippedLinesCallback | LineCallbackHandler | スキップするファイルの行の生の行コンテンツを渡すインターフェース。 |
strict | boolean | 厳格モードでは、入力リソースが存在しない場合、リーダーは |
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 行読み取ります。
String
行をLineTokenizer#tokenize()
メソッドに渡して、FieldSet
を取得します。トークン化から返された
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 の次のスニペットのようになります。
@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
の各エントリについて、マッパーは、Spring コンテナーがプロパティ名に一致する setter を検索するのと同じ方法で、Player
オブジェクトの新しいインスタンスで対応する setter を検索します(このため、プロトタイプスコープが必要です)。FieldSet
で使用可能な各フィールドがマップされ、結果の Player
オブジェクトが返されます。コードは不要です。
固定長ファイル形式
これまでのところ、区切りファイルのみが詳細に議論されてきました。ただし、それらはファイル読み取りイメージの半分のみを表します。フラットファイルを使用する多くの組織は、固定長形式を使用しています。固定長ファイルの例は次のとおりです。
UK21341EAH4121131.11customer1 UK21341EAH4221232.11customer2 UK21341EAH4321333.11customer3 UK21341EAH4421434.11customer4 UK21341EAH4521535.11customer5
これは 1 つの大きなフィールドのように見えますが、実際には 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
このファイルには、3 つの型のレコード、"USER"、"LINEA"、"LINEB" があります。"USER" 行は、User
オブジェクトに対応します。"LINEA" と "LINEB" は両方とも Line
オブジェクトに対応していますが、"LINEA" は "LINEB" よりも多くの情報を持っています。
ItemReader
は各行を個別に読み取りますが、ItemWriter
が正しい項目を受け取るように、異なる LineTokenizer
オブジェクトと FieldSetMapper
オブジェクトを指定する必要があります。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
では、特別な意味を持つ 2 つのワイルドカード文字を使用できます。疑問符(" ? ")は正確に 1 文字に一致し、アスタリスク("*" )は 0 個以上の文字に一致します。上記の構成では、すべてのパターンがアスタリスクで終わり、効果的に行のプレフィックスになることに注意してください。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
を提供します。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
が正しく作成され、返されるようになりました。ただし、残りの値には空のトークンのみが含まれます。