Macchinetta Batch Framework (2.x) Development Guideline
Macchinetta Batch Framework (2.x) Development Guideline - version 2.6.1.RELEASE, 2026-3-30
> INDEX

# Macchinetta Batch Framework (2.x) Development Guideline

NTT Corporation.
version 2.6.1.RELEASE, 2026-3-30

## 1. はじめに

### 1.1. 利用規約

#### 1.1.1. Macchinetta 利用規約

本ドキュメントを使用するにあたり、以下の規約に同意していただく必要があります。同意いただけない場合は、本ドキュメント及びその複製物の全てを直ちに消去又は破棄してください。

1. 本ドキュメントの著作権及びその他一切の権利は、日本電信電話株式会社（以下「NTT」とする）あるいはNTTに権利を許諾する第三者に帰属します。

2. 本ドキュメントの一部または全部を、自らが使用する目的において、複製、翻訳、翻案することができます。ただし本ページの規約全文、およびNTTの著作権表示を削除することはできません。

3. 本ドキュメントの一部または全部を、自らが使用する目的において改変したり、本ドキュメントを用いた二次的著作物を作成することができます。ただし、「参考文献：Macchinetta Batch Framework Development Guideline」あるいは同等の表現を、作成したドキュメント及びその複製物に記載するものとします。

4. 前2項によって作成したドキュメント及びその複製物を、無償の場合に限り、第三者へ提供することができます。

5. NTTの書面による承諾を得ることなく、本規約に定められる条件を超えて、本ドキュメント及びその複製物を使用したり、本規約上の権利の全部又は一部を第三者に譲渡したりすることはできません。

6. NTTは、本ドキュメントの内容の正確性、使用目的への適合性の保証、使用結果についての的確性や信頼性の保証、及び瑕疵担保義務も含め、直接、間接に被ったいかなる損害に対しても一切の責任を負いません。

7. NTTは、本ドキュメントが第三者の著作権、その他如何なる権利も侵害しないことを保証しません。また、著作権、その他の権利侵害を直接又は間接の原因としてなされる如何なる請求（第三者との間の紛争を理由になされる請求を含む。）に関しても、NTTは一切の責任を負いません。

本ドキュメントで使用されている各社の会社名及びサービス名、商品名に関する登録商標および商標は、以下の通りです。

- Macchinetta は、NTTの登録商標です。

- その他の会社名、製品名は、各社の登録商標または商標です。

### 1.2. 導入

#### 1.2.1. ガイドラインの目的

本ガイドラインではSpring Framework、Spring Batch、MyBatis を中心としたフルスタックフレームワークを利用して、 保守性の高いバッチアプリケーション開発をするためのベストプラクティスを提供する。

本ガイドラインを読むことで、ソフトウェア開発(主にコーディング)が円滑に進むことを期待する。

#### 1.2.2. ガイドラインの対象読者

本ガイドラインはソフトウェア開発経験のあるアーキテクトやプログラマ向けに書かれており、 以下の知識があることを前提としている。

- Spring FrameworkのDIやAOPに関する基礎的な知識がある

- Javaを使用してアプリケーションを開発したことがある

- SQLに関する知識がある

- Mavenを使用したことがある

これからJavaを勉強し始めるという人向けではない。

Spring Frameworkに関して、本ドキュメントを読むための基礎知識があるかどうかを測るために Spring Framework理解度チェックテスト を実施するとよい。 この理解度テストが4割回答できない場合は、別途以下のような書籍で学習することを推奨する。

- Spring徹底入門 (翔泳社) [日本語]

- ［改訂新版］Spring入門――Javaフレームワーク・より良い設計とアーキテクチャ [日本語]

- Pro Spring 6th Edition (Apress)

#### 1.2.3. ガイドラインの構成

まず、重要なこととして、本ガイドラインは Macchinetta Server Framework (1.x) Development Guideline (以降、Macchinetta Server 1.x 開発ガイドライン)のサブセットとして位置づけている。 出来る限りMacchinetta Server 1.x 開発ガイドラインを活用し説明の重複を省くことで、ユーザの学習コスト低減を狙っている。 よって随所にMacchinetta Server 1.x 開発ガイドラインへの参照を示しているため、両方のガイドを活用しながら開発を進めていってほしい。

Macchinetta Batch Framework (2.x)のコンセプト

  バッチ処理の基本的な考え方、Macchinetta Batch Framework (2.x)の基本的な考え方、Spring Batchの概要を説明する。

アプリケーション開発の流れ

  Macchinetta Batch Framework (2.x)を利用してアプリケーション開発する上で必ず押さえておかなくてはならない知識や作法について説明する。

ジョブの起動

  同期実行、非同期実行、起動パラメータといったジョブの起動方法について説明する。

データの入出力

  データベースアクセス、ファイルアクセスといった、各種リソースへの入出力について説明する。

異常系への対応

  入力チェックや例外ハンドリングといった異常系について説明する。

ジョブの管理

  ジョブの実行管理の方法について説明する。

フロー制御と並列･多重処理

  ジョブを並列処理/分散処理する方法について説明する。

チュートリアル

  基本的なバッチアプリケーション開発をとおして、Macchinetta Batch Framework (2.x)によるバッチアプリケーション開発を体験する。

#### 1.2.4. ガイドラインの読み方

以下のコンテンツはMacchinetta Batch Framework (2.x)を使用するすべての開発者が読むことを強く推奨する。

- Macchinetta Batch Framework (2.x)のコンセプト

- アプリケーション開発の流れ

以下のコンテンツは通常必要となるため、基本的には読んでおくこと。 開発対象に応じて、取捨選択するとよい。

- ジョブの起動

- データの入出力

- 異常系への対応

- ジョブの管理

以下のコンテンツは一歩進んだ実装をする際にはじめて参照すれば良い。

- フロー制御と並列･多重処理

以下のコンテンツはMacchinetta Batch Framework (2.x)を使用して実際のアプリケーション開発を体験したい開発者が読むことを推奨する。 はじめてMacchinetta Batch Framework (2.x)に触れる場合は、まずこのコンテンツから読み始め、他のコンテンツを参照しながら進めるとよい。

- チュートリアル

##### 1.2.4.1. ガイドラインの表記

本ガイドラインの表記について、留意事項を述べる。

WindowsコマンドプロンプトとUnix系ターミナルについて

  WindowsとUnix系の表記の違いで動作しなくなる場合は併記する。 そうでない場合は、Unix系の表記で統一する。

プロンプト記号

  Unix系の`$`にて表記する。

プロンプト表記例

    $ java -version

コメント記号

  Unix系の`#`にて表記する。
  なお、本ガイドラインでは、root権限でコマンドを実行することはなく、`#`はコメント記号としてのみ扱う。

コメント表記例

    $ # 行頭コメント
    $ java -version # 行末コメント

Bean定義（XMLConfig）のプロパティとコンストラクタについて

  本ガイドラインでは、`p`と`c`のネームスペースを用いた表記とする。 ネームスペースを用いることで、Bean定義の記述が簡潔になったり、コンストラクタ引数が明確になる効果がある。

ネームスペースを利用した記述

    <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
        <property name="lineTokenizer">
            <bean class="org.terasoluna.batch.item.file.transform.FixedByteLengthLineTokenizer"
                  c:ranges="1-6, 7-10, 11-12, 13-22, 23-32"
                  c:charset="MS932"
                  p:names="branchId,year,month,customerId,amount"/>
        </property>
    </bean>

参考までに、ネームスペースを利用しない記述を示す。

ネームスペースを利用しない記述

    <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
        <property name="lineTokenizer">
            <bean class="org.terasoluna.batch.item.file.transform.FixedByteLengthLineTokenizer">
                <constructor-arg index="0" value="1-6, 7-10, 11-12, 13-22, 23-32"/>
                <constructor-arg index="1" value="MS932"/>
                <property name="names" value="branchId,year,month,customerId,amount"/>
            </bean>
        </property>
    </bean>

なお、ユーザに対してネームスペースを用いる記述を強要することはない。 あくまで説明を簡潔にするための配慮であると受け止めてほしい。

注釈の凡例

  本ガイドラインで記載する注釈表記の凡例を以下に示す。

| NOTE 補足説明について記す。

| TIP 簡単な拡張方法や参考資料などについて記す。

| IMPORTANT 順守すべき事など重要事項について記す。

| WARNING 間違えやすい内容など注意すべき事項について記す。

| CAUTION 問題を引き起こしかねない設定や実装など非推奨事項について記す。

#### 1.2.5. ガイドラインの動作検証環境

本ガイドラインで説明している内容の動作検証環境については、 「 テスト済み環境 」を参照。

| Macchinetta Batch Framework (2.x)は、Spring Framework 6の動作環境に合わせ、以下を前提としている。 JDKのバージョンは、Spring Framework 6のベースラインであるJDK 17を使用する。 Tomcat 10.0.xがEOLしたことに伴い、Tomcat 10.1.xを使用する。 これに伴い、Jakarta EE 10ベースとなる。 Jakarta EE 9以降では、ライブラリが内包するクラスのパッケージ名が`javax`から`jakarta`に変更されている。 これに伴い、Jakarta EE 9以降に対応したアプリケーションサーバ以外では動かない点に注意すること。

### 1.3. 更新履歴

更新日付 | 更新箇所 | 変更内容
--- | --- | ---
2026-3-30 | - | 2.6.1.RELEASE 版公開
2025-3-28 | - | 2.6.0.RELEASE 版公開
2024-3-28 | - | 2.5.0.RELEASE 版公開
2023-3-30 | - | 2.4.0.RELEASE 版公開
2022-3-30 | - | 2.3.1.RELEASE 版公開
2021-3-26 | - | 2.3.0.RELEASE 版公開
2020-3-25 | - | 2.2.0.RELEASE 版公開
2019-3-26 | - | 2.1.1.RELEASE 版公開
2018-2-28 | - | 2.0.1.RELEASE 版公開
| Macchinetta Batch Framework (2.x)のスタック | CVE-2018-1199対応によるSpring Frameworkのバージョンアップデート
2017-12-22 | - | 2.0.0.RELEASE 版公開

## 2. Macchinetta Batch Framework (2.x)のコンセプト

### 2.1. 一般的なバッチ処理

#### 2.1.1. 一般的なバッチ処理とは

一般的に、バッチ処理とは「まとめて一括処理する」ことを指す。
データベースやファイルから大量のレコードを読み込み、処理し、書き出す処理であることが多い。
バッチ処理には以下の特徴があり、オンライン処理と比較して、応答性より処理スループットを優先した処理方式である。

バッチ処理の特徴

- データを一定の量でまとめて処理する。

- 処理に一定の順序がある。

- スケジュールに従って実行・管理される。

次にバッチ処理を利用する主な目的を以下に示す。

スループットの向上

  データをまとめて処理することで、処理のスループットを向上できる。
  ファイルやデータベースは、1件ごとにデータを入出力せず、一定件数にまとめることで、入出力待ちのオーバヘッドが劇的に少なくなり効率的である。 1件ごとの入出力待ちは微々たるものでも、大量データを処理する場合はその累積が致命的な遅延となる。

応答性の確保

  オンライン処理の応答性を確保するため、即時処理を行う必要がない処理をバッチ処理に切り出す。
  たとえば、すぐに処理結果が必要でない場合、オンライン処理で受付まで処理を行い、裏でバッチ処理を行う構成がある。 このような処理方式は、`ディレードバッチ`、`ディレードオンライン`などと呼ばれる。

時間やイベントへの対応

  特定の時刻に実行する処理やイベントに応じて実行する処理は、バッチ処理として実装することが素直と言える。
  たとえば、特定の時刻に実行する処理としては、業務要件により1ヶ月分のデータを翌月第1週の週末に集計する処理や、システム運用ルールに則って週末日曜の午前2時に1週間分の業務データをバックアップする処理などがある。 特定のイベントに応じて実行する処理としては、ジョブスケジューラと連携して特定のファイルの受信を検知した契機で、そのファイルを読み取って処理するなどがある。

外部システムとの連携上の制約

  ファイルなど外部システムとのインタフェースが制約となるために、バッチ処理を利用することもある。
  外部システムから送付されてきたファイルは、一定期間のデータをまとめたものになる。 これを取り込む処理は、オンライン処理よりもバッチ処理が向いている。

バッチ処理を実現するには、さまざまな技術要素を組み合わせることが一般的である。ここでは、主要な技術を紹介する。

ジョブスケジューラ

  バッチ処理の1実行単位をジョブと呼ぶ。これを管理するためのミドルウェアである。
  バッチシステムにおいて、ジョブが数個であることは稀であり、通常は数百、ときには数千にいたる場合もある。 そのため、ジョブの関連を定義し、実行スケジュールを管理する専用の仕組みが不可欠になる。

シェルスクリプト

  ジョブを実現する方法の1つ。OSやミドルウェアなどに実装されているコマンドを組み合わせて1つの処理を実現する。
  手軽に実装できる反面、複雑なビジネスロジックを記述するには不向きであるため、ファイルのコピー・バックアップ・テーブルクリアなど主にシンプルな処理に用いる。 また、別のプログラミング言語で実装した処理を実行する際に、起動前の設定や実行後の処理だけをシェルスクリプトが担うことも多い。

プログラミング言語

  ジョブを実現する方法の1つ。シェルスクリプトよりも構造化されたコードを記述でき、開発生産性・メンテナンス性・品質などを確保するのに有利である。 そのため、比較的複雑なロジックになりやすいファイルやデータベースのデータを加工するようなビジネスロジックの実装によく使われる。

#### 2.1.2. バッチ処理に求められる要件

業務処理を実現するために、バッチ処理に求められる要件には以下のようなものがある。

- 性能向上

  - 一定量のデータをまとめて処理できる。

  - ジョブを並列/多重に実行できる。

- 異常発生時のリカバリ

  - 再実行(手動/スケジュール)ができる。

  - 再処理した時に、処理済レコードをスキップして、未処理部分だけを処理できる。

- 多様な起動方式

  - 同期実行ができる。

  - 非同期実行ができる。

    - 実行契機としては、DBポーリング、HTTPリクエスト、などがある。

- さまざまな入出力インタフェース

  - データベース

  - ファイル

    - CSVやTSVなどの可変長

    - 固定長

    - XML

上記の要件について具体的な内容を以下に示す。

大量データを一定のリソースで効率よく処理できる(性能向上)

  大量のデータをまとめて処理することで処理時間を短縮する。このとき重要なのは、「一定のリソースで」の部分である。
  100万件でも1億件でも、一定のCPUやメモリの使用で処理でき、件数に応じて緩やかにかつリニアに処理時間が延びるのが理想である。 まとめて処理するには、一定件数ごとにトランザクションを開始・終了させ、まとめて入出力することで、 使用するリソースを平準化させる仕組みが必要となる。
  それでも処理しきれない膨大なデータを相手にする場合は、一歩進んでハードウェアリソースを限界まで使い切る仕組みも追加で必要になる。 処理対象データを件数やグループで分割して、複数プロセス・複数スレッドによって多重処理する。 さらに推し進めて複数マシンによる分散処理をすることもある。 リソースを限界まで使い切る際には、入出力を限りなく低減することがきわめて重要になる。

可能な限り処理を継続する(異常発生時のリカバリ)

  大量データを処理するにあたって、入力データが異常な場合や、システム自体に異常が発生した場合の防御策を考えておく必要がある。
  大量データは必然的に処理し終わるまでに長時間かかるが、エラー発生後に復旧までの時間が長期化すると、システム運用に大きな影響を及ぼしてしまう。
  たとえば、1000万件のデータを処理する場合を考える。999万件目でエラーになり、それまでの処理をすべてやり直すとしたら、 運用スケジュールに影響が出てしまうことは明白である。
  このような影響を抑えるために、バッチ処理ならではの処理継続性が重要となる。これにはエラーデータをスキップしながら次のデータを処理する仕組み、 処理をリスタートする仕組み、可能な限り自動復旧を試みる仕組み、などが必要となる。また、1つのジョブを極力シンプルなつくりにし、再実行を容易にすることも重要である。

実行契機に応じて柔軟に実行できる(多様な起動方式)

  時刻を契機とする場合、オンラインや外部システムとの連携を契機とした場合など、さまざまな実行契機に対応する仕組みが必要になる。 同期実行ではジョブスケジューラから定時になったら処理を起動する、 非同期実行ではプロセスを常駐させておきイベントに応じて随時バッチ処理を行う、というような 様々な仕組みが一般的に知られている。

さまざまな入出力インタフェースを扱える(さまざまな入出力インタフェース)

  オンラインや外部システムと連携するということは、データベースはもちろん、CSV/XMLといったさまざまなフォーマットのファイルを扱えることが重要となる。 さらに、それぞれの入出力形式を透過的に扱える仕組みがあると実装しやすくなり、複数フォーマットへの対応も迅速に行なえるようになる。

#### 2.1.3. バッチ処理で考慮する原則と注意点

バッチ処理システムを構築する際に考慮すべき重要な原則、および、いくつかの一般的な考慮事項を示す。

- 単一のバッチ処理は可能な限り簡素化し、複雑な論理構造を避ける。

- 処理とデータは物理的に近い場所におく(処理を実行する場所にデータを保存する)。

- システムリソース(特に入出力)の利用を最小限にし、できるだけインメモリで多くの操作を実行する。

- また、不要な物理入出力を避けるため、アプリケーションの入出力(SQLなど)を見直す。

- 複数のジョブで同じ処理を繰り返さない。

  - たとえば、集計処理とレポート処理がある場合に、レポート処理で集計処理を再度することは避ける。

- 常にデータの整合性に関しては最悪の事態を想定する。十分なチェックとデータの整合性を維持するために、データの検証を行う。

- バックアップについて十分に検討する。特にシステムが年中無休で実行されている場合は、バックアップの難易度が高くなる。

### 2.2. Macchinetta Batch Framework (2.x)のスタック

#### 2.2.1. 概要

Macchinetta Batch Framework (2.x)の構成について説明し、Macchinetta Batch Framework (2.x)の担当範囲を示す。

#### 2.2.2. Macchinetta Batch Framework (2.x)のスタック

Macchinetta Batch Framework (2.x)で使用するSoftware Frameworkは、 Spring Framework (Spring Batch) を中心としたOSSの組み合わせである。以下にMacchinetta Batch Framework (2.x)のスタック概略図を示す。

図 1. Macchinetta Batch Framework (2.x)のスタック概略図

ジョブスケジューラやデータベースなどの製品についての説明は、本ガイドラインの説明対象外とする。

##### 2.2.2.1. 利用するOSSのバージョン

Macchinetta Batch Framework (2.x)のバージョン2.6.1.RELEASEで利用するOSSのバージョン一覧を以下に示す。

| Macchinetta Batch Framework (2.x)で使用するOSSのバージョンは、原則として、Spring Boot Dependenciesの定義に準じている。 なお、バージョン2.6.1.RELEASEにおけるSpring Boot Dependenciesのバージョンは、 3.5.9である。

表 1. OSSバージョン一覧
用途 | 用途説明 | GroupId | ArtifactId | Version
--- | --- | --- | --- | ---
TERASOLUNA Batch フレームワーク | バッチフレームワークを拡張する。 | org.terasoluna.batch | terasoluna-batch | 5.7.1.RELEASE
バッチフレームワーク | バッチフレームワークとして、Spring Batchを利用する。 | org.springframework.batch | spring-batch-core | 5.2.4
org.springframework.batch | spring-batch-infrastructure | 5.2.4
基盤フレームワーク | フレームワークの基盤として、利用する。 | org.springframework | spring-aop | 6.2.15
org.springframework | spring-beans | 6.2.15
org.springframework | spring-context | 6.2.15
org.springframework | spring-expression | 6.2.15
org.springframework | spring-core | 6.2.15
org.springframework | spring-tx | 6.2.15
org.springframework | spring-jdbc | 6.2.15
org.springframework | spring-oxm | 6.2.15
org.springframework.retry | spring-retry | 2.0.12
Webコンテナ起動の際に、Spring Batchと連携する。 | org.springframework | spring-webmvc | 6.2.15
O/R Mapper |  | org.mybatis | mybatis | 3.5.19
| org.mybatis | mybatis-spring | 3.0.5
DI |  | jakarta.inject | jakarta.inject-api | 2.0.1
ログ出力 | 実装にLogbackを利用し、SLF4Jはロギングの抽象化レイヤーを提供する。 | ch.qos.logback | logback-classic | 1.5.22
org.slf4j | slf4j-api | 2.0.17
入力チェック | 単項目チェックにJakarta Validationを利用し、実装にはHibernate Validatorを使用する。 | jakarta.validation | jakarta.validation-api | 3.0.2
org.hibernate.validator | hibernate-validator | 8.0.3.Final
コネクションプール |  | org.apache.commons | commons-dbcp2 | 2.13.0
EL式 |  | org.apache.tomcat.embed | tomcat-embed-el | 10.1.50
インメモリデータベース | H2をデフォルトで使用しているが切り替え可能。 | com.h2database | h2 | 2.3.232
JDBCドライバ |  | org.postgresql | postgresql | 42.7.8
JSON | JSONデータのシリアライズおよびデシリアライズに使用する。 | com.fasterxml.jackson.core | jackson-databind | 2.19.4
XML | XMLデータのシリアライズおよびデシリアライズに使用する。 | jakarta.xml.bind | jakarta.xml.bind-api | 4.0.4
com.sun.xml.bind | jaxb-impl | 4.0.6

##### 2.2.2.2. TERASOLUNA Framework for Java (5.x) Dependencies

TERASOLUNA Framework for Java (5.x) Dependencies (terasoluna-dependencies)はMacchinetta Batch Framework (2.x)で使用するOSSライブラリのバージョンを一元管理するためのBOM(Project Object Model)プロジェクトである。 terasoluna-dependenciesでは以下を定義している。

- Macchinetta Batch Framework (2.x)が参照する spring-boot-dependenciesのバージョン

- spring-boot-dependenciesが管理しているライブラリ以外でMacchinetta Batch Framework (2.x)が参照するライブラリのバージョン

terasoluna-dependenciesで管理されているライブラリのバージョンをそのまま利用する場合は、アプリケーション側のpom.xmlにおいて、ライブラリのバージョンを定義する必要はない。

terasoluna-dependenciesで定義しているライブラリとそのバージョンについては、 Managed Dependency Coordinatesで公開している。

#### 2.2.3. Macchinetta Batch Framework (2.x)の構成要素

Macchinetta Batch Framework (2.x)のSoftware Framework構成要素について説明する。 以下にSoftware Framework構成要素の概略図を示す。各要素の概要は後述する。

図 2. Software Framework構成要素の概略図

以下に、各要素の概要を示す。

非同期実行

  非同期実行を実現する方法として、以下の機能を利用する。

  DBポーリングによる周期起動

    TERASOLUNA Batch Framework for Java (5.x)が提供するライブラリを利用する。

    - "非同期実行(DBポーリング)"

ファイルアクセス

  Spring Batchから提供されている機能 に加えて、補助機能をTERASOLUNA Batch Framework for Java (5.x)が提供する。

  - "ファイルアクセス"

バリデーション
  相関チェック

    相関チェックにはBean Validation、もしくはSpring Validationを利用する。

    - Spring Validation 6.2.15

### 2.3. Spring Batchのアーキテクチャ

#### 2.3.1. Overview

Macchinetta Batch Framework (2.x)の基盤となる、Spring Batchのアーキテクチャについて説明をする。

##### 2.3.1.1. Spring Batchとは

Spring Batchは、その名のとおりバッチアプリケーションフレームワークである。 SpringがもつDIコンテナやAOP、トランザクション管理機能をベースとして以下の機能を提供している。

処理の流れを定型化する機能
  タスクレットモデル
    シンプルな処理

      自由に処理を記述する方式である。SQLを1回発行するだけ、コマンドを発行するだけ、といった簡素なケースや 複数のデータベースやファイルにアクセスしながら処理するような複雑で定型化しにくいケースで用いる。

  チャンクモデル
    大量データを効率よく処理

      一定件数のデータごとにまとめて入力/加工/出力する方式。データの入力/加工/出力といった処理の流れを定型化し、 一部を実装するだけでジョブが実装できる。

様々な起動方法

  コマンドライン実行、Servlet上で実行、その他のさまざまな契機での実行を実現する。

様々なデータ形式の入出力

  ファイル、データベース、メッセージキューをはじめとするさまざまなデータリソースとの入出力を簡単に行う。

処理の効率化

  多重実行、並列実行、条件分岐を設定ベースで行う。

ジョブの管理

  実行状況の永続化、データ件数を基準にしたリスタートなどを可能にする。

##### 2.3.1.2. Hello, Spring Batch！

Spring Batchのアーキテクチャを理解する上で、未だSpring Batchに触れたことがない場合は、 以下の公式ドキュメントを一読するとよい。 Spring Batchを用いた簡単なアプリケーションの作成を通して、イメージを掴んでほしい。

Creating a Batch Service

##### 2.3.1.3. Spring Batchの基本構造

Spring Batchの基本的な構造を説明する。

Spring Batchはバッチ処理の構造を定義している。この構造を理解してから開発を行うことを推奨する。

図 3. Spring Batchに登場する主な構成要素

表 2. Spring Batchに登場する主な構成要素
構成要素 | 役割
--- | ---
Job | Spring Batchにおけるバッチアプリケーションの一連の処理をまとめた1実行単位。
Step | Jobを構成する処理の単位。1つのJobに1～N個のStepをもたせることが可能。 1つのJobを複数のStepに分割して処理することにより、処理の再利用、並列化、条件分岐が可能になる。 Stepは、チャンクモデルまたはタスクレットモデル(これらについては後述する)のいずれかで実装する。
JobLauncher | Jobを起動するためのインタフェース。 JobLauncherをユーザが直接利用することも可能だが、javaコマンドから `CommandLineJobRunner`を起動することでより簡単にバッチ処理を開始できる。 `CommandLineJobRunner`は、JobLauncherを起動するための各種処理を引き受けてくれる。
ItemReader ItemProcessor ItemWriter | チャンクモデルを実装する際に、データの入力/加工/出力の3つに分割するためのインタフェース。 バッチアプリケーションは、この3パターンの処理で構成されることが多いことに由来し、 Spring Batchでは主にチャンクモデルでこれらインタフェースの実装を活用する。 ユーザはビジネスロジックをそれぞれの役割に応じて分割して記述する。 データの入出力を担う`ItemReader`と`ItemWriter`は、データベースやファイルからJavaオブジェクトへの変換、もしくはその逆の処理であることが多い。 そのため、Spring Batchから標準的な実装が提供されている。 ファイルやデータベースからデータの入出力を行う一般的なバッチアプリケーションの場合は、 Spring Batchの標準実装をそのまま使用するだけで要件を満たせるケースもある。 データの加工を担う`ItemProcessor`は、入力チェックやビジネスロジックを実装する。 タスクレットモデルでは、`ItemReader`/`ItemProcessor`/`ItemWriter`が、1つの`Tasklet`インタフェース実装に置き換わる。Tasklet内に入出力、入力チェック、ビジネスロジックのすべてを実装する必要がある。
JobRepository | JobやStepの状況を管理する機構。これらの管理情報は、Spring Batchが規定するテーブルスキーマをもとにデータベース上に永続化される。

#### 2.3.2. Architecture

OverviewではSpring Batchの基本構造については簡単に説明した。

これを踏まえて、以下の点について説明をする。

- 処理全体の流れ

- Jobの起動

- ビジネスロジックの実行

- JobRepositoryのメタデータスキーマ

最後に、Spring Batchを利用したバッチアプリケーションの性能チューニングポイントについて説明をする。

- 代表的な性能チューニングポイント

##### 2.3.2.1. 処理全体の流れ

Spring Batchの主な構成要素と処理全体の流れについて説明をする。 また、ジョブの実行状況などのメタデータがどのように管理されているかについても説明する。

Spring Batchの主な構成要素と処理全体の流れ(チャンクモデル)を下図に示す。

図 4. Spring Batchの主な構成要素と処理全体の流れ

中心的な処理の流れ(黒線)とジョブ情報を永続化する流れ(赤線)について説明する。

中心的な処理の流れ

1. ジョブスケジューラからJobLauncherが起動される。

2. JobLauncherからJobを実行する。

3. JobからStepを実行する。

4. StepはItemReaderによって入力データを取得する。

5. StepはItemProcessorによって入力データを加工する。

6. StepはItemWriterによって加工されたデータを出力する

ジョブ情報を永続化する流れ

1. JobLauncherはJobRepositoryを介してDatabaseにJobInstanceを登録する。

2. JobLauncherはJobRepositoryを介してDatabaseにジョブが実行開始したことを登録する。

3. JobStepはJobRepositoryを介してDatabaseに入出力件数や状態など各種情報を更新する。

4. JobLauncherはJobRepositoryを介してDatabaseにジョブが実行終了したことを登録する。

新たに構成要素と永続化に焦点をあてたJobRepositoryについての説明を以下に示す。

表 3. 永続化に関連する構成要素
構成要素 | 役割
--- | ---
JobInstance | Spring BatchはJobの「論理的」な実行を示す。JobInstanceをJob名と引数によって識別している。 言い換えると、Job名と引数が同一である実行は、同一JobInstanceの実行と認識し、前回起動時の続きとしてJobを実行する。 対象のJobが再実行をサポートしており、前回実行時にエラーなどで処理が途中で中断していた場合は処理の途中から実行される。 一方、再実行をサポートしていないJobや、対象のJobInstanceがすでに正常に処理が完了している場合は例外が発生し、Javaプロセスが異常終了する。 たとえば、すでに正常に処理が完了している場合はJobInstanceAlreadyCompleteExceptionが発生する。
JobExecution ExecutionContext | JobExecutionはJobの「物理的」な実行を示す。JobInstance とは異なり、同一のJobを再実行する場 合も別のJobExecutionとなる。結果、JobInstanceとJobExecutionは1対多の関係になる。 同一のJobExecution内で処理の進捗状況などのメタデータを共有するための領域として、ExecutionContextがある。 ExecutionContextは主にSpring Batchがフレームワークの状態などを記録するために使用されているが、アプリケーションがExecutionContextへアクセスする手段も提供されている。 JobExecutionContextに格納するオブジェクトは、`java.io.Serializable`を実装したクラスでなければならない。
StepExecution ExecutionContext | StepExecutionはStep の「物理的」な実行を示す。JobExecutionとStepExecutionは1対多の関係になる。 JobExecutionと同様に、Step内でデータを共有するための領域としてExecutionCotnextがある。データの局所化という観点から、 複数のStepで共有しなくてもよい情報はJobのExecutionContextを使用するのでなく、対象StepのExecutionContextを利用したほうがよい。 StepExecutionContextに格納するオブジェクトは、`java.io.Serializable`を実装したクラスでなければならない。
JobRepository | JobExecutionやStepExecutionなどのバッチアプリケーション実行結果や状態を管理するためのデータを管理、永続化する機能を提供する。 一般的なバッチアプリケーションはJavaプロセスを起動することで処理が開始し、処理の終了とともにJavaプロセスも終了させるケースが多い。 そのためこれらのデータはJavaプロセスを跨いで参照される可能性があることから、揮発性なメモリ上だけではなくデータベースなどの永続層へ格納する。 データベースに格納する場合は、JobExecutionやStepExecutionを格納するためのテーブルやシーケンスなどのデータベースオブジェクトが必要になる。 Spring Batch が提供するスキーマ情報をもとにデータベースオブジェクトを生成する必要がある。

Spring Batchが重厚にメタデータの管理を行っている理由は、再実行を実現するためである。 バッチ処理を再実行可能にするには、前回実行時のスナップショットを残しておく必要があり、メタデータやJobRepositoryはそのための基盤となっている。

##### 2.3.2.2. Jobの起動

Jobをどのように起動するかについて説明する。

Javaプロセス起動直後にバッチ処理を開始し、バッチ処理完了後にJavaプロセスを終了するケースを考える。 Spring Batch上で定義されたJobを開始するには、Javaを起動するシェルスクリプトを記述するのが一般的である。 また、Spring Batchが提供するCommandLineJobRunnerを使用することで、ユーザが定義したSpring Batch上のJobを簡単に起動することができる。

下図にJavaプロセス起動からバッチ処理開始までの流れを示す。

図 5. Javaプロセス起動からバッチ処理開始までの流れ

Javaプロセス起動からバッチ処理開始までの流れについて説明する。

Javaプロセス起動からバッチ処理開始までの流れ

1. シェルスクリプトからジョブを起動するためにCommandLineJobRunnerを起動する。

   - CommandLineJobRunnerは起動するJob名だけでなく、引数(ジョブパラメータ)を渡すことも可能であり、引数は`<Job引数名>=<値>`の形式で指定する。

2. CommandLineJobRunnerはJobLauncherを起動する。

3. JobLauncherはJobRepositoryからJob名と引数に合致するJobInstanceをデータベースから取得する。

   - 該当するJobInstanceが存在しない場合は、JobInstanceを新規登録する。

   - 該当するJobInstanceが存在した場合は、紐付いているJobExecutionを復元する。

   - Spring Batchでは日次実行など繰り返して起動する可能性のあるJobに対しては、JobExecutionをユニークにするためだけの引数を追加する方法がとられている。 たとえば、システム時刻であったり、乱数を引数に追加する方法が挙げられる。
     本ガイドラインで推奨している方法についてはパラメータ変換クラスについてを参照。

4. JobLauncherはJobExecutionを生成する。

5. JobLauncherはExecutionContextおよびJobParametersを登録する。

   - CommandLineJobRunnerに渡されたすべての引数はCommandLineJobRunnerおよびJobLauncherが解釈とチェックを行なったうえで、JobExecutionへJobParametersに変換して格納される。詳細はジョブの起動パラメータを参照。

6. JobLauncherはjobを実行する。

##### 2.3.2.3. ビジネスロジックの実行

Spring Batchでは、JobをStepと呼ぶさらに細かい単位に分割する。 Jobが起動すると、StepExecutionを生成し、Jobは自身に登録されているStepを起動する。 Stepはあくまで処理を分割するための枠組みであり、ビジネスロジックの実行はStepから呼び出されるTaskletに任されている。

StepからTaskletへの流れを以下に示す。

図 6. StepからTaskletへの流れ

StepからTaskletへの流れについて説明する。

StepからTaskletへの流れ

1. JobはStepExecutionを生成する。

2. JobはExecutionContextを登録する。

3. JobはStepを実行する。

4. StepはTaskletを実行する。

Taskletの実装方法には「チャンクモデル」と「タスクレットモデル」の2つの方式がある。 概要についてはすでに説明しているため、ここではその構造について説明する。

###### 2.3.2.3.1. チャンクモデル

前述したようにチャンクモデルとは、処理対象となるデータを1件ずつ処理するのではなく、一定数の塊(チャンク)を単位として処理を行う方式である。 ChunkOrientedTaskletがチャンク処理をサポートしたTaskletの具象クラスとなる。 このクラスがもつcommit-intervalという設定値により、チャンクに含めるデータの最大件数(以降、「チャンク数」と呼ぶ)を調整することができる。 `ItemReader`、`ItemProcessor`、`ItemWriter`は、いずれもチャンク処理を前提としたインタフェースとなっている。

次に、ChunkOrientedTasklet がどのように`ItemReader`、`ItemProcessor`、`ItemWriter`を呼び出しているかを説明する。

ChunkOrientedTaskletが1つのチャンクを処理するシーケンス図を以下に示す。

図 7. ChunkOrientedTaskletによるチャンク処理

ChunkOrientedTaskletは、チャンク数分だけ`ItemReader`および`ItemProcessor`、すなわちデータの読み込みと加工を繰り返し実行する。 チャンク数分のデータすべての読み込みが完了してから、ItemWriterのデータ書き込み処理が1回だけ呼び出され、チャンクに含まれるすべての加工済みデータが渡される。 データの更新処理がチャンクに対して1回呼び出されるように設計されているのは、JDBCのaddBatch、executeBatchのように入出力をまとめやすくするためである。

次に、チャンク処理において実際の処理を担う`ItemReader`、`ItemProcessor`、`ItemWriter`について紹介する。 各インタフェースともユーザが独自に実装を行うことが想定されているが、Spring Batchが提供する汎用的な具象クラスでまかなうことができる場合がある。

特に`ItemProcessor`はビジネスロジックそのものが記述されることが多いため、Spring Batchからは具象クラスがあまり提供されていない。 ビジネスロジックを記述する場合は`ItemProcessor`インタフェースを実装する。 `ItemProcessor`はタイプセーフなプログラミングが可能になるよう、入出力で使用するオブジェクトの型をそれぞれ型引数に指定できるようになっている。

項番 | 説明
--- | ---
(1) | 入出力で使用するオブジェクトの型をそれぞれ型引数に指定した`ItemProcessor`インタフェースを実装する。
(2) | `process`メソッドを実装する。引数のitemが入力データである。
(3) | 出力オブジェクトを作成し、入力データのitemに対して処理したビジネスロジックの結果を格納する。
(4) | 出力オブジェクトを返却する。

ItemReaderやItemWriterは様々な具象クラスがSpring Batchから提供されており、それらを利用することで十分な場合が多い。 しかし、特殊な形式のファイルを入出力したりする場合は、独自のItemReaderやItemWriterを実装した具象クラスを作成し使用することができる。

実際のアプリケーション開発時におけるビジネスロジックの実装に関しては、アプリケーション開発の流れを参照。

最後にSpring Batchが提供するItemReader、ItemProcessor、ItemWriterの代表的な具象クラスを示す。

表 4. Spring Batchが提供するItemReader、ItemProcessor、ItemWriterの代表的な具象クラス
インタフェース | 具象クラス名 | 概要
--- | --- | ---
ItemReader | FlatFileItemReader | CSVファイルなどの、フラットファイル(非構造的なファイル)の読み込みを行う。Resourceオブジェクトをインプットとし、区切り文字やオブジェクトへのマッピングルールをカスタマイズすることができる。
StaxEventItemReader | XMLファイルの読み込みを行う。名前のとおり、StAXをベースとしたXMLファイルの読み込みを行う実装となっている。
JdbcCursorItemReader JdbcPagingItemReader | JDBCを使用してSQLを実行し、データベース上のレコードを読み込む。データベース上にある大量のデータを処理する場合は、全件をメモリ上に読み込むことを避け、一度の処理に必要なデータのみの読み込み、破棄を繰り返す必要がある。 JdbcPagingItemReaderはJdbcTemplateを用いてSELECT SQLをページごとに分けて発行することで実現する。一方、JdbcCursorItemReaderはJDBCのカーソルを使用することで、1回のSELECT SQLの発行で実現する。
MyBatisCursorItemReader MyBatisPagingItemReader | MyBatisと連携してデータベース上のレコードを読み込む。MyBatisが提供しているSpring連携ライブラリMyBatis-Springから提供されている。PagingとCursorの違いについては、MyBatisを利用して実現していること以外はJdbcXXXItemReaderと同様。 Macchinetta Batch 2.xでは、データベースを参照する際には`MyBatisCursorItemReader`を利用することを基本とする。
JmsItemReader AmqpItemReader | JMSやAMQPからメッセージを受信し、その中に含まれるデータの読み込みを行う。
JpaCursorItemReader JpaPagingItemReader | JPA実装と連携してデータベース上のレコードを読み込む。
HibernateCursorItemReader HibernatePagingItemReader | Hibernateと連携してデータベース上のレコードを読み込む。
ItemProcessor | PassThroughItemProcessor | 何も行なわない。入力データの加工や修正が不要な場合に使用する。 詳細は、ItemProcessorの省略を参照。
ValidatingItemProcessor | 入力チェックを行う。入力チェックルールの実装には、Spring Batch独自の org.springframework.batch.item.validator.Validatorを実装する必要がある。 しかし、Springから提供されている汎用的なorg.springframework.validation.ValidatorへのアダプタであるSpringValidatorが提供されており、 org.springframework.validation.Validatorのルールを利用できる。 Macchinetta Batch 2.xではValidatingItemProcessorの利用は禁止している。 詳細は、入力チェックを参照。
CompositeItemProcessor | 同一の入力データに対し、複数のItemProcessorを逐次的に実行する。ValidatingItemProcessorによる入力チェックの後にビジネスロジックを実行したい場合などに有効。
ItemWriter | FlatFileItemWriter | 処理済みのJavaオブジェクトを、CSVファイルなどのフラットファイルとして書き込みを行う。区切り文字やオブジェクトからファイル行へのマッピングルールをカスタマイズできる。
StaxEventItemWriter | 処理済みのJavaオブジェクトをXMLファイルとして書き込みを行う。
JdbcBatchItemWriter | JDBCを使用してSQLを実行し、処理済みのJavaオブジェクトをデータベースへ出力する。内部ではJdbcTemplateが使用されている。
MyBatisBatchItemWriter | MyBatisと連携して、処理済みのJavaオブジェクトをデータベースへ出力する。MyBatisが提供しているSpring連携ライブラリMyBatis-Springから提供されている。
JmsItemWriter AmqpItemWriter | 処理済みのJavaオブジェクトを、JMSやAMQPでメッセージを送信する。
JpaItemWriter | JPA実装と連携してデータベースへの出力を行う。 Macchinetta Batch 2.xでは、JpaItemWriterは利用しない。
HibernateItemWriter | Hibernateと連携してデータベースへの出力を行う。 Macchinetta Batch 2.xでは、HibernateItemWriterは利用しない。

###### 2.3.2.3.2. タスクレットモデル

チャンクモデルは、複数の入力データを1件ずつ読み込み、一連の処理を行うバッチアプリケーションに適した枠組みとなっている。 しかし、時にはチャンク処理の型に当てはまらないような処理を実装することもある。 たとえば、システムコマンドを実行したり、制御用テーブルのレコードを1件だけ更新したいような場合などである。

そのような場合には、チャンク処理によって得られる性能面のメリットが少なく、 設計や実装を困難にするデメリットの方が大きいため、タスクレットモデルを使用するほうが合理的である。

タスクレットモデルを使用する場合は、Spring Batchから提供されている`Tasklet`インタフェースをユーザが実装する必要がある。 また、Spring Batchでは以下の具象クラスが提供されているが、Macchinetta Batch 2.xでは以降説明しない。

表 5. Spring Batchが提供するTaskletの具象クラス
クラス名 | 概要
--- | ---
SystemCommandTasklet | 非同期にシステムコマンドを実行するためのTasklet。commandプロパティに実行したいコマンドを指定する。 システムコマンドは呼び出しもとのスレッドと別スレッドで実行されるため、タイムアウトを設定したり、処理中にシステムコマンドの実行スレッドをキャンセルすることも可能である。
MethodInvokingTaskletAdapter | POJOクラスに定義された特定のメソッドを実行するためのTasklet。targetObjectプロパティに対象クラスのBeanを、targetMethodプロパティに実行させたいメソッド名を指定する。 POJOクラスはバッチ処理の終了した状態をメソッドの返り値として返却することができるが、その場合は後述する`ExitStatus`をメソッドの返り値とする必要がある。 他の型で返り値を返却した場合は、返り値の内容にかかわらず正常終了した(ExitStatus.COMPLETED)とみなされる。

##### 2.3.2.4. JobRepositoryのメタデータスキーマ

JobRepositoryのメタデータスキーマについて説明する。

なお、Spring Batchのリファレンス Appendix B. Meta-Data Schema にて説明されている内容も含めて、全体像を説明する。

Spring Batchメタデータテーブルは、Javaでそれらを表すドメインオブジェクト(Entityオブジェクト)に対応している。

表 6. 対応一覧
テーブル | Entityオブジェクト | 概要
BATCH_JOB_INSTANCE | JobInstance | ジョブ名、およびジョブパラメータをシリアライズした文字列を保持する。
BATCH_JOB_EXECUTION | JobExecution | ジョブの状態・実行結果を保持する。
BATCH_JOB_EXECUTION_PARAMS | JobExecutionParams | 起動時に与えられたジョブパラメータを保持する。
BATCH_JOB_EXECUTION_CONTEXT | JobExecutionContext | ジョブ内部のコンテキストを保持する。
BATCH_STEP_EXECUTION | StepExecution | ステップの状態・実行結果、コミット・ロールバック件数を保持する。
BATCH_STEP_EXECUTION_CONTEXT | StepExecutionContext | ステップ内部のコンテキストを保持する。

JobRepositoryは、各Javaオブジェクトに保存された内容を、テーブルへ正確に格納する責任がある。

| メタデータテーブルへ格納する文字列について メタデータテーブルへ格納する文字列には文字数の制限があり、制限を超えた分の文字列を切り捨てる。 またSpring Batchではマルチバイト文字を考慮しておらず、Spring Batchが提供するメタデータテーブルのDDLでは格納する文字列が制限に収まる文字数でもエラーになる可能性がある。 マルチバイト文字を格納するためには、メタデータテーブルのカラムを使用するエンコーディングによってサイズ拡張したり、文字データ型を文字数定義に設定する必要がある。

6つの全テーブルと相互関係のERDモデルを以下に示す。

図 8. ER図

###### 2.3.2.4.1. バージョン

データベーステーブルの多くは、バージョンカラムが含まれている。 Spring Batchは、データベースへの更新を扱う楽観的ロック戦略を採用しているため、このカラムは重要となる。 このレコードは、バージョンカラムの値がインクリメントされるたびに更新されることを意味している。 JobRepositoryが値の更新時に、バージョン番号が変更されている場合、同時アクセスのエラーが発生したことを示すOptimisticLockingFailureExceptionがスローされる。 別のバッチジョブは異なるマシンで実行されているかもしれないが、それらはすべて同じデータベーステーブルを使用しているため、このチェックが必要となる。

###### 2.3.2.4.2. ID(シーケンス)定義

BATCH_JOB_INSTANCE、BATCH_JOB_EXECUTION、およびBATCH_STEP_EXECUTIONはそれぞれ、JOB_INSTANCE_ID、JOB_EXECUTION_ID、STEP_EXECUTION_IDという列を有している。 これらの列は、それぞれのテーブル用主キーとして機能し、個別のシーケンスによって生成される。 これは、データベースにドメインオブジェクトの一つを挿入した後、与えられたキーをJavaで一意に識別できるように、実際のオブジェクトに設定する必要があるためである。
データベースによってはシーケンスをサポートしていないことがある。この場合、以下のようにテーブルをシーケンスとして作成する。以下のクエリはいずれのデータベースでも適用できるものではないので、利用するデータベースに合わせて適宜読み替えてほしい。

テーブルをシーケンスとして作成する例

    CREATE TABLE BATCH_JOB_SEQ (ID BIGINT NOT NULL);
    INSERT INTO BATCH_JOB_SEQ values(0);

###### 2.3.2.4.3. テーブル定義

各テーブルの項目について説明をする。

BATCH_JOB_INSTANCE

BATCH_JOB_INSTANCEテーブルはJobInstanceに関連するすべての情報を保持し、全体的な階層の最上位である。

表 7. BATCH_JOB_INSTANCEの定義
カラム名 | 説明
--- | ---
JOB_INSTANCE_ID | インスタンスを識別する一意のIDで主キーである。
VERSION | バージョンを参照。
JOB_NAME | ジョブの名前。 インスタンスを識別するために必要とされるので非nullである。
JOB_KEY | 同じジョブを別々のインスタンスとして一意に識別するためのシリアライズ化されたJobParameters。 同じジョブ名をもつJobInstancesは、異なるJobParameters(つまり、異なるJOB_KEY値)をもつ必要がある。

BATCH_JOB_EXECUTION

BATCH_JOB_EXECUTIONテーブルはJobExecutionオブジェクトに関連するすべての情報を保持する。 ジョブが実行されるたびに、常に新しいJobExecutionでこの表に新しい行が登録される。

表 8. BATCH_JOB_EXECUTIONの定義
カラム名 | 説明
--- | ---
JOB_EXECUTION_ID | 一意にこのジョブ実行を識別する主キー。
VERSION | バージョンを参照。
JOB_INSTANCE_ID | このジョブ実行が属するインスタンスを示すBATCH_JOB_INSTANCEテーブルからの外部キー。 インスタンスごとに複数の実行が存在する場合がある。
CREATE_TIME | ジョブ実行が作成された時刻。
START_TIME | ジョブ実行が開始された時刻。
END_TIME | ジョブ実行が成功または失敗に関係なく、終了した時刻を表す。 ジョブが現在実行されていないにもかかわらず、このカラムの値が空であることは、いくつかのエラータイプがあり、フレームワークが最後のセーブを実行できなかったことを示す。
STATUS | ジョブ実行のステータスを表す文字列。BatchStatus列挙オブジェクトが出力する文字列である。
EXIT_CODE | ジョブ実行の終了コードを表す文字列。 CommandLineJobRunnerによる起動の場合、これを数値に変換することができる。
EXIT_MESSAGE | ジョブが終了した状態をより詳細に説明する文字列。 障害が発生した場合には、可能であればスタックトレースをできるだけ多く含む文字列となる場合がある。
LAST_UPDATED | このレコードのジョブ実行が最後に更新された時刻。

BATCH_JOB_EXECUTION_PARAMS

BATCH_JOB_EXECUTION_PARAMSテーブルは、JobParametersオブジェクトに関連するすべての情報を保持する。 これはジョブに渡された0以上のキーと値とのペアが含まれ、ジョブが実行されたパラメータを記録する役割を果たす。

表 9. BATCH_JOB_EXECUTION_PARAMSの定義
カラム名 | 説明
--- | ---
JOB_EXECUTION_ID | このジョブパラメータが属するジョブ実行を示すBATCH_JOB_EXECUTIONテーブルからの外部キー。
PARAMETER_NAME | パラメータキー。
PARAMETER_TYPE | データ型を示す文字列。
PARAMETER_VALUE | パラメータ値を示す文字列。
IDENTIFYING | パラメータがジョブインスタンスが一意であることを識別するための値であることを示すフラグ。

BATCH_JOB_EXECUTION_CONTEXT

BATCH_JOB_EXECUTION_CONTEXTテーブルは、JobのExecutionContextに関連するすべての情報を保持する。 特定のジョブ実行に必要とされるジョブレベルのデータがすべて含まれている。 このデータは、ジョブが失敗した後で処理を再処理する際に取得しなければならない状態を表し、失敗したジョブが「処理を中断したところから始める」ことを可能にする。

表 10. BATCH_JOB_EXECUTION_CONTEXTの定義
カラム名 | 説明
--- | ---
JOB_EXECUTION_ID | このJobのExecutionContextが属するジョブ実行を示すBATCH_JOB_EXECUTIONテーブルからの外部キー。
SHORT_CONTEXT | SERIALIZED_CONTEXTの文字列表現。
SERIALIZED_CONTEXT | シリアライズされたコンテキスト全体。

BATCH_STEP_EXECUTION

BATCH_STEP_EXECUTIONテーブルは、StepExecutionオブジェクトに関連するすべての情報を保持する。 このテーブルには、BATCH_JOB_EXECUTIONテーブルと多くの点で非常に類似しており、各JobExecutionが作られるごとに常にStepごとに少なくとも1つのエントリがある。

表 11. BATCH_STEP_EXECUTIONの定義
カラム名 | 説明
--- | ---
STEP_EXECUTION_ID | 一意にこのステップ実行を識別する主キー。
VERSION | バージョンを参照。
STEP_NAME | ステップの名前。
JOB_EXECUTION_ID | このStepExecutionが属するJobExecutionを示すBATCH_JOB_EXECUTIONテーブルからの外部キー。
START_TIME | ステップ実行が開始された時刻。
END_TIME | ステップ実行が成功または失敗に関係なく、終了した時刻を表す。 ジョブが現在実行されていないにもかかわらず、このカラムの値が空であることは、いくつかのエラータイプがあり、フレームワークが最後のセーブを実行できなかったことを示す。
STATUS | ステップ実行のステータスを表す文字列。BatchStatus列挙オブジェクトが出力する文字列である。
COMMIT_COUNT | トランザクションをコミットしている回数。
READ_COUNT | ItemReaderで読み込んだデータ件数。
FILTER_COUNT | ItemProcessorでフィルタリングしたデータ件数。
WRITE_COUNT | ItemWriterで書き込んだデータ件数。
READ_SKIP_COUNT | ItemReaderでスキップしたデータ件数。
WRITE_SKIP_COUNT | ItemWriterでスキップしたデータ件数。
PROCESS_SKIP_COUNT | ItemProcessorでスキップしたデータ件数。
ROLLBACK_COUNT | トランザクションをロールバックしている回数。
EXIT_CODE | ステップ実行の終了コードを表す文字列。 CommandLineJobRunnerによる起動の場合、これを数値に変換することができる。
EXIT_MESSAGE | ステップが終了した状態をより詳細に説明する文字列。 障害が発生した場合には、可能であればスタックトレースをできるだけ多く含む文字列となる場合がある。
LAST_UPDATED | このレコードのステップ実行が最後に更新された時刻。

BATCH_STEP_EXECUTION_CONTEXT

BATCH_STEP_EXECUTION_CONTEXTテーブルは、StepのExecutionContext に関連するすべての情報を保持する。 特定のステップ実行に必要とされるステップレベルのデータがすべて含まれている。 このデータは、ジョブが失敗した後で処理を再処理する際に取得しなければならない状態を表し、失敗したジョブが「処理を中断したところから始める」ことを可能にする。

表 12. BATCH_STEP_EXECUTION_CONTEXTの定義
カラム名 | 説明
--- | ---
STEP_EXECUTION_ID | このStepのExecutionContextが属するジョブ実行を示すBATCH_STEP_EXECUTIONテーブルからの外部キー。
SHORT_CONTEXT | SERIALIZED_CONTEXTの文字列表現。
SERIALIZED_CONTEXT | シリアライズされたコンテキスト全体。

###### 2.3.2.4.4. DDLスクリプト

Spring Batch CoreのJARファイルには、いくつかのデータベースプラットフォームに応じたリレーショナル表を作成するサンプルスクリプトが含まれている。 これらのスクリプトはそのまま使用、または必要に応じて追加のインデックスと制約を変更することができる。
スクリプトは、org.springframework.batch.coreのパッケージに含まれており、ファイル名は、`schema-*.sql`で形成されている。 "*"は、ターゲット・データベース・プラットフォームの短い名前である。

##### 2.3.2.5. 代表的な性能チューニングポイント

Spring Batchにおける代表的な性能チューニングポイントを説明する。

チャンクサイズの調整

  リソースへの出力によるオーバヘッドを抑えるために、チャンクサイズを大きくする。
  ただし、チャンクサイズを大きくしすぎるとリソース側の負荷が高くなりかえって性能が低下することがあるので、 適度なサイズになるように調整を行う。

フェッチサイズの調整

  リソースからの入力によるオーバヘッドを抑えるために、リソースに対するフェッチサイズ(バッファサイズ)を大きくする。

ファイル読み込みの効率化

  BeanWrapperFieldSetMapperを使用すると、Beanのクラスとプロパティ名を順番に指定するだけでレコードをBeanにマッピングしてくれる。 しかし、内部で複雑な処理を行うため時間がかかる。マッピングを行う専用の`FieldSetMapper`インタフェース実装を用いることで処理時間を短縮できる可能性がある。
  ファイル入出力の詳細は、"ファイルアクセス"を参照。

並列処理・多重処理

  Spring Batchでは、Step実行の並列化、データ分割による多重処理をサポートしている。並列化もしくは多重化を行い、処理を並列走行させることで性能を改善できる。 しかし、並列数および多重数を大きくしすぎるとリソース側の負荷が高くなりかえって性能が低下することがあるので、適度なサイズになるように調整を行う。
  並列処理・多重処理の詳細は、"並列処理と多重処理"を参照。

分散処理の検討

  Spring Batchでは、複数マシンでの分散処理もサポートしている。指針は、並列処理・多重処理と同様である。
  分散処理は、基盤設計や運用設計が複雑化するため、本ガイドラインでは説明を行わない。

### 2.4. Macchinetta Batch Framework (2.x)のアーキテクチャ

#### 2.4.1. 概要

Macchinetta Batch Framework (2.x)のアーキテクチャ全体像を説明する。

Macchinetta Batch Framework (2.x)では、"一般的なバッチ処理システム"で説明したとおり TERASOLUNA Batch Framework for Java (5.x)を中心としたOSSの組み合わせを利用して実現する。

TERASOLUNA Batch Framework for Java (5.x)の階層アーキテクチャを含めたMacchinetta Batch Framework (2.x)の構成概略図を以下に示す。

図 9. Macchinetta Batch Framework (2.x)の構成概略図
Spring Batchの階層アーキテクチャの説明

アプリケーション

  開発者によって書かれたすべてのジョブ定義およびビジネスロジック。

コア

  TERASOLUNA Batch Framework for Java (5.x) が提供するバッチジョブを起動し、制御するために必要なコア・ランタイム・クラス。

インフラストラクチャ

  TERASOLUNA Batch Framework for Java (5.x) が提供する開発者およびコアフレームワーク自体が利用する一般的なItemReader/ItemProcessor/ItemWriterの実装。

#### 2.4.2. ジョブの構成要素

ジョブの構成要素を説明するため、ジョブの構成概略図を下記に示す。

図 10. ジョブの構成概略図

この節では、ジョブとステップについて構成すべき粒度の指針も含めて説明をする。

##### 2.4.2.1. ジョブ

ジョブとは、バッチ処理全体をカプセル化するエンティティであり、ステップを格納するためのコンテナである。
1つのジョブは、1つ以上のステップで構成することができる。

ジョブの定義は、XMLによるBean定義ファイルに記述する。 ジョブ定義ファイルには複数のジョブを定義することができるが、ジョブの管理が煩雑になりやすくなる。

従って、Macchinetta Batch Framework (2.x)では以下の指針とする。

1ジョブ=1ジョブ定義ファイル

##### 2.4.2.2. ステップ

ステップとは、バッチ処理を制御するために必要な情報を定義したものである。 ステップにはチャンクモデルとタスクレットモデルを定義することができる。

チャンクモデル

  - ItemReader、ItemProcessor、およびItemWriterで構成される。

タスクレットモデル

  - Taskletだけで構成される。

チャンクモデル/タスクレットモデルの構成要素を実装したクラスは、`@Component`を付与してBean定義する。 "バッチ処理で考慮する原則と注意点"にあるとおり、 単一のバッチ処理では、可能な限り簡素化し、複雑な論理構造を避ける必要がある。

従って、Macchinetta Batch Framework (2.x)では以下の指針とする。

1ステップ=1バッチ処理=1ビジネスロジック

| チャンクモデルでのビジネスロジック分割 1つのビジネスロジックが複雑で規模が大きくなる場合、ビジネスロジックを分割することがある。 概略図を見るとわかるとおり、1つのステップには1つのItemProcessorしか設定できないため、ビジネスロジックの分割ができないように思える。 しかし、CompositeItemProcssorという複数のItemProcessorをまとめるItemProcessorがあり、 この実装を使うことでビジネスロジックを分割して実行することができる。

| Macchinetta Batch 2.xでのBean定義 Macchinetta Batch 2.xでのBean定義は、以下を前提とする。 ジョブの定義は、XMLによるBean定義ファイルに記述する チャンクモデル/タスクレットモデルの構成要素を実装したクラスは、`@Component`を付与してBean定義する

#### 2.4.3. ステップの実装方式

##### 2.4.3.1. チャンクモデル

チャンクモデルの定義と使用目的を説明する。

定義

  ItemReader、ItemProcessorおよびItemWriter実装とチャンク数をChunkOrientedTaskletに設定する。それぞれの役割を説明する。

  - ChunkOrientedTasklet･･･ItemReader/ItemProcessorを呼び出し、チャンクを作成する。作成したチャンクをItemWriterへ渡す。

  - ItemReader･･･入力データを読み込む。

  - ItemProcessor･･･読み込んだデータを加工する。

  - ItemWriter･･･加工されたデータをチャンク単位で出力する。 :: チャンクモデルの概要は、 "チャンクモデル" を参照。

使用目的

  一定件数のデータをまとめて処理を行うため、大量データを取り扱う場合に用いられる。

##### 2.4.3.2. タスクレットモデル

タスクレットモデルの定義と使用目的を説明する。

定義

  Tasklet実装だけを設定する。
  タスクレットモデルの概要は、 "タスクレットモデル" を参照。

使用目的

  システムコマンドの実行など、入出力を伴わない処理を実行するために用いられる。
  また、一括でデータをコミットしたい場合にも用いられる。

##### 2.4.3.3. チャンクモデルとタスクレットモデルの対比

チャンクモデルとタスクレットモデルの差異について説明する。 詳細については各機能の節を参照してもらい、ここでは概略のみにとどめる。

表 13. 処理モデルの対比表
項目 | チャンクモデル | タスクレットモデル
--- | --- | ---
構成要素 | ItemReader、ItemProcessor、ItemWriter、ChunkOrientedTaskletで構成される。 | Taskletのみで構成される。
トランザクション制御 | チャンク単位にトランザクションが発生する。トランザクション制御は一定件数ごとにトランザクションを確定する中間コミット方式のみ。 | 1トランザクションで処理する。トランザクション制御は、全件を1トランザクションで確定する一括コミット方式と中間コミット方式のいずれかを利用可能。 前者はSpring Batchが持つトランザクション制御の仕組みを利用するが、後者はユーザにてトランザクションを直接操作する。
処理の再実行 | リランおよび、ステートレスリスタート(件数ベースリスタート)、ステートフルリスタート(処理状態を判断したリスタート)が利用できる。 | リランのみ利用することを原則とする。処理状態を判断したリスタートが利用できる。
例外ハンドリング | Spring Batch提供の各種`Listener`インタフェースを使うことでハンドリング処理が容易になっている。try-catchによる独自実装も可能。 | タスクレット実装内にて独自にtry-catchを実装することが基本。`ChunkListener`インタフェースの利用も可能。

#### 2.4.4. ジョブの起動方式

ジョブの起動方式について説明する。ジョブの起動方式には以下のものがある。

- 同期実行方式

- 非同期実行方式

それぞれの起動方式について説明する。

##### 2.4.4.1. 同期実行方式

同期実行方式とは、ジョブを起動してからジョブが終了するまで起動元へ制御が戻らない実行方式である。

ジョブスケジューラからジョブを起動する概略図を示す。

図 11. 同期実行概略図

1. ジョブスケジューラからジョブを起動するためのシェルスクリプトを起動する。
   シェルスクリプトから終了コード(数値)が返却するまでジョブスケジューラは待機する。

2. シェルスクリプトからジョブを起動するために`CommandLineJobRunner`を起動する。
   `CommandLineJobRunner`から終了コード(数値)が返却するまでシェルスクリプトは待機する。

3. `CommandLineJobRunner`はジョブを起動する。ジョブは処理終了後に終了コード(文字列)を`CommandLineJobRunner`へ返却する。
   `CommandLineJobRunner`は、ジョブから返却された終了コード(文字列)から終了コード(数値)に変換してシェルスクリプトへ返却する。

##### 2.4.4.2. 非同期実行方式

非同期実行方式とは、起動元とは別の実行基盤(別スレッドなど)でジョブを実行することで、ジョブ起動後すぐに起動元へ制御が戻る方式である。 この方式の場合、ジョブの実行結果はジョブ起動とは別の手段で取得する必要がある。

Macchinetta Batch Framework (2.x)では、以下に示す2とおりの方法について説明をする。

- 非同期実行方式(DBポーリング)

- 非同期実行方式(Webコンテナ)

| その他の非同期実行方式 MQなどのメッセージを利用して非同期実行を実現することもできるが、ジョブ実行のポイントは同じであるため、Macchinetta Batch Framework (2.x)では説明は割愛する。

###### 2.4.4.2.1. 非同期実行方式(DBポーリング)

"非同期実行(DBポーリング)"とは、 ジョブ実行の要求をデータベースに登録し、その要求をポーリングして、ジョブを実行する方式である。

Macchinetta Batch Framework (2.x)で利用しているTERASOLUNA Batch Framework for Java (5.x)は、DBポーリング機能を提供している。提供しているDBポーリングによる起動の概略図を示す。

図 12. DBポーリング概略図

1. ユーザはデータベースへジョブ要求を登録する。

2. DBポーリング機能は、定期的にジョブ要求の登録を監視していて、登録されたことを検知すると該当するジョブを実行する。

   - SimpleJobOperatorからジョブを起動し、ジョブ終了後に`JobExecutionId`を受け取る。

   - JobExecutionIdとは、ジョブ実行を一意に識別するIDであり、このIDを使ってJobRepositoryから実行結果を参照する。

   - ジョブの実行結果は、Spring Batchの仕組みによって、JobRepositoryへ登録される。

   - DBポーリング自体が非同期で実行されている。

3. DBポーリング機能は、SimpleJobOperatorから返却されたJobExecutionIdとスタータスを起動したジョブ要求に対して更新を行う。

4. ジョブの処理経過・結果は、JobExecutionIdを利用して別途参照を行う。

###### 2.4.4.2.2. 非同期実行方式(Webコンテナ)

"非同期実行(Webコンテナ)"とは、 Webコンテナ上のWebアプリケーションへのリクエストを契機にジョブを非同期実行する方式である。 Webアプリケーションは、ジョブの終了を待たずに起動後すぐにレスポンスを返却することができる。

図 13. Webコンテナ概略図

1. クライアントからWebアプリケーションへリクエストを送信する。

2. Webアプリケーションは、リクエストから要求されたジョブを非同期実行する。

   - SimpleJobOperatorからジョブを起動直後に`JobExecutionId`を受け取る。

   - ジョブの実行結果は、Spring Batchの仕組みによって、JobRepositoryへ登録される。

3. Webアプリケーションは、ジョブの終了を待たずにクライアントへレスポンスを返信する。

4. ジョブの処理経過・結果は、JobExecutionIdを利用して別途参照を行う。

また、 Macchinetta Server Framework (1.x)で構築されるWebアプリケーションと連携することも可能である。

#### 2.4.5. 利用する際の検討ポイント

Macchinetta Batch Framework (2.x)を利用する際の検討ポイントを示す。

ジョブ起動方法
  同期実行方式

    スケジュールどおりにジョブを起動したり、複数のジョブを組み合わせてバッチ処理行う場合に利用する。

  非同期実行方式(DBポーリング)

    ディレード処理、処理時間が短いジョブの連続実行、大量ジョブの集約などに利用する。

  非同期実行方式(Webコンテナ)

    DBポーリングと同様だが、起動までの即時性が求められる場合にはこちらを利用する。

実装方式
  チャンクモデル

    大量データを効率よく処理したい場合に利用する。

  タスクレットモデル

    シンプルな処理や、定型化しにくい処理、データを一括で処理したい場合に利用する。

## 3. アプリケーション開発の流れ

### 3.1. バッチアプリケーションの開発

#### 3.1.1. バッチアプリケーションのBean定義の構成

Macchinetta Batch Framework (2.x)のバッチアプリケーションでは、Bean定義をもちいて各種設定を行う。

Spring Frameworkや、Spring Batchをはじめとする、様々なOSSを基盤としているフレームワークであるMacchinetta Batch Framework (2.x)は、Bean定義で設定できる機能も多岐に及び、これらのOSSに対する設定を行う、Bean定義も複雑なものになりやすい。

Macchinetta Batch Framework (2.x)では、Bean定義の複雑さを緩和し、管理が行いやすくなるように、役割ごとにBean定義ファイルを作成することを想定している。本ガイドラインで想定するBean定義の分類は以下の通りである。

表 14. 役割別のBean定義の分類
分類 | 役割 | 開発者のやること
--- | --- | ---
アプリケーション全体のBean定義 | アプリケーション全体で共通する設定をここにまとめる。これによってジョブ定義間の設定の重複を抑止する。 | カスタマイズ（必要な場合）
ジョブのBean定義 | 業務要件にもとづくバッチ処理の定義をここにまとめる。 | 新規作成
非同期バッチデーモンのBean定義 | 非同期処理をおこなう場合の定義をここにまとめる。 | カスタマイズ（必要な場合）

各Bean定義とBean定義ファイルの関係は、Bean定義の分類とBean定義ファイルの対応関係を参照のこと。

開発者がいちから作成するのはジョブのBean定義のみである。

ブランクプロジェクト（後述）では、アプリケーション全体のBean定義、非同期バッチデーモンのBean定義の定義ファイルを、初期設定済みの状態で提供する。これらは必要なときのみカスタマイズすればよい。

各Bean定義で記述する設定の詳細は、以降で説明する。

##### 3.1.1.1. アプリケーション全体のBean定義

代表的な設定内容としては主に以下がある。

- Macchinetta Batch Framework (2.x)を構成するOSSスタックの初期設定

  - Spring Batchの挙動設定（トランザクションの設定など）

  - MyBatis3の挙動設定（データソースからのフェッチサイズのチューニングなど）

- 上記以外の複数のジョブに共通する設定

  - 入力チェック用バリデータの設定

  - メッセージファイルの設定

このBean定義は、他のBean定義（ジョブのBean定義、非同期バッチデーモンのBean定義）から参照される。

詳細はアプリケーション全体の設定のこと。

##### 3.1.1.2. ジョブのBean定義

ジョブの構成要素（ジョブ、ステップ）を定義する。

詳細はジョブの作成を参照のこと。

##### 3.1.1.3. 非同期バッチデーモンのBean定義

非同期バッチデーモンの起動設定を行う。

Macchinetta Batch Framework (2.x)では、ジョブの非同期実行が可能である。テーブルに登録されているジョブの情報を監視し、非同期でジョブを起動するモジュールを非同期バッチデーモンという。

詳細は非同期実行(DBポーリング)を参照のこと。

#### 3.1.2. Bean定義の記述方法

Macchinetta Batch Framework (2.x)では、Bean定義の記述方法としてJavaConfig または XMLConfig のいずれかを選択することができる。

| JavaConfig/XMLConfigの説明の併記 以降、説明対象が同じであるがBean定義の媒体が異なる場合、JavaConfig/XMLConfigという形式で併記する。また、JavaConfig/XMLConfigで異なる説明が必要な箇所では、タブ切り替えを用いる。

Springの各種OSSにおけるリファレンスドキュメントでは、コード例の提示がJavaConfigしかないケースが増えているため、開発元からの正式なアナウンスは現状ないものの、今後はSpring全体として、XMLConfigよりもJavaConfigにより注力すると考えられる。

#### 3.1.3. Bean定義の分類とBean定義ファイルの対応関係

各Bean定義の分類と、ブランクプロジェクトのBean定義ファイルとの関係は以下の通り。

表 15. Bean定義の分類とBean定義ファイルの対応関係
Bean定義の分類 | Bean定義ファイル(JavaConfig) | Bean定義ファイル(XMLConfig)
--- | --- | ---
アプリケーション全体のBean定義 | JobBaseContextConfig.java LaunchContextConfig.java TerasolunaBatchConfiguration.java | job-base-context.xml launch-context.xml
ジョブのBean定義 （ジョブ毎にBean定義ファイル作成） | Job01Config.java | job01.xml
非同期バッチデーモンのBean定義 | AsyncBatchDaemonConfig.java | async-batch-daemon.xml

上表からも明らかなように、JavaConfigの`TerasolunaBatchConfiguration.java`に相当するBean定義ファイルがXMLConfigには存在しない。この理由は、Spring Batchから提供される、Bean定義をサポートする仕組み（`DefaultBatchConfiguration`）が、JavaConfig向けにしかないためである。

`DefaultBatchConfiguration`の詳細は、AppendixのDefaultBatchConfigurationを参照のこと。

Macchinetta Batch Framework (2.x)では、`DefaultBatchConfiguration`を直接使用せず、その継承クラス`TerasolunaBatchConfiguration`を使用する。これは、Macchinetta Batch Framework (2.x)の動作上、`DefaultBatchConfiguration`の一部のBean定義のカスタマイズが必須となるためである。

`TerasolunaBatchConfiguration`の詳細は、AppendixのTerasolunaBatchConfigurationを参照のこと。

#### 3.1.4. ブランクプロジェクトとは

ブランクプロジェクトとは、各Bean定義（アプリケーション全体のBean定義、ジョブのBean定義（サンプル）、非同期バッチデーモンのBean定義）をあらかじめ行った開発プロジェクトの雛形であり、 アプリケーション開発のスタート地点である。
本ガイドラインでは、シングルプロジェクト構成のブランクプロジェクトを提供する。
構成の説明については、プロジェクトの構成を参照。

| Macchinetta Server 1.xとの違い Macchinetta Server 1.xはマルチプロジェクト構成を推奨している。 この理由は主に、以下の様なメリットを享受するためである。 環境差分を吸収しやすくする ビジネスロジックとプレゼンテーションを分離しやすくする しかし、本ガイドラインではMacchinetta Server 1.xと異なりシングルプロジェクト構成としている。 これは、前述の点はバッチアプリケーションの場合においても考慮すべきだが、 シングルプロジェクト構成にすることで1ジョブに関連する資材を近づけることを優先している。 また、バッチアプリケーションの場合、 環境差分はプロパティファイルや環境変数で切替れば十分なケースが多いことも理由の1つである。

#### 3.1.5. プロジェクトの作成

`Maven Archetype Plugin`の`archetype:generate`を使用して、プロジェクトを作成する方法を説明する。

| 作成環境の前提について 以下を前提とし説明する。 Java SE Development Kit 17 Apache Maven 3.x インターネットに繋がっていること インターネットにプロキシ経由で繋ぐ場合は、Mavenのプロキシ設定 が行われていること IDE Spring Tool Suite / Eclipse 等

プロジェクトを作成するディレクトリにて、以下のコマンドを実行する。

JavaConfig

`archetypeArtifactId`に`macchinetta-batch-archetype`を指定

コマンドプロンプト(Windows)

    C:\xxx>mvn archetype:generate ^
      -DarchetypeGroupId=com.github.macchinetta.blank ^
      -DarchetypeArtifactId=macchinetta-batch-archetype ^
      -DarchetypeVersion=2.6.1.RELEASE

Bash(Unix, Linux, …​)

    $ mvn archetype:generate \
      -DarchetypeGroupId=com.github.macchinetta.blank \
      -DarchetypeArtifactId=macchinetta-batch-archetype \
      -DarchetypeVersion=2.6.1.RELEASE

XMLConfig

`archetypeArtifactId`に`macchinetta-batch-xmlconfig-archetype`を指定

コマンドプロンプト(Windows)

    C:\xxx>mvn archetype:generate ^
      -DarchetypeGroupId=com.github.macchinetta.blank ^
      -DarchetypeArtifactId=macchinetta-batch-xmlconfig-archetype ^
      -DarchetypeVersion=2.6.1.RELEASE

Bash(Unix, Linux, …​)

    $ mvn archetype:generate \
      -DarchetypeGroupId=com.github.macchinetta.blank \
      -DarchetypeArtifactId=macchinetta-batch-xmlconfig-archetype \
      -DarchetypeVersion=2.6.1.RELEASE

その後、利用者の状況に合わせて、以下を対話式に設定する。

- groupId

- artifactId

- version

- package

以下の値を設定し実行した例を示す。

表 16. ブランクプロジェクトの各要素の説明
項目名 | 設定例
--- | ---
groupId | com.example.batch
artifactId | batch
version | 1.0.0-SNAPSHOT
package | com.example.batch

コマンドプロンプトでの実行例

    C:\xxx>mvn archetype:generate ^
    More? -DarchetypeGroupId=com.github.macchinetta.blank ^
    More? -DarchetypeArtifactId=macchinetta-batch-archetype ^
    More? -DarchetypeVersion=2.6.1.RELEASE
    [INFO] Scanning for projects…​
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building Maven Stub Project (No POM) 1
    [INFO] ------------------------------------------------------------------------

    (.. omitted)

    Define value for property 'groupId': com.example.batch
    Define value for property 'artifactId': batch
    Define value for property 'version' 1.0-SNAPSHOT: : 1.0.0-SNAPSHOT
    Define value for property 'package' com.example.batch: :
    Confirm properties configuration:
    groupId: com.example.batch
    artifactId: batch
    version: 1.0.0-SNAPSHOT
    package: com.example.batch
     Y: : y
    [INFO] ------------------------------------------------------------------------
    [INFO] Using following parameters for creating project from Archetype: macchinetta-batch-archetype:2.6.1.RELEASE
    [INFO] ------------------------------------------------------------------------
    [INFO] Parameter: groupId, Value: com.example.batch
    [INFO] Parameter: artifactId, Value: batch
    [INFO] Parameter: version, Value: 1.0.0-SNAPSHOT
    [INFO] Parameter: package, Value: com.example.batch
    [INFO] Parameter: packageInPathFormat, Value: com/example/batch
    [INFO] Parameter: package, Value: com.example.batch
    [INFO] Parameter: version, Value: 1.0.0-SNAPSHOT
    [INFO] Parameter: groupId, Value: com.example.batch
    [INFO] Parameter: artifactId, Value: batch
    [INFO] Project created from Archetype in dir: C:\xxx\batch
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 02:02 min
    [INFO] Finished at: 2019-09-03T09:24:55+09:00
    [INFO] Final Memory: 13M/89M
    [INFO] ------------------------------------------------------------------------

Bashでの実行例

    $ mvn archetype:generate \
    > -DarchetypeGroupId=com.github.macchinetta.blank \
    > -DarchetypeArtifactId=macchinetta-batch-archetype \
    > -DarchetypeVersion=2.6.1.RELEASE
    [INFO] Scanning for projects…​
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building Maven Stub Project (No POM) 1
    [INFO] ------------------------------------------------------------------------

    (.. omitted)

    Define value for property 'groupId': com.example.batch
    Define value for property 'artifactId': batch
    Define value for property 'version' 1.0-SNAPSHOT: : 1.0.0-SNAPSHOT
    Define value for property 'package' com.example.batch: :
    Confirm properties configuration:
    groupId: com.example.batch
    artifactId: batch
    version: 1.0.0-SNAPSHOT
    package: com.example.batch
     Y: : y
    [INFO] ----------------------------------------------------------------------------
    [INFO] Using following parameters for creating project from Archetype: macchinetta-batch-archetype:2.6.1.RELEASE
    [INFO] ----------------------------------------------------------------------------
    [INFO] Parameter: groupId, Value: com.example.batch
    [INFO] Parameter: artifactId, Value: batch
    [INFO] Parameter: version, Value: 1.0.0-SNAPSHOT
    [INFO] Parameter: package, Value: com.example.batch
    [INFO] Parameter: packageInPathFormat, Value: com/example/batch
    [INFO] Parameter: package, Value: com.example.batch
    [INFO] Parameter: version, Value: 1.0.0-SNAPSHOT
    [INFO] Parameter: groupId, Value: com.example.batch
    [INFO] Parameter: artifactId, Value: batch
    [INFO] Project created from Archetype in dir: C:\xxx\batch
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 01:46 min
    [INFO] Finished at: 2019-09-03T02:39:57+00:00
    [INFO] Final Memory: 15M/179M
    [INFO] ------------------------------------------------------------------------

以上により、プロジェクトの作成が完了した。

正しく作成出来たかどうかは、ブランクプロジェクトに同梱されているサンプルジョブ(`job01`)を以下の要領で実行することで確認できる。

JavaConfig
コマンドプロンプトでの実行(正しく作成できたことの確認)

    C:\xxx>cd batch
    C:\xxx\batch>mvn clean dependency:copy-dependencies -DoutputDirectory=lib package
    C:\xxx\batch>java -cp "lib/*;target/*" ^
    org.springframework.batch.core.launch.support.CommandLineJobRunner ^
    com.example.batch.jobs.Job01Config job01

Bashでの実行(正しく作成できたことの確認)

    $ cd batch
    $ mvn clean dependency:copy-dependencies -DoutputDirectory=lib package
    $ java -cp 'lib/*:target/*' \
    org.springframework.batch.core.launch.support.CommandLineJobRunner \
    com.example.batch.jobs.Job01Config job01

以下が確認出来れば、プロジェクトの作成は成功である。

- `C:\xxx\batch\target`配下に`output.csv`が作成されていること。

- 標準出力に`following status: [COMPLETED]`が表示されていること（以下例）。

コマンドプロンプトでの出力例

    C:\xxx\batch>mvn clean dependency:copy-dependencies -DoutputDirectory=lib package
    [INFO] Scanning for projects...
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building Macchinetta Batch Framework (2.x) Blank Project 1.0.0-SNAPSHOT
    [INFO] ------------------------------------------------------------------------

    (.. omitted)

    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 56.497 s
    [INFO] Finished at: 2019-09-03T10:39:59+09:00
    [INFO] Final Memory: 25M/145M
    [INFO] ------------------------------------------------------------------------

    C:\xxx\batch>java -cp "lib/*;target/*" ^
    More? org.springframework.batch.core.launch.support.CommandLineJobRunner ^
    More? com.example.batch.jobs.Job01Config job01

    (.. omitted)

    [2019/09/03 10:41:24] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] launched with the following parameters: [{jsr_batch_run_id=1}]
    [2019/09/03 10:41:24] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [job01.step01]
    [2019/09/03 10:41:24] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] completed with the following parameters: [{jsr_batch_run_id=1}] and the following status: [COMPLETED]

Bashでの出力例

    $ mvn clean dependency:copy-dependencies -DoutputDirectory=lib package
    [INFO] Scanning for projects...
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building Macchinetta Batch Framework (2.x) Blank Project 1.0.0-SNAPSHOT
    [INFO] ------------------------------------------------------------------------

    (.. omitted)

    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 02:39 min
    [INFO] Finished at: 2019-09-03T02:43:01+00:00
    [INFO] Final Memory: 27M/189M
    [INFO] ------------------------------------------------------------------------

    $ java -cp 'lib/*:target/*' \
    > org.springframework.batch.core.launch.support.CommandLineJobRunner \
    > com.example.batch.jobs.Job01Config job01

    (.. omitted)

    [2019/09/03 02:43:11] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] launched with the following parameters: [{jsr_batch_run_id=1}]
    [2019/09/03 02:43:11] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [job01.step01]
    [2019/09/03 02:43:11] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] completed with the following parameters: [{jsr_batch_run_id=1}] and the following status: [COMPLETED]

XMLConfig
コマンドプロンプトでの実行(正しく作成できたことの確認)

    C:\xxx>cd batch
    C:\xxx\batch>mvn clean dependency:copy-dependencies -DoutputDirectory=lib package
    C:\xxx\batch>java -cp "lib/*;target/*" ^
    org.springframework.batch.core.launch.support.CommandLineJobRunner ^
    META-INF/jobs/job01.xml job01

Bashでの実行(正しく作成できたことの確認)

    $ cd batch
    $ mvn clean dependency:copy-dependencies -DoutputDirectory=lib package
    $ java -cp 'lib/*:target/*' \
    org.springframework.batch.core.launch.support.CommandLineJobRunner \
    META-INF/jobs/job01.xml job01

以下の出力が得られ、`C:\xxx\batch\target`配下に`output.csv`が作成されていれば、プロジェクトは正しく作成できている。

コマンドプロンプトでの出力例

    C:\xxx\batch>mvn clean dependency:copy-dependencies -DoutputDirectory=lib package
    [INFO] Scanning for projects...
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building Macchinetta Batch Framework (2.x) Blank Project 1.0.0-SNAPSHOT
    [INFO] ------------------------------------------------------------------------

    (.. omitted)

    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 56.497 s
    [INFO] Finished at: 2019-09-03T10:39:59+09:00
    [INFO] Final Memory: 25M/145M
    [INFO] ------------------------------------------------------------------------

    C:\xxx\batch>java -cp "lib/*;target/*" ^
    More? org.springframework.batch.core.launch.support.CommandLineJobRunner ^
    More? META-INF/jobs/job01.xml job01

    (.. omitted)

    [2019/09/03 10:41:24] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] launched with the following parameters: [{jsr_batch_run_id=1}]
    [2019/09/03 10:41:24] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [job01.step01]
    [2019/09/03 10:41:24] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] completed with the following parameters: [{jsr_batch_run_id=1}] and the following status: [COMPLETED]

Bashでの出力例

    $ mvn clean dependency:copy-dependencies -DoutputDirectory=lib package
    [INFO] Scanning for projects...
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building Macchinetta Batch Framework (2.x) Blank Project 1.0.0-SNAPSHOT
    [INFO] ------------------------------------------------------------------------

    (.. omitted)

    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 02:39 min
    [INFO] Finished at: 2019-09-03T02:43:01+00:00
    [INFO] Final Memory: 27M/189M
    [INFO] ------------------------------------------------------------------------

    $ java -cp 'lib/*:target/*' \
    > org.springframework.batch.core.launch.support.CommandLineJobRunner \
    > META-INF/jobs/job01.xml job01

    (.. omitted)

    [2019/09/03 02:43:11] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] launched with the following parameters: [{jsr_batch_run_id=1}]
    [2019/09/03 02:43:11] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [job01.step01]
    [2019/09/03 02:43:11] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] completed with the following parameters: [{jsr_batch_run_id=1}] and the following status: [COMPLETED]

#### 3.1.6. プロジェクトの構成

前述までで作成したプロジェクトの構成について説明する。 プロジェクトは、以下の点を考慮した構成となっている。

- 起動方式に依存しないジョブの実装を実現する

- Spring BatchやMyBatisといった各種設定の手間を省く

- 環境依存の切替を容易にする

以下に構成を示し、各要素について説明する。
(わかりやすさのため、前述の`mvn archetype:generate`実行時の出力をもとに説明する。)

JavaConfig
図 14. プロジェクトのディレクトリ構造

表 17. ブランクプロジェクトの各要素の説明
項番 | 説明
--- | ---
(1) | バッチアプリケーション全体の各種クラスを格納するrootパッケージ。
(2) | バッチアプリケーション全体に関わるBean定義ファイルを格納するディレクトリ。 Spring BatchやMyBatisの初期設定や、同期/非同期といった起動契機に依らずにジョブを起動するための設定を行っている。
(3) | 非同期実行(DBポーリング)機能に関連する設定を記述したBean定義ファイル。 Ch04～Ch08以外（commonとか）の新規クラスについて、Copyrightとsinceの修正が完了しました。レビューをお願いします。
(4) | ジョブ固有のBean定義ファイルにてimportすることで、各種設定を削減するためのBean定義ファイル。 これをimportすることで、ジョブは起動契機によるBean定義の差を吸収することが出来る。
(5) | Spring Batchの挙動や、ジョブ共通の設定に対するBean定義ファイル。
(6) | Spring Batchの挙動のうち、インフラストラクチャBeanに関するBean定義ファイル。
(7) | ジョブ定義ファイルから参照される各種クラスを格納するパッケージ。 ここには、DTO、TaskletやProcessorの実装、MyBatis3の`Mapper`インタフェースを格納する。 初期状態を参考にユーザにて自由にカスタムしてよいが、各ジョブで利用する資材をジョブごとにまとめて格納することで、 `@ComponentScan`や`@MapperScan`によるスキャン範囲を最小限にする構成が望ましい。 スキャン範囲の影響や設定方法ついてはMyBatis-Springの設定を参考にすること。
(8) | ジョブ定義ファイルを格納するディレクトリ。 階層構造はジョブ個別の設計に応じて構成する。(7)のパッケージ階層と合わせておくとよい。
(9) | バッチアプリケーション全体に関わる設定ファイルで、`LaunchContextConfig.java/launch-context.xml`から読み込まれる。 環境依存となるプロパティ値はここに記述する。 初期状態では、データベースの接続や、非同期実行に関する設定を記述している。 設定方法の詳細は、データベース関連の設定を参照のこと。
(10) | Logback(ログ出力)の設定ファイル。
(11) | BeanValidationを用いた入力チェックにて、エラーとなった際に表示するメッセージを定義する設定ファイル。 初期状態では、BeanValidationと、その実装であるHibernateValidatorのデフォルトメッセージを定義したうえで、 すべてコメントアウトしている。 この状態ではデフォルトメッセージを使うため、メッセージをカスタマイズしたい場合にのみ アンコメントし任意のメッセージに修正すること。 詳細は、入力チェックの表「主な比較一覧」＞「エラーメッセージの設定」を参照のこと。
(12) | MyBatis3の`Mapper`インタフェースの対となるMapper XMLファイル。
(13) | 主にログ出力時に用いるメッセージを定義するプロパティファイル。

また、各ファイルの関連図を以下に示す。

XMLConfig
図 15. プロジェクトのディレクトリ構造

表 18. ブランクプロジェクトの各要素の説明
項番 | 説明
--- | ---
(1) | バッチアプリケーション全体の各種クラスを格納するrootパッケージ。
(2) | バッチアプリケーション全体に関わるBean定義ファイルを格納するディレクトリ。 Spring BatchやMyBatisの初期設定や、同期/非同期といった起動契機に依らずにジョブを起動するための設定を行っている。
(3) | 非同期実行(DBポーリング)機能に関連する設定を記述したBean定義ファイル。
(4) | ジョブ固有のBean定義ファイルにてimportすることで、各種設定を削減するためのBean定義ファイル。 これをimportすることで、ジョブは起動契機によるBean定義の差を吸収することが出来る。
(5) | Spring Batchの挙動や、ジョブ共通の設定に対するBean定義ファイル。
(6) | ジョブ定義ファイルから参照される各種クラスを格納するパッケージ。 ここには、DTO、TaskletやProcessorの実装、MyBatis3の`Mapper`インタフェースを格納する。 初期状態を参考にユーザにて自由にカスタムしてよいが、各ジョブで利用する資材をジョブごとにまとめて格納することで、 `<context:component-scan>`や`<mybatis:scan>`によるスキャン範囲を最小限にする構成が望ましい。 スキャン範囲の影響や設定方法ついてはMyBatis-Springの設定を参考にすること。
(7) | バッチアプリケーション全体に関わる設定ファイルで、`LaunchContextConfig.java/launch-context.xml`から読み込まれる。 環境依存となるプロパティ値はここに記述する。 初期状態では、データベースの接続や、非同期実行に関する設定を記述している。 設定方法の詳細は、データベース関連の設定を参照のこと。
(8) | Logback(ログ出力)の設定ファイル。
(9) | BeanValidationを用いた入力チェックにて、エラーとなった際に表示するメッセージを定義する設定ファイル。 初期状態では、BeanValidationと、その実装であるHibernateValidatorのデフォルトメッセージを定義したうえで、 すべてコメントアウトしている。 この状態ではデフォルトメッセージを使うため、メッセージをカスタマイズしたい場合にのみ アンコメントし任意のメッセージに修正すること。 詳細は、入力チェックの表「主な比較一覧」＞「エラーメッセージの設定」を参照のこと。
(10) | MyBatis3の`Mapper`インタフェースの対となるMapper XMLファイル。
(11) | 主にログ出力時に用いるメッセージを定義するプロパティファイル。
(12) | ジョブ定義ファイルを格納するディレクトリ。 階層構造はジョブ個別の設計に応じて構成する。(6)のパッケージ階層と合わせておくとよい。

また、各ファイルの関連図を以下に示す。

#### 3.1.7. 開発の流れ

ジョブを開発する一連の流れについて説明する。
ここでは、詳細な説明ではなく、大まかな流れを把握することを主眼とする。

##### 3.1.7.1. IDEへの取り込み

生成したプロジェクトはMavenのプロジェクト構成に従っているため、 各種IDEによって、Mavenプロジェクトとしてimportする。
詳細な手順は割愛する。

##### 3.1.7.2. アプリケーション全体の設定

ブランクプロジェクトによってあらかじめ設定されているものは説明を割愛する。以下では、ユーザの状況に応じてカスタマイズする箇所について説明する。

- pom.xmlのプロジェクト情報

- データベース関連の設定

これら以外の設定をカスタマイズする方法については、個々の機能にて説明する。

###### 3.1.7.2.1. pom.xmlのプロジェクト情報

プロジェクトのPOMには以下の情報が仮の値で設定されているため、状況に応じて設定すること。

- プロジェクト名(name要素)

- プロジェクト説明(description要素)

- プロジェクトURL(url要素)

- プロジェクト創設年(inceptionYear要素)

- プロジェクトライセンス(licenses要素)

- プロジェクト組織(organization要素)

###### 3.1.7.2.2. データベース関連の設定

データベース関連の設定は複数箇所にあるため、それぞれを修正すること。

pom.xml

    <!-- (1) -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>

batch-application.properties

    # (2)
    # Admin DataSource settings.
    admin.jdbc.driver=org.h2.Driver
    admin.jdbc.url=jdbc:h2:mem:batch-admin;DB_CLOSE_DELAY=-1
    admin.jdbc.username=sa
    admin.jdbc.password=

    # (2)
    # Job DataSource settings.
    #jdbc.driver=org.postgresql.Driver
    #jdbc.url=jdbc:postgresql://localhost:5432/postgres
    #jdbc.username=postgres
    #jdbc.password=postgres
    jdbc.driver=org.h2.Driver
    jdbc.url=jdbc:h2:mem:batch;DB_CLOSE_DELAY=-1
    jdbc.username=sa
    jdbc.password=

    # (3)
    # Spring Batch schema initialize.
    data-source.initialize.enabled=true
    spring-batch.schema.script=classpath:org/springframework/batch/core/schema-h2.sql
    terasoluna-batch.commit.script=classpath:org/terasoluna/batch/async/db/schema-commit.sql

JavaConfig
com.example.batch.config.LaunchContextConfig.java

    // (3)
    @Bean
    public DataSourceInitializer dataSourceInitializer(@Qualifier("adminDataSource") DataSource adminDataSource,
                                                       @Value("${data-source.initialize.enabled:false}") boolean enabled,
                                                       @Value("${spring-batch.schema.script}") Resource script,
                                                       @Value("${terasoluna-batch.commit.script}") Resource commitScript) {
        final DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
        dataSourceInitializer.setDataSource(adminDataSource);
        dataSourceInitializer.setEnabled(enabled);
        ResourceDatabasePopulator resourceDatabasePopulator = new ResourceDatabasePopulator(script, commitScript);
        resourceDatabasePopulator.setContinueOnError(true);
        dataSourceInitializer.setDatabasePopulator(resourceDatabasePopulator);
        return dataSourceInitializer;
    }

    // (4)
    @Bean(destroyMethod = "close")
    public BasicDataSource adminDataSource(@Value("${admin.jdbc.driver}") String driverClassName,
                                           @Value("${admin.jdbc.url}") String url,
                                           @Value("${admin.jdbc.username}") String username,
                                           @Value("${admin.jdbc.password}") String password) {
        final BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setMaxTotal(10);
        dataSource.setMinIdle(1);
        dataSource.setMaxWait(Duration.ofMillis(5000L));
        dataSource.setDefaultAutoCommit(false);
        return dataSource;
    }

    // (4)
    @Bean(destroyMethod = "close")
    public BasicDataSource jobDataSource(@Value("${jdbc.driver}") String driverClassName,
                                         @Value("${jdbc.url}") String url,
                                         @Value("${jdbc.username}") String username,
                                         @Value("${jdbc.password}") String password) {
        final BasicDataSource basicDataSource = new BasicDataSource();
        basicDataSource.setDriverClassName(driverClassName);
        basicDataSource.setUrl(url);
        basicDataSource.setUsername(username);
        basicDataSource.setPassword(password);
        basicDataSource.setMaxTotal(10);
        basicDataSource.setMinIdle(1);
        basicDataSource.setMaxWait(Duration.ofMillis(5000L));
        basicDataSource.setDefaultAutoCommit(false);
        return basicDataSource;
    }

    // (5)
    @Bean
    public SqlSessionFactory jobSqlSessionFactory(@Qualifier("jobDataSource") DataSource jobDataSource) throws Exception {
        final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(jobDataSource);

        final org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setLocalCacheScope(LocalCacheScope.STATEMENT);
        configuration.setLazyLoadingEnabled(true);
        configuration.setAggressiveLazyLoading(false);
        configuration.setDefaultFetchSize(1000);
        configuration.setDefaultExecutorType(ExecutorType.REUSE);

        sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }

XMLConfig
META-INF/spring/launch-context.xml

    <!-- (3) -->
    <jdbc:initialize-database data-source="adminDataSource"
                              enabled="${data-source.initialize.enabled:false}"
                              ignore-failures="ALL">
        <jdbc:script location="${spring-batch.schema.script}" />
        <jdbc:script location="${terasoluna-batch.commit.script}" />
    </jdbc:initialize-database>

    <!-- (4) -->
    <bean id="adminDataSource" class="org.apache.commons.dbcp2.BasicDataSource"
          destroy-method="close"
          p:driverClassName="${admin.jdbc.driver}"
          p:url="${admin.jdbc.url}"
          p:username="${admin.jdbc.username}"
          p:password="${admin.jdbc.password}"
          p:maxTotal="10"
          p:minIdle="1"
          p:defaultAutoCommit="false">
        <property name="maxWait">
            <bean class="java.time.Duration" factory-method="ofMillis">
                <constructor-arg name="millis" type="long" value="5000" />
            </bean>
        </property>
    </bean>

    <!-- (4) -->
    <bean id="jobDataSource" class="org.apache.commons.dbcp2.BasicDataSource"
          destroy-method="close"
          p:driverClassName="${jdbc.driver}"
          p:url="${jdbc.url}"
          p:username="${jdbc.username}"
          p:password="${jdbc.password}"
          p:maxTotal="10"
          p:minIdle="1"
          p:defaultAutoCommit="false">
        <property name="maxWait">
            <bean class="java.time.Duration" factory-method="ofMillis">
                <constructor-arg name="millis" type="long" value="5000" />
            </bean>
        </property>
    </bean>

    <!-- (5) -->
    <bean id="jobSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"
          p:dataSource-ref="jobDataSource" >
        <property name="configuration">
            <bean class="org.apache.ibatis.session.Configuration"
                p:localCacheScope="STATEMENT"
                p:lazyLoadingEnabled="true"
                p:aggressiveLazyLoading="false"
                p:defaultFetchSize="1000"
                p:defaultExecutorType="REUSE" />
        </property>
    </bean>

JavaConfig
com.example.batch.config.AsyncBatchDaemonConfig.java

    // (5)
    @Bean
    public SqlSessionFactory adminSqlSessionFactory(@Qualifier("adminDataSource") DataSource adminDataSource,
                                                    DatabaseIdProvider databaseIdProvider) throws Exception {
        final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(adminDataSource);
        sqlSessionFactoryBean.setDatabaseIdProvider(databaseIdProvider);
        final org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setLocalCacheScope(LocalCacheScope.STATEMENT);
        configuration.setLazyLoadingEnabled(true);
        configuration.setAggressiveLazyLoading(false);
        configuration.setDefaultFetchSize(1000);
        configuration.setDefaultExecutorType(ExecutorType.REUSE);
        sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }

XMLConfig
META-INF/spring/async-batch-daemon.xml

    <!-- (5) -->
    <bean id="adminSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"
          p:dataSource-ref="adminDataSource" >
        <property name="configuration">
            <bean class="org.apache.ibatis.session.Configuration"
                  p:localCacheScope="STATEMENT"
                  p:lazyLoadingEnabled="true"
                  p:aggressiveLazyLoading="false"
                  p:defaultFetchSize="1000"
                  p:defaultExecutorType="REUSE" />
        </property>
    </bean>

表 19. データベース関連の設定における各要素の説明
項番 | 説明
--- | ---
(1) | pom.xmlでは利用するデータベースへの接続に使用するJDBCドライバの依存関係を定義する。 初期状態ではH2 Database(インメモリデータベース)とPostgreSQLが設定されているが、必要に応じて追加削除を行うこと。
(2) | JDBCドライバの接続設定をする。 - `admin.jdbc.xxx`はSpring BatchやMacchinetta Batch 2.xが利用する - `jdbc.xxx～`はジョブ個別が利用する
(3) | Spring BatchやMacchinetta Batch 2.xが利用するデータベースの初期化処理を実行するか否か、および、利用するスクリプトを定義する。 Spring BatchはJobRepositoryにアクセスするため、データベースが必須となる。 また、Macchinetta Batch 2.xは非同期実行(DBポーリング)にてジョブ要求テーブルにアクセスするため、 データベースが必須となる。 有効にするか否かは、以下を基準とするとよい。 - H2 Databaseを利用する場合は有効にする。無効にするとJobRepositoryやジョブ要求テーブルにアクセスできずエラーになる。 - H2 Databaseを利用しない場合は事故を予防するために無効にする。
(4) | データソースの設定をする。 必要に応じて接続数等をチューニングする。
(5) | MyBatisの挙動を設定する。 必要に応じてフェッチサイズ等をチューニングする。

#### 3.1.8. ジョブの作成

ジョブは、ジョブのBean定義ファイルと、そこから参照されるコンポーネントで構成される。

基本的な実装方針を下表にまとめた。

| JavaConfig | XMLConfig
--- | --- | ---
ジョブのBean定義 | `@Bean`を付与したメソッドで定義する。メソッド内で`JobBuilder`を使用する。 | `<batch:job>`で定義する。
ステップのBean定義 | `@Bean`を付与したメソッドで定義する。メソッド内で`StepBuilder`を使用する。 | `<batch:job>`の内側で`<batch:step>`を定義する。
タスクレットの定義 （タスクレットモデル） | `@Component`をつけたクラスとして実装し、ジョブのBean定義ファイルから`@ComponentScan`で読み込む。 | `@Component`をつけたクラスとして実装し、ジョブのBean定義ファイルから`<context:component-scan>`で読み込む。
ItemReader/ItemWriter （チャンクモデル） | `@Bean`を付与したメソッドで定義する。 | `<bean>`で定義する。
ItemProcessor （チャンクモデル） | `@Component`をつけたクラスとして実装し、ジョブのBean定義ファイルから`@ComponentScan`で読み込む。 | `@Component`をつけたクラスとして実装し、ジョブのBean定義ファイルから`<context:component-scan>`で読み込む。
Bean名（BeanID）の決定 | `@Bean`を付与したメソッドのメソッド名 | `<bean>`、`<batch:job>`、`<batch:step>`等の`id`属性に指定した名前
Beanのインジェクション方法 | メソッドインジェクションを使用する | `<bean>`の`p`、`c`ネームスペースや、`<batch:job>`、`<batch:step>`等がもつそれぞれの属性にBean名を指定する。

| JavaConfigでメソッドインジェクションを使用する理由 JavaConfigでは同じBean定義クラス内に様々なスコープのBeanを定義することができる。一方でコンストラクタやフィールドインジェクションを使用した場合、JavaConfigクラスのインスタンス生成時に行われるただ一度のインジェクションで固定されてしまうため、`step` スコープなどのように、シングルトンではないBeanのインジェクションには対応できなくなる。 このため、JavaConfigでBeanのインジェクションを行う場合、`@Bean`アノテーションが付与されたメソッドの引数を使用し、コンストラクタやフィールドに対するインジェクションを使用しないこと。

ネームスペースについては、以下の公式ドキュメントを参照のこと。

- p-namespaceを使用したXMLショートカット

- c-namespaceを使用したXMLショートカット

より詳細な作成方法の説明は、以下を参照。

- チャンクモデルジョブの作成

- タスクレットモデルジョブの作成

##### 3.1.8.1. 同じ型をもつBeanが複数ある場合は@Qualifierアノテーションを付与する（JavaConfigのみ）

JavaConfigでは、デフォルトではBeanの特定は型名で行われる。そのため、同じ型をもつBeanが複数定義されていると、Beanが特定できずエラーとなる。エラーを回避するために、一意なBeanIDを明示する`@Qualifier`アノテーションを付与する。

なお、Macchinetta Batch 2.xでは、次に紹介するケースで`@Qualifier`アノテーションの付与が必須となる。

###### 3.1.8.1.1. データソース、トランザクションマネージャをインジェクションするとき

Macchinetta Batch 2.xでは、データソース、トランザクションマネージャは、同じ型をもつ複数のBean定義が存在する。そのため、これらのBeanをインジェクションするときは、`@Qualifier`アノテーションでBeanIDを指定しBeanを特定する必要がある。

具体的には以下のBeanが対象となる。

BeanID | 用途 | 型
--- | --- | ---
`adminDataSource` | Spring BatchやMacchinetta Batch 2.xが利用するデータソース | `org.apache.commons.dbcp2.BasicDataSource`
`jobDataSource` | ジョブが利用するデータソース
`adminTransactionManager` | Spring BatchやMacchinetta Batch 2.xが利用するトランザクションマネージャ | `org.springframework.transaction.PlatformTransactionManager`
`jobTransactionManager` | ジョブが利用するトランザクションマネージャ

以下はジョブ定義で`jobTransactionManager`を`@Qualifier`アノテーション付きでインジェクションする例となる。

    @Bean
    public Step exampleStep(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager, // (1)
                       ItemReader reader,
                       ItemProcessor processor,
                       ItemWriter writer) {
        return new StepBuilder("exampleJob.exampleStep",
                jobRepository)
                .<ItemReader, ItemWriter> chunk(10, transactionManager)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .build();
    }

表 20. jobTransactionManagerのインジェクション
項番 | 説明
--- | ---
(1) | `Bean`メソッドの引数`transactionManager`に`@Qualifier`アノテーションを付与する。その際、BeanID（`jobTransactionManager`）を指定する。

###### 3.1.8.1.2. ジョブのフロー制御や並列処理で複数のステップを定義するとき

フロー制御や並列処理では複数のステップを定義するため、型（`org.springframework.batch.core.Step`）が重複する。ジョブのBean定義にステップをインジェクションする際に、`@Qualifier`アノテーションを付与する。

以下は複数のステップを`@Qualifier`アノテーション付きでインジェクションする例となる。

    @Bean
    public Step stepA(JobRepository jobRepository,
                      SequentialFlowTasklet tasklet,
                      ChangeExitCodeReturnListener listener,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
      // omitted.
    }

    @Bean
    public Step stepB(JobRepository jobRepository,
                      SequentialFlowTasklet tasklet,
                      ChangeExitCodeReturnListener listener,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
      // omitted.
    }

    @Bean
    public Step stepC(JobRepository jobRepository,
                      SequentialFlowTasklet tasklet,
                      ChangeExitCodeReturnListener listener,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
      // omitted.
    }

    @Bean
    public Job jobConditionalFlow(JobRepository jobRepository,
                                  @Qualifier("stepA") Step stepA, // (1)
                                  @Qualifier("stepB") Step stepB, // (2)
                                  @Qualifier("stepC") Step stepC) { // (3)
      // omitted.
    }

表 21. jobTransactionManagerのインジェクション
項番 | 説明
--- | ---
(1) | `Bean`メソッドの引数`stepA`に`@Qualifier`アノテーションを付与する。その際、BeanID（`stepA`）を指定する。
(2) | `Bean`メソッドの引数`stepB`に`@Qualifier`アノテーションを付与する。その際、BeanID（`stepB`）を指定する。
(3) | `Bean`メソッドの引数`stepC`に`@Qualifier`アノテーションを付与する。その際、BeanID（`stepC`）を指定する。

#### 3.1.9. プロジェクトのビルドと実行

プロジェクトのビルドと実行について説明する。

##### 3.1.9.1. アプリケーションのビルド

プロジェクトのルートディレクトリに移動し、以下のコマンドを発行する。

ビルド(Windows/Bash)

    $ mvn clean dependency:copy-dependencies -DoutputDirectory=lib package
    [INFO] Scanning for projects...
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building Macchinetta Batch Framework (2.x) Blank Project 1.0.0-SNAPSHOT
    [INFO] ------------------------------------------------------------------------

    (.. omitted)

    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 02:39 min
    [INFO] Finished at: 2019-09-03T02:43:01+00:00
    [INFO] Final Memory: 27M/189M
    [INFO] ------------------------------------------------------------------------

これにより、以下が生成される。

- <ルートディレクトリ>/target/[artifactId]-[version].jar

  - 作成したバッチアプリケーションのJarが生成される

- <ルートディレクトリ>/lib/(依存Jarファイル)

  - 依存するJarファイル一式がコピーされる

試験環境や商用環境へ配備する際は、これらのJarファイルを任意のディレクトリにコピーすればよい。

###### 3.1.9.1.1. 環境に応じた設定ファイルの切替

プロジェクトのpom.xmlでは、初期値として以下のProfileを設定している。

pom.xmlのProfiles設定

    <profiles>
        <!-- Including application properties and log settings into package. (default) -->
        <profile>
            <id>IncludeSettings</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <exclude-property/>
                <exclude-log/>
            </properties>
        </profile>

        <!-- Excluding application properties and log settings into package. -->
        <profile>
            <id>ExcludeSettings</id>
            <activation>
                <activeByDefault>false</activeByDefault>
            </activation>
            <properties>
                <exclude-property>batch-application.properties</exclude-property>
                <exclude-log>logback.xml</exclude-log>
            </properties>
        </profile>
    </profiles>

ここでは、`環境依存となる設定ファイルを含めるかどうか`を切替ている。 この設定を活用して、環境配備の際に設定ファイルを別途配置することで環境差分を吸収することができる。 また、これを応用して、試験環境と商用環境でJarに含める設定ファイルを変えることもできる。 以下に、一例を示す。

環境ごとに設定ファイルを切替えるpom.xmlの記述例

    <dependencies>
        <!-- omitted -->
        <dependency>
            <groupId>${jdbc.driver.groupId}</groupId>
            <artifactId>${jdbc.driver.artifactId}</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <!-- omitted -->

    <profiles>
        <!-- omitted -->
        <profile>
            <id>postgresql12-local</id>
            <properties>
                <jdbc.driver.groupId>org.postgresql</jdbc.driver.groupId>
                <jdbc.driver.artifactId>postgresql</jdbc.driver.artifactId>
                <!-- omitted -->
            </properties>
        </profile>
        <!-- omitted -->
        <profile>
            <id>example-db-local</id>
            <properties>
                <jdbc.driver.groupId>com.example.database.jdbc</jdbc.driver.groupId>
                <jdbc.driver.artifactId>jdbc-driver</jdbc.driver.artifactId>
                <!-- omitted -->
            </properties>
        </profile>
    </profiles>

なお、MavenのProfileは以下の要領で、コマンド実行時に有効化することができる。
必要に応じて、複数Profileを有効化することもできる。必要に応じて、有効活用してほしい。

MavenのProfileを有効化する例

    $ mvn -P profile-1,profile-2

| LocalVariableTableParameterNameDiscovererの廃止について `Spring Framework` 6.1から、`LocalVariableTableParameterNameDiscoverer`が削除された。 `LocalVariableTableParameterNameDiscoverer`は、`Spring Framework`内部で、リフレクションを用いたメソッドパラメータ名を取得する処理で用いられていた。 アプリケーションで`LocalVariableTableParameterNameDiscoverer`を用いてメソッドパラメータ名を取得する処理を実装している場合は、以下の対応が必要となる。 `LocalVariableTableParameterNameDiscoverer`のかわりに`StandardReflectionParameterNameDiscoverer`を利用する。 `maven-compiler-plugin`の`<configuration>`に`<parameters>true</parameters>`を追加する。 pom.xmlの記述例
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>${maven-compiler-plugin.version}</version>
        <configuration>
            <source>${java-version}</source>
            <target>${java-version}</target>
            <parameters>true</parameters> // 追加
        </configuration>
    </plugin>

##### 3.1.9.2. アプリケーションの実行

前段でビルドした結果をもとに、ジョブを実行する例を示す。
`[artifactId]`と`[version]`はプロジェクトの作成にて設定したものに、ユーザに応じて読み替えてほしい。

JavaConfig
コマンドプロンプト(Windows)

    C:\xxx>java -cp "target\[artifactId]-[version].jar;lib\*" ^
    org.springframework.batch.core.launch.support.CommandLineJobRunner ^
    com.example.batch.jobs.Job01Config job01

    (.. omitted)
    [2019/09/03 10:41:24] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] launched with the following parameters: [{jsr_batch_run_id=1}]
    [2019/09/03 10:41:24] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [job01.step01]
    [2019/09/03 10:41:24] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] completed with the following parameters: [{jsr_batch_run_id=1}] and the following status: [COMPLETED]

Bash(Unix, Linux, …​)

    $ java -cp 'target/[artifactId]-[version].jar:lib/*' \
    org.springframework.batch.core.launch.support.CommandLineJobRunner \
    com.example.batch.jobs.Job01Config job01

    (.. omitted)
    [2019/09/03 02:43:11] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] launched with the following parameters: [{jsr_batch_run_id=1}]
    [2019/09/03 02:43:11] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [job01.step01]
    [2019/09/03 02:43:11] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] completed with the following parameters: [{jsr_batch_run_id=1}] and the following status: [COMPLETED]

XMLConfig
コマンドプロンプト(Windows)

    C:\xxx>java -cp "target\[artifactId]-[version].jar;lib\*" ^
    org.springframework.batch.core.launch.support.CommandLineJobRunner ^
    META-INF/jobs/job01.xml job01

    (.. omitted)
    [2019/09/03 10:41:24] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] launched with the following parameters: [{jsr_batch_run_id=1}]
    [2019/09/03 10:41:24] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [job01.step01]
    [2019/09/03 10:41:24] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] completed with the following parameters: [{jsr_batch_run_id=1}] and the following status: [COMPLETED]

Bash(Unix, Linux, …​)

    $ java -cp 'target/[artifactId]-[version].jar:lib/*' \
    org.springframework.batch.core.launch.support.CommandLineJobRunner \
    META-INF/jobs/job01.xml job01

    (.. omitted)
    [2019/09/03 02:43:11] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] launched with the following parameters: [{jsr_batch_run_id=1}]
    [2019/09/03 02:43:11] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [job01.step01]
    [2019/09/03 02:43:11] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] completed with the following parameters: [{jsr_batch_run_id=1}] and the following status: [COMPLETED]

これにより、<ルートディレクトリ>/target/output.csvが生成される。

| javaコマンドが返却する終了コードをハンドリングする必要性 実際のシステムでは、 ジョブスケジューラからジョブを発行する際にjavaコマンドを直接発行するのではなく、 java起動用のシェルスクリプトを挟んで起動することが一般的である。 これはjavaコマンド起動前の環境変数を設定するためや、javaコマンドの終了コードをハンドリングするためである。 この、`javaコマンドの終了コードのハンドリング`は、以下を理由に常に行うことを推奨する。 javaコマンドの終了コードは正常:`0`、異常:`1`であるが、ジョブスケジューラはジョブの成功/失敗を終了コードの範囲で判断する。 そのため、ジョブスケジューラの設定によっては、javaコマンドは異常終了したのにもかかわらずジョブスケジューラは正常終了したと判断してしまう。 OSやジョブスケジューラが扱うことができる終了コードは有限の範囲である。 OSやジョブスケジューラの仕様に応じて、ユーザにて使用する終了コードの範囲を定義することが重要である。 一般的に、POSIX標準で策定されている0から255の間に収めることが多い。 ブランクプロジェクトでは、正常:`0`、それ以外:`255`として終了コードを返却するよう設定している。 以下に、終了コードのハンドリング例を示す。 終了コードのハンドリング例
    #!/bin/bash

    # ..omitted.

    java -cp ...
    RETURN_CODE=$?
    if [ $RETURN_CODE = 1 ]; then
       return 255
    else
       return $RETURN_CODE
    fi

#### 3.1.10. Appendix

##### 3.1.10.1. DefaultBatchConfiguration

`org.springframework.batch.core.configuration.support.DefaultBatchConfiguration`は、Spring Batchにより、JavaConfig向けにのみ提供されるConfigurationクラスである。

詳しくは、Spring Batchの公式リファレンス（以下）を参照のこと。

- クラス DefaultBatchConfiguration

`DefaultBatchConfiguration`は、Spring Batchの動作に必要ないくつかのBean定義（以下）を、あらかじめ設定済みにして提供する。

- `JobRepository`

- `JobExplorer`

- `JobLauncher`

- `JobRegistry`

- `JobOperator`

`DefaultBatchConfiguration`に相当する仕組みは、XMLConfig向けには提供されない。そのためXMLConfigでは、上述の5つのBean定義は、`launch-context.xml`に記述している。

##### 3.1.10.2. TerasolunaBatchConfiguration

`TerasolunaBatchConfiguration`は、`DefaultBatchConfiguration`のBean定義をカスタマイズするために用意されている。Bean定義のカスタマイズは、`DefaultBatchConfiguration`の`Bean`メソッドをオーバーライドすることで実現する。

`TerasolunaBatchConfiguration`でカスタマイズするBeanと、カスタマイズ内容は以下となる。

- `JobRepository`

  - トランザクション分離レベルを`SERIALIZABLE`から`READ COMMITTED`に変更

- `JobOperator`

  - `JobParametersConverter`の実装クラスを、`org.springframework.batch.core.converter.DefaultJobParametersConverter`から`org.terasoluna.batch.converter.JobParametersConverterImpl`に変更

以下は、`JobRepository`と`JobOperator`のカスタマイズ箇所のコードとなる。

TerasolunaBatchConfiguration.java

    // omitted.
    @Override
    @Bean
    public JobRepository jobRepository() throws BatchConfigurationException {
        JobRepositoryFactoryBean jobRepositoryFactoryBean = new JobRepositoryFactoryBean();
        jobRepositoryFactoryBean.setDataSource(
                getDataSource());
        jobRepositoryFactoryBean.setTransactionManager(
                getTransactionManager());
        jobRepositoryFactoryBean.setIncrementerFactory(getIncrementerFactory());
        jobRepositoryFactoryBean.setClobType(getClobType());
        jobRepositoryFactoryBean.setTablePrefix(getTablePrefix());
        jobRepositoryFactoryBean.setSerializer(getExecutionContextSerializer());
        jobRepositoryFactoryBean.setConversionService(getConversionService());
        jobRepositoryFactoryBean.setJdbcOperations(getJdbcOperations());
        jobRepositoryFactoryBean.setCharset(getCharset());
        jobRepositoryFactoryBean.setMaxVarCharLength(getMaxVarCharLength());
        jobRepositoryFactoryBean.setIsolationLevelForCreateEnum(
                Isolation.READ_COMMITTED); // (1)
        jobRepositoryFactoryBean.setValidateTransactionState(
                getValidateTransactionState());

         try {
            jobRepositoryFactoryBean.setDatabaseType(getDatabaseType());
            jobRepositoryFactoryBean.afterPropertiesSet();
            return jobRepositoryFactoryBean.getObject();

        } catch (BatchConfigurationException e) {
            throw e;
        } catch (Exception e) {
            throw new BatchConfigurationException(
                    "Unable to configure the customized job repository", e);
        }
    }
    // omitted.
    @Override
    @Bean
    public JobOperator jobOperator(JobRepository jobRepository, JobExplorer jobExplorer, JobRegistry jobRegistry, JobLauncher jobLauncher) throws BatchConfigurationException {
        JobOperatorFactoryBean jobOperatorFactoryBean = new JobOperatorFactoryBean();
        jobOperatorFactoryBean.setTransactionManager(this.getTransactionManager());
        jobOperatorFactoryBean.setJobRepository(jobRepository);
        jobOperatorFactoryBean.setJobExplorer(jobExplorer);
        jobOperatorFactoryBean.setJobRegistry(jobRegistry);
        jobOperatorFactoryBean.setJobLauncher(jobLauncher);
        JobParametersConverterImpl jobParametersConverter = new JobParametersConverterImpl(
                getDataSource()); // (2)
        jobOperatorFactoryBean.setJobParametersConverter(
                jobParametersConverter);

        try {
            jobParametersConverter.afterPropertiesSet();
            jobOperatorFactoryBean.afterPropertiesSet();
            return jobOperatorFactoryBean.getObject();

        } catch (BatchConfigurationException e) {
            throw e;
        } catch (Exception e) {
            throw new BatchConfigurationException(
                    "Unable to configure the customized job operator", e);
        }
    }
    // omitted.

項番 | 説明
--- | ---
(1) | トランザクション分離レベルを`READ COMMITTED`に変更する。
(2) | `JobParametersConverter`の実装クラスを`JobParametersConverterImpl`に変更する。

### 3.2. チャンクモデルジョブの作成

#### 3.2.1. Overview

チャンクモデルジョブの作成方法について説明する。 チャンクモデルのアーキテクチャについては、Spring Batchのアーキテクチャを参照。

ここでは、チャンクモデルジョブの構成要素について説明する。

##### 3.2.1.1. 構成要素

チャンクモデルジョブの構成要素を以下に示す。 これらの構成要素をBean定義にて組み合わせることで1つのジョブを実現する。

表 22. チャンクモデルジョブの構成要素
項番 | 名称 | 役割 | 設定必須 | 実装必須
--- | --- | --- | --- | ---
1 | ItemReader | 様々なリソースからデータを取得するためのインタフェース。 Spring Batchにより、フラットファイルや データベースを対象とした実装が提供されているため、ユーザにて作成する必要はない。 |  |
2 | ItemProcessor | 入力から出力へデータを加工するためのインタフェース。 ユーザは必要に応じてこのインタフェースを`implements`し、ビジネスロジックを実装する。 |  |
3 | ItemWriter | 様々なリソースへデータを出力するためのインタフェース。 `ItemReader`と対になるインタフェースと考えてよい。 Spring Batchにより、フラットファイルや データベースのための実装が提供されているため、ユーザにて作成する必要はない。 |  |

この表のポイントは以下である。

- 入力リソースから出力リソースへ単純にデータを移し替えるだけであれば、設定のみで実現できる。

- `ItemProcessor`は、必要が生じた際にのみ実装すればよい。

以降、これらの構成要素を用いたジョブの実装方法について説明する。

#### 3.2.2. How to use

ここでは、実際にチャンクモデルジョブを実装する方法について、以下の順序で説明する。

- ジョブの設定

- コンポーネントの実装

##### 3.2.2.1. ジョブの設定

Bean定義ファイルにて、チャンクモデルジョブを構成する要素の組み合わせ方を定義する。 以下に例を示し、構成要素の繋がりを説明する。

JavaConfig
Bean定義ファイルの例(チャンクモデル)

    @Configuration
    @Import(JobBaseContextConfig.class) // (1)
    @ComponentScan("jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common") // (2)
    @MapperScan(value = "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.mst", sqlSessionFactoryRef = "jobSqlSessionFactory") // (3)
    public class JobCustomerList01Config {

        // (4)
        @Bean
        @StepScope
        public MyBatisCursorItemReader<Customer> reader(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory) {
            return new MyBatisCursorItemReaderBuilder<Customer>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .queryId(
                            "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.mst.CustomerRepository.findAll")
                    .build();
        }

        // (5)
        // Item Processor
        // Item Processor in order that based on the Bean defined by the annotations, not defined here

        // (6)
        @Bean
        @StepScope
        public FlatFileItemWriter<Customer> writer(
                @Value("#{jobParameters['outputFile']}") File outputFile) {
            final BeanWrapperFieldExtractor<Customer> fieldExtractor = new BeanWrapperFieldExtractor<>();
            fieldExtractor.setNames(
                    new String[] { "customerId", "customerName", "customerAddress",
                            "customerTel", "chargeBranchId" });
            final DelimitedLineAggregator<Customer> lineAggregator = new DelimitedLineAggregator<>();
            lineAggregator.setDelimiter(",");
            lineAggregator.setFieldExtractor(fieldExtractor);
            return new FlatFileItemWriterBuilder<Customer>()
                    .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                    .resource(new FileSystemResource(outputFile))
                    .transactional(false)
                    .lineAggregator(lineAggregator)
                    .build();
        }

        @Bean
        public Step step01(JobRepository jobRepository, // (8)
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           @Qualifier("reader") ItemReader<Customer> reader,
                           @Qualifier("processor") ItemProcessor<Customer, Customer> processor,
                           @Qualifier("writer") ItemWriter<Customer> writer,
                           @Qualifier("loggingItemReaderListener") LoggingItemReaderListener listener) {
            return new StepBuilder("jobCustomerList01.step01", jobRepository) // // (9)
                    .<Customer, Customer> chunk(10, transactionManager) // (10)
                    .reader(reader) // (11)
                    .processor(processor)
                    .writer(writer)
                    .listener(listener)
                    .build();
        }

        @Bean
        public Job jobCustomerList01(JobRepository jobRepository, // (8)
                                     Step step01,
                                     @Qualifier("jobExecutionLoggingListener") JobExecutionLoggingListener listener) {
            return new JobBuilder("jobCustomerList01", jobRepository) // (7)
                    .start(step01)
                    .listener(listener)
                    .build();
        }
    }

XMLConfig
Bean定義ファイルの例(チャンクモデル)

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd
                 http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

        <!-- (1) -->
        <import resource="classpath:META-INF/spring/job-base-context.xml"/>

        <!-- (2) -->
        <context:component-scan
            base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common" />

        <!-- (3) -->
        <mybatis:scan
            base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.mst"
            factory-ref="jobSqlSessionFactory"/>

        <!-- (4) -->
        <bean id="reader"
              class="org.mybatis.spring.batch.MyBatisCursorItemReader" scope="step"
              p:queryId="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.mst.CustomerRepository.findAll"
              p:sqlSessionFactory-ref="jobSqlSessionFactory"/>

        <!-- (5) -->
        <!-- Item Processor -->
        <!-- Item Processor in order that based on the Bean defined by the annotations, not defined here -->

        <!-- (6) -->
        <bean id="writer"
              class="org.springframework.batch.item.file.FlatFileItemWriter"
              scope="step"
              p:resource="file:#{jobParameters['outputFile']}">
            <property name="lineAggregator">
                <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
                    <property name="fieldExtractor">
                        <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                              p:names="customerId,customerName,customerAddress,customerTel,chargeBranchId"/>
                    </property>
                </bean>
            </property>
        </bean>

        <!-- (7) -->
        <batch:job id="jobCustomerList01" job-repository="jobRepository"> <!-- (8) -->
            <batch:step id="jobCustomerList01.step01"> <!-- (9) -->
                <batch:tasklet transaction-manager="jobTransactionManager"> <!-- (10) -->
                    <batch:chunk reader="reader"
                                 processor="processor"
                                 writer="writer"
                                 commit-interval="10" /> <!-- (11) -->
                </batch:tasklet>
            </batch:step>
        </batch:job>
    </beans>

ItemProcessor実装クラスの設定

    @Component("processor") // (5)
    public class CustomerProcessor implements ItemProcessor<Customer, Customer> {
      // omitted.
    }

項番 | 説明
--- | ---
(1) | Macchinetta Batch 2.xを利用する際に、常に必要なBean定義を読み込む設定をインポートする。
(2) | コンポーネントスキャン対象のベースパッケージを設定する。
(3) | MyBatis-Springの設定。 MyBatis-Springの設定の詳細は、データベースアクセスを参照。
(4) | ItemReaderの設定。 ItemReaderの詳細は、データベースアクセス、ファイルアクセスを参照。
(5) | ItemProcessorは、(2)によりアノテーションにて定義することができ、Bean定義ファイルで定義する必要がない。
(6) | ItemWriterの設定。 ItemWriterの詳細は、データベースアクセス、ファイルアクセスを参照。
(7) | ジョブの設定。 `JobBuilderのコンストラクタの引数name/<batch:job>のid属性`は、1つのバッチアプリケーションに含まれる全ジョブの範囲内で一意とする必要がある。
(8) | `JobRepository`の設定。 `JobBuilder、StepBuilderのコンストラクタの引数jobRepository/<batch:job>のjob-repository属性`に設定する値は、特別な理由がない限り`jobRepository`固定とすること。 これにより、すべてのジョブが1つの`JobRepository`で管理できる。 `jobRepository`のBean定義は、(1)により解決する。
(9) | ステップの設定。 `StepBuilderのコンストラクタの引数name/<batch:job>のid属性`は、1つのバッチアプリケーションに含まれる全ジョブの範囲内で一意とする必要はないが、障害発生時に追跡しやすくなる等の様々なメリットがあるため一意とする。 特別な理由がない限り、(7)で指定した`JobBuilderのコンストラクタの引数name/<batch:job>のid属性`に[step+連番]を付加する形式とする。
(10) | タスクレットの設定。 `StepBuilderのchunkメソッドの引数transactionManager/<batch:tasklet>のtransaction-manager属性`に設定する値は、特別な理由がない限り`jobTransactionManager`固定とすること。 これにより、(11)の`commit-interval`ごとにトランザクションが管理される。 詳細については、トランザクション制御を参照。 `jobTransactionManager`のBean定義は、(1)により解決する。
(11) | チャンクモデルジョブの設定。 `StepBuilderのreaderメソッド、writerメソッド/<batch:chunk>のreader属性、writer属性`に、前項までに定義した`ItemReader`、`ItemWriter`のBeanIDを指定する。 `StepBuilderのprocessorメソッド/processor属性`に、ItemProcessorの実装クラスのBeanIDを指定する。 `StepBuilderのchunkメソッドの引数chunkSize/<batch:chunk>のcommit-interval属性`に1チャンクあたりの入力データ件数を設定する。

| アノテーションベースのBeanの依存性解決の有効化方法 アノテーションベースのBeanの依存性解決を有効化する方法は、JavaConfig/XMLConfigで異なる。 JavaConfig ジョブのBean定義ファイルのクラスに付与する`@Configuration`によって有効化される。 XMLConfig ジョブのBean定義ファイルに`<context:component-scan>`を設定することで有効化される（正確には、`<context:component-scan>`が内部で呼び出す`<context:annotation-config>`によって有効化される）。コンポーネントスキャンを利用しない場合は`<context:annotation-config>`を設定する必要がある。

| chunkSize/commit-intervalのチューニング `chunkSize/commit-interval`はチャンクモデルジョブにおける、性能上のチューニングポイントである。 前述の例では10件としているが、利用できるマシンリソースやジョブの特性によって適切な件数は異なる。 複数のリソースにアクセスしてデータを加工するジョブであれば10件から100件程度で処理スループットが頭打ちになることもある。 一方、入出力リソースが1:1対応しておりデータを移し替える程度のジョブであれば5000件や10000件でも処理スループットが伸びることがある。 ジョブ実装時の`chunkSize/commit-interval`は100件程度で仮置きしておき、 その後に実施した性能測定の結果に応じてジョブごとにチューニングするとよい。

##### 3.2.2.2. コンポーネントの実装

ここでは主に、ItemProcessorを実装する方法について説明する。

他のコンポーネントについては、以下を参照。

- ItemReader、ItemWriter

  - データベースアクセス、 ファイルアクセス

- Listener

  - リスナー

###### 3.2.2.2.1. ItemProcessorの実装

ItemProcessorの実装方法を説明する。

`ItemProcessor`は、以下のインタフェースが示すとおり、入力リソースから取得したデータ1件をもとに、 出力リソースに向けたデータ1件を作成する役目を担う。 つまり、`ItemProcessor`はデータ1件に対するビジネスロジックを実装する箇所、と言える。

ItemProcessorインタフェース

    public interface ItemProcessor<I, O> {
        O process(I item) throws Exception;
    }

なお、インタフェースが示す`I`と`O`は以下のとおり同じ型でも異なる型でもよい。 同じ型であれば入力データを一部修正することを意味し、 異なる型であれば入力データをもとに出力データを生成することを意味する。

ItemProcessor実装例(入出力が同じ型)

    @Component
    public class AmountUpdateItemProcessor implements
            ItemProcessor<SalesPlanDetail, SalesPlanDetail> {

        @Override
        public SalesPlanDetail process(SalesPlanDetail item) throws Exception {
            item.setAmount(new BigDecimal("1000"));
            return item;
        }
    }

ItemProcessor実装例(入出力が異なる型)

    @Component
    public class UpdateItemFromDBProcessor implements
            ItemProcessor<SalesPerformanceDetail, SalesPlanDetail> {

        @Inject
        CustomerRepository customerRepository;

        @Override
        public SalesPlanDetail process(SalesPerformanceDetail readItem) throws Exception {
            Customer customer = customerRepository.findOne(readItem.getCustomerId());

            SalesPlanDetail writeItem = new SalesPlanDetail();
            writeItem.setBranchId(customer.getChargeBranchId());
            writeItem.setYear(readItem.getYear());
            writeItem.setMonth(readItem.getMonth());
            writeItem.setCustomerId(readItem.getCustomerId());
            writeItem.setAmount(readItem.getAmount());
            return writeItem;
        }
    }

| ItemProcessorからnullを返却することの意味 ItemProcessorからnullを返却することは、当該データを後続処理(Writer)に渡さないことを意味し、 言い換えるとデータをフィルタすることになる。 これは、入力データの妥当性検証を実施する上で有効活用できる。 詳細については、入力チェックを参照。

| ItemProcessorの処理スループットをあげるには 前述した実装例のように、ItemProcessorの実装クラスではデータベースやファイルを始めとしたリソースにアクセスしなければならないことがある。 ItemProcessorは入力データ1件ごとに実行されるため、入出力が少しでも発生するとジョブ全体では大量の入出力が発生することになる。 そのため、極力入出力を抑えることが処理スループットをあげる上で重要となる。 1つの方法として、後述のListenerを活用することで事前に必要なデータをメモリ上に確保しておき、 ItemProcessorにおける処理の大半を、CPU/メモリ間で完結するように実装する手段がある。 ただし、1ジョブあたりのメモリを大量に消費することにも繋がるので、何でもメモリ上に確保すればよいわけではない。 入出力回数やデータサイズをもとに、メモリに格納するデータを検討すること。 この点については、データの入出力でも紹介する。

#### 3.2.3. How to extend

ItemProcessorの省略

  入力データを加工せず、そのまま出力する場合は、`ItemProcessor`を省略するか、`PassThroughItemProcessor`を利用する。 以下には`ItemProcessor`を省略する設定例となる。

JavaConfig

    @Bean
    public Step exampleStep(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader reader,
                       ItemWriter writer) {
        return new StepBuilder("exampleJob.exampleStep",
                jobRepository)
                .<ItemReader, ItemWriter> chunk(10, transactionManager)
                .reader(reader)
                .writer(writer)
                .build();
    }

XMLConfig

    <batch:job id="exampleJob">
        <batch:step id="exampleStep">
            <batch:tasklet>
                <batch:chunk reader="reader" writer="writer" commit-interval="10" />
            </batch:tasklet>
        </batch:step>
    </batch:job>

複数のItemProcessorを同時に利用する

  複数のItemProcessorを同時に利用したい場合は、 Spring Batchが提供する`CompositeItemProcessor`を利用し連結することで実現できる。

JavaConfig

    @Bean
    public CompositeItemProcessor processor(
            ItemProcessor commonItemProcessor,
            ItemProcessor businessLogicItemProcessor) {
        return new CompositeItemProcessorBuilder<>()
                .delegates(commonItemProcessor)
                .delegates(businessLogicItemProcessor)
                .build();
    }

XMLConfig

    <bean id="processor"
          class="org.springframework.batch.item.support.CompositeItemProcessor">
        <property name="delegates">
            <list>
                <ref bean="commonItemProcessor"/>
                <ref bean="businessLogicItemProcessor"/>
            </list>
        </property>
    </bean>

`delegates`属性に指定した順番に処理されることに留意すること。

### 3.3. タスクレットモデルジョブの作成

#### 3.3.1. Overview

タスクレットモデルジョブの作成方法について説明する。 タスクレットモデルのアーキテクチャについては、Spring Batchのアーキテクチャを参照。

##### 3.3.1.1. 構成要素

タスクレットモデルジョブでは、複数の構成要素は登場しない。 `org.springframework.batch.core.step.tasklet.Tasklet`を実装し、Bean定義で設定するのみである。 また、発展的な実装手段としてチャンクモデルの構成要素である`ItemReader`や`ItemWriter`をコンポーネントとして使うことも可能である。

#### 3.3.2. How to use

ここでは、実際にタスクレットモデルジョブを実装する方法について、以下の順序で説明する。

- ジョブの設定

- Taskletの実装

##### 3.3.2.1. ジョブの設定

Bean定義ファイルにて、タスクレットモデルジョブを定義する。 以下に例を示す。

JavaConfig
Bean定義ファイルの例(タスクレットモデル)

    @Configuration
    @Import(JobBaseContextConfig.class) // (1)
    @ComponentScan("jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common") // (2)
    public class JobSimpleJobConfig {

        // (3)
        // Tasklet
        // Tasklet in order that based on the Bean defined by the annotations, not defined here

        @Bean
        public Step step01(JobRepository jobRepository, // (5)
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           SimpleJobTasklet simpleJobTasklet) {
            return new StepBuilder("jobSimpleJob.step01", jobRepository) // (6)
                    .tasklet(simpleJobTasklet, transactionManager) // (7)
                    .build();
        }

        @Bean
        public Job jobSimpleJob(JobRepository jobRepository, // (5)
                                Step step01) {
            return new JobBuilder("jobSimpleJob", jobRepository) // (4)
                    .start(step01)
                    .build();
        }
    }

XMLConfig
Bean定義ファイルの例(タスクレットモデル)

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd">

        <!-- (1) -->
        <import resource="classpath:META-INF/spring/job-base-context.xml"/>

        <!-- (2) -->
        <context:component-scan
              base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common"/>

        <!-- (3) -->
        <!-- Tasklet -->
        <!-- Tasklet in order that based on the Bean defined by the annotations, not defined here -->

        <!-- (4) -->
        <batch:job id="simpleJob" job-repository="jobRepository"> <!-- (5) -->
            <batch:step id="simpleJob.step01"> <!-- (6) -->
                <batch:tasklet transaction-manager="jobTransactionManager"
                               ref="simpleJobTasklet"/> <!-- (7) -->
            </batch:step>
        </batch:job>

    </beans>

Tasklet実装クラスの例

    package jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common;

    @Component // (3)
    public class SimpleJobTasklet implements Tasklet {
      // omitted.
    }

項番 | 説明
--- | ---
(1) | Macchinetta Batch 2.xを利用する際に、常に必要なBean定義を読み込む設定をインポートする。
(2) | コンポーネントスキャン対象のベースパッケージを設定する。 タスクレットモデルはアノテーションによるBean定義を基本とし、Tasklet実装クラスのBean定義はBean定義ファイル上では不要とする。
(3) | Taskletは、(2)によりアノテーションにて定義することができ、Bean定義ファイルで定義する必要がない。
(4) | ジョブの設定。 `JobBuilderのコンストラクタのname引数/<batch:job>のid属性`は、1つのバッチアプリケーションに含まれる全ジョブの範囲内で一意とする必要がある。
(5) | `JobRepository`の設定。 `JobBuilder、StepBuilderのコンストラクタの引数jobRepository/<batch:job>のjob-repository属性`に設定する値は、特別な理由がない限り`jobRepository`固定とすること。 これにより、すべてのジョブが1つの`JobRepository`で管理できる。 `jobRepository`のBean定義は、(1)により解決する。
(6) | ステップの設定。 `StepBuilderのコンストラクタのname引数/<batch:step>のid属性`は、1つのバッチアプリケーションに含まれる全ジョブの範囲内で一意とする必要はないが、障害発生時に追跡しやすくなる等の様々なメリットがあるため一意とする。 特別な理由がない限り、(4)で指定した`JobBuilderのコンストラクタのname引数/<batch:job>のid属性`に[step+連番]を付加する形式とする。
(7) | タスクレットの設定。 `StepBuilderのtaskletメソッドのtransactionManager引数/<batch:tasklet>のtransaction-manager属性`に設定する値は、特別な理由がない限り`jobTransactionManager`固定とすること。 これにより、タスクレット全体の処理が1つのトランザクションで管理される。 詳細については、トランザクション制御を参照。 `jobTransactionManager`のBean定義は、(1)により解決する。 また、`StepBuilderのtaskletメソッドの引数tasklet/<batch:tasklet>のref属性`は、(2)により解決するTaskletの実装クラスのBeanIDを指定する。 ここでは、Tasklet実装クラス名`SimpleJobTasklet`の先頭を小文字にした`simpleJobTasklet`となる。

| アノテーション利用時のBean名 `@Component`アノテーション利用時のBean名は、デフォルトでは `org.springframework.context.annotation.AnnotationBeanNameGenerator` を通じて生成されるため、命名ルールについては本クラスのJavadocを参照。

##### 3.3.2.2. Taskletの実装

まずはシンプルな実装で概要を理解し、次にチャンクモデルのコンポーネントを利用する実装へと進む。

以下の順序で説明する。

- シンプルなTaskletの実装

- チャンクモデルのコンポーネントを利用するTasklet実装

##### 3.3.2.3. シンプルなTaskletの実装

ログを出力するのみのTasklet実装を通じ、最低限のポイントを説明する。

シンプルなTasklet実装クラスの例

    package jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common;

    // omitted.

    @Component
    public class SimpleJobTasklet implements Tasklet { // (1)

        private static final Logger logger =
                LoggerFactory.getLogger(SimpleJobTasklet.class);

        @Override
        public RepeatStatus execute(StepContribution contribution,
                ChunkContext chunkContext) throws Exception {  // (2)
            logger.info("called tasklet."); // (3)
            return RepeatStatus.FINISHED; // (4)
        }
    }

項番 | 説明
--- | ---
(1) | `org.springframework.batch.core.step.tasklet.Tasklet`インタフェースを`implements`して実装する。
(2) | `Tasklet`インタフェースが定義する`execute`メソッドを実装する。 引数の`StepContribution`, `ChunkContext`は必要に応じて利用するが、ここでは説明を割愛する。
(3) | 任意の処理を実装する。ここではINFOログを出力している。
(4) | Taskletの処理が完了したかどうかを返却する。 常に`return RepeatStatus.FINISHED;`と明示する。

##### 3.3.2.4. チャンクモデルのコンポーネントを利用するTasklet実装

Spring Batch では、Tasklet実装の中でチャンクモデルの各種コンポーネントを利用することに言及していない。 Macchinetta Batch 2.xでは、以下のような状況に応じてこれを選択してよい。

- 複数のリソースを組み合わせながら処理するため、チャンクモデルの形式に沿いにくい

- チャンクモデルでは処理が複数箇所に実装することになるため、タスクレットモデルの方が全体像を把握しやすい

- リカバリをシンプルにするため、チャンクモデルの中間コミットではなく、タスクレットモデルの一括コミットを使いたい

また、チャンクモデルのコンポーネントを利用してTasklet実装するうえで処理の単位についても考慮してほしい。 出力件数の単位としては以下の3パターンが考えられる。

表 23. 出力件数の単位と特徴
出力件数 | 特徴
--- | ---
1件 | データを1件ずつ、入力、処理、出力しているため、処理のイメージがしやすい。 大量データの場合は入出力の多発により性能劣化を引き起こす可能性があるので留意が必要である。
全件 | データを1件ずつ、入力、処理してメモリ上に貯めておき、最後に全件一括で出力する。 少量データの場合はデータの整合性を担保するとともに性能向上を期待できる。 ただし、大量データの場合はリソース(CPU、メモリ)に高負荷がかかる可能性があるので留意が必要である。
一定件数 | データを1件ずつ、入力、処理してメモリ上に貯めておき、一定件数まできたところで出力する。 大量データを一定のリソース(CPU、メモリ)で効率よく処理できることにより性能向上を期待できる。 また、一定件数ごとに処理するため、トランザクション制御を実装することにより中間コミット方式にも移行できる。 ただし、中間コミット方式とする場合、ジョブが異常終了した後のリカバリは処理済みデータと未処理データが混在する可能性があるので留意が必要である。

以下に、チャンクモデルのコンポーネントである`ItemReader`や`ItemWriter`を利用するTasklet実装について説明する。

この実装例は、1件単位に処理している例である。

チャンクモデルのコンポーネントを利用するTasklet実装例1

    @Component
    @Scope("step") // (1)
    public class SalesPlanChunkTranTask implements Tasklet {

        @Inject
        @Named("detailCSVReader") // (2)
        ItemStreamReader<SalesPlanDetail> itemReader; // (3)

        @Inject
        SalesPlanDetailRepository repository; // (4)

        @Override
        public RepeatStatus execute(StepContribution contribution,
                ChunkContext chunkContext) throws Exception {

            SalesPlanDetail item;

            try {
                itemReader.open(chunkContext.getStepContext().getStepExecution()
                        .getExecutionContext()); // (5)

                while ((item = itemReader.read()) != null) { // (6)

                    // do some processes.

                    repository.create(item); // (7)
                }
            } finally {
                itemReader.close(); // (8)
            }
            return RepeatStatus.FINISHED;
        }
    }

JavaConfig
Bean定義例1

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan(value = "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.transaction.component", scopedProxy = ScopedProxyMode.TARGET_CLASS)
    @MapperScan(basePackages = "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan", sqlSessionFactoryRef = "jobSqlSessionFactory") // (9)
    public class CreateSalesPlanChunkTranTaskConfig {

        // (10)
        @Bean
        @StepScope
        public FlatFileItemReader<SalesPlanDetail> detailCSVReader(
                @Value("#{jobParameters['inputFile']}") File inputFile) {
            DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
            lineTokenizer.setNames("branchId", "year", "month", "customerId", "amount");
            BeanWrapperFieldSetMapper<SalesPlanDetail> fieldSetMapper = new BeanWrapperFieldSetMapper<>();
            fieldSetMapper.setTargetType(SalesPlanDetail.class);
            DefaultLineMapper<SalesPlanDetail> lineMapper = new DefaultLineMapper<>();
            lineMapper.setLineTokenizer(lineTokenizer);
            lineMapper.setFieldSetMapper(fieldSetMapper);
            return new FlatFileItemReaderBuilder<SalesPlanDetail>()
                    .name(ClassUtils.getShortName(FlatFileItemReader.class))
                    .resource(new FileSystemResource(inputFile))
                    .lineMapper(lineMapper)
                    .build();
        }

        // (11)
        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobResourcelessTransactionManager") PlatformTransactionManager transactionManager,
                           SalesPlanChunkTranTask salesPlanChunkTranTask) {
            return new StepBuilder("createSalesPlanChunkTranTask.step01",
                    jobRepository)
                    .tasklet(salesPlanChunkTranTask, transactionManager)
                    .build();
        }

        @Bean
        public Job createSalesPlanChunkTranTask(JobRepository jobRepository,
                                                Step step01) {
            return new JobBuilder("createSalesPlanChunkTranTask",jobRepository)
                    .start(step01)
                    .build();
        }
    }

XMLConfig
Bean定義例1

    <!-- omitted -->
    <import resource="classpath:META-INF/spring/job-base-context.xml"/>

    <context:component-scan
        base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.plan" />
    <context:component-scan
        base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.transaction.component" />

    <!-- (9) -->
    <mybatis:scan
        base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan"
        factory-ref="jobSqlSessionFactory"/>

    <!-- (10) -->
    <bean id="detailCSVReader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}">
        <property name="lineMapper">
            <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
                <property name="lineTokenizer">
                    <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
                          p:names="branchId,year,month,customerId,amount"/>
                </property>
                <property name="fieldSetMapper">
                    <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
                          p:targetType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.plan.SalesPlanDetail"/>
                </property>
            </bean>
        </property>
    </bean>

    <!-- (11) -->
    <batch:job id="createSalesPlanChunkTranTask" job-repository="jobRepository">
        <batch:step id="createSalesPlanChunkTranTask.step01">
            <batch:tasklet transaction-manager="jobTransactionManager"
                           ref="salesPlanChunkTranTask"/>
        </batch:step>
    </batch:job>

項番 | 説明
--- | ---
(1) | 本クラス内で利用するItemReaderのBeanスコープに合わせ、stepスコープとする。
(2) | 入力リソース(この例ではフラットファイル)へのアクセスは`ItemReader`を通じて行う。 ここでは、`detailCSVReader`というBean名を指定するが、わかりやすさのためなので任意とする。
(3) | `ItemReader`のサブインタフェースである、`ItemStreamReader`として型を定義する。 これは、(5), (8)のリソースオープン/クローズを実装する必要があるためである。 後ほど補足する。
(4) | 出力リソース(この例ではデータベース)へのアクセスはMyBatisのMapperを通じて行う。 ここでは、簡単のためMapperを直接利用している。常に`ItemWriter`を用いる必要はない。 もちろん、`MyBatisBatchItemWriter`を用いてもよい。
(5) | 入力リソースをオープンする。
(6) | 入力リソース全件を逐次ループ処理する。 `ItemReader#read`は、入力データがすべて読み取り末端に到達した場合、`null`を返却する。
(7) | データベースへ出力する。
(8) | リソースは必ずクローズすること。 なお、例外処理は必要に応じて実装すること。 ここで例外が発生した場合、タスクレット全体のトランザクションがロールバックされ、 例外のスタックトレースを出力し、ジョブが異常終了する。
(9) | MyBatis-Springの設定。 MyBatis-Springの設定の詳細は、データベースアクセスを参照。
(10) | ファイルから入力するため、`FlatFileItemReader`のBean定義を追加する。詳細はここでは割愛する。
(11) | 各種コンポーネントはアノテーションによって解決するため、 シンプルなTaskletの実装の場合と同様となる。

| スコープの統一について Tasklet実装クラスと、InjectするBeanのスコープは、同じスコープに統一すること。 たとえば、`FlatFileItemReader`が引数から入力ファイルパスを受け取る場合にはBeanスコープを`step`にする必要がある。 この時、Tasklet実装クラスのスコープも`step`にする必要がある。 仮にTasklet実装クラスのスコープを`singleton`としたケースを説明する。 この時、アプリケーション起動時の`ApplicationContext`生成時にTasklet実装クラスをインスタンス化した後、 `FlatFileItemReader`のインスタンスを解決してInjectしようとする。 しかし、`FlatFileItemReader`は`step`スコープでありstep実行時に生成するためまだ存在しない。 結果、Tasklet実装クラスをインスタンス化できないと判断し`ApplicationContext`生成に失敗してしまう。

| @Injectを付与するフィールドの型について 利用する実装クラスに応じて、以下のいずれかとする。 ItemReader/ItemWriter 対象となるリソースへのオープン・クローズを実施する必要がない場合に利用する。 ItemSteamReader/ItemStreamWriter 対象となるリソースへのオープン・クローズを実施する必要がある場合に利用する。 必ずjavadocを確認してどちらを利用するか判断すること。以下に代表例を示す。 FlatFileItemReader/Writerの場合 ItemSteamReader/ItemStreamWriterにて扱う MyBatisCursorItemReaderの場合 ItemStreamReaderにて扱う MyBatisBatchItemWriterの場合 ItemWriterにて扱う

この実装例は、一定件数単位に処理するチャンクモデルを模倣した例である

チャンクモデルのコンポーネントを利用するTasklet実装例2

    @Component
    @Scope("step")
    public class SalesPerformanceTasklet implements Tasklet {

        @Inject
        ItemStreamReader<SalesPerformanceDetail> reader;

        @Inject
        ItemWriter<SalesPerformanceDetail> writer; // (1)

        int chunkSize = 10; // (2)

        @Override
        public RepeatStatus execute(StepContribution contribution,
                ChunkContext chunkContext) throws Exception {

            try {
                reader.open(chunkContext.getStepContext().getStepExecution()
                        .getExecutionContext());

                List<SalesPerformanceDetail> items = new ArrayList<>(chunkSize); // (2)
                SalesPerformanceDetail item = null;
                do {
                    // Pseudo operation of ItemReader
                    for (int i = 0; i < chunkSize; i++) { // (3)
                        item = reader.read();
                        if (item == null) {
                            break;
                        }
                        // Pseudo operation of ItemProcessor
                        // do some processes.

                        items.add(item);
                    }

                    // Pseudo operation of ItemWriter
                    if (!items.isEmpty()) {
                        writer.write(new Chunk(items)); // (4)
                        items.clear();
                    }
                } while (item != null);
            } finally {
                try {
                    reader.close();
                } catch (Exception e) {
                    // do nothing.
                }
            }

            return RepeatStatus.FINISHED;
        }
    }

JavaConfig
Bean定義例2

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan(value = { "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common",
            "jp.co.ntt.fw.macchinetta.batch.functionaltest.functionaltest.ch06.exceptionhandling" }, scopedProxy = ScopedProxyMode.TARGET_CLASS)
    @MapperScan(value = "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.performance", sqlSessionFactoryRef = "jobSqlSessionFactory")
    public class JobSalesPerfTaskletConfig {

        @Bean
        @StepScope
        public FlatFileItemReader<SalesPerformanceDetail> detailCSVReader(
                @Value("#{jobParameters['inputFile']}") File inputFile) {
            final DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
            tokenizer.setNames("branchId", "year", "month", "customerId", "amount");
            final BeanWrapperFieldSetMapper<SalesPerformanceDetail> fieldSetMapper = new BeanWrapperFieldSetMapper<>();
            fieldSetMapper.setTargetType(SalesPerformanceDetail.class);
            final DefaultLineMapper<SalesPerformanceDetail> lineMapper = new DefaultLineMapper<>();
            lineMapper.setLineTokenizer(tokenizer);
            lineMapper.setFieldSetMapper(fieldSetMapper);
            return new FlatFileItemReaderBuilder<SalesPlanDetail>()
                    .name(ClassUtils.getShortName(FlatFileItemReader.class))
                    .resource(new FileSystemResource(inputFile))
                    .lineMapper(lineMapper)
                    .build();
        }

        // (1)
        @Bean
        public MyBatisBatchItemWriter<SalesPerformanceDetail> detailWriter(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
                SqlSessionTemplate batchModeSqlSessionTemplate) {
            return new MyBatisBatchItemWriterBuilder<SalesPerformanceDetail>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .statementId(
                            "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.performance.SalesPerformanceDetailRepository.create")
                    .sqlSessionTemplate(batchModeSqlSessionTemplate).build();
        }

        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           SalesPerformanceTasklet salesPerformanceTasklet) {
            return new StepBuilder("jobSalesPerfTasklet.step01",
                    jobRepository)
                    .tasklet(salesPerformanceTasklet, transactionManager)
                    .build();
        }

        @Bean
        public Job jobSalesPerfTasklet(JobRepository jobRepository,
                                                  Step step01,
                                                  JobExecutionLoggingListener listener) {
            return new JobBuilder("jobSalesPerfTasklet", jobRepository)
                    .start(step01)
                    .listener(listener)
                    .build();
        }
    }

XMLConfig
Bean定義例2

    <!-- omitted -->
    <import resource="classpath:META-INF/spring/job-base-context.xml"/>

    <context:component-scan
        base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common,
            jp.co.ntt.fw.macchinetta.batch.functionaltest.ch06.exceptionhandling"/>
    <mybatis:scan
        base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.performance"
        factory-ref="jobSqlSessionFactory"/>

    <bean id="detailCSVReader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}">
        <property name="lineMapper">
            <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
                <property name="lineTokenizer">
                    <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
                          p:names="branchId,year,month,customerId,amount"/>
                </property>
                <property name="fieldSetMapper">
                    <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
                          p:targetType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.performance.SalesPerformanceDetail"/>
                </property>
            </bean>
        </property>
    </bean>

    <!-- (1) -->
    <bean id="detailWriter"
          class="org.mybatis.spring.batch.MyBatisBatchItemWriter"
          p:statementId="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.performance.SalesPerformanceDetailRepository.create"
          p:sqlSessionTemplate-ref="batchModeSqlSessionTemplate"/>

    <batch:job id="jobSalesPerfTasklet" job-repository="jobRepository">
        <batch:step id="jobSalesPerfTasklet.step01">
            <batch:tasklet ref="salesPerformanceTasklet"
                           transaction-manager="jobTransactionManager"/>
        </batch:step>
    </batch:job>

項番 | 説明
--- | ---
(1) | `ItemWriter`の実装として`MyBatisBatchItemWriter`を利用する。
(2) | `ItemWriter`は一定件数をまとめて出力する。 ここでは10件ごとに処理し出力する。
(3) | チャンクモデルの動作にそって、 read→process→read→process→…​→writeとなるようにする。
(4) | `ItemWriter`を通じてまとめて出力する。

`ItemReader`や`ItemWriter`の実装クラスを利用するかどうかは都度判断してほしいが、 ファイルアクセスは`ItemReader`や`ItemWriter`の実装クラスを利用するとよい。 それ以外のデータベースアクセス等は無理に使う必要はない。性能向上のために使えばよい。

### 3.4. チャンクモデルとタスクレットモデルの使い分け

#### 3.4.1. チャンクモデルとタスクレットモデルの比較

ここでは、チャンクモデルとタスクレットモデルの使い分けについて、それぞれの特徴を整理することで説明する。 なお、詳細な説明については適宜対応する章を参照。

また、以降の内容は考え方の一例として捉えてほしい。制約や推奨事項ではない。 ユーザやシステムの特性に応じてジョブを作成する際の参考にしてほしい。

以下に、チャンクモデルとタスクレットモデルの主要な違いについて列挙する。

表 24. チャンクモデルとタスクレットモデルの比較
項目 | チャンクモデル | タスクレットモデル
--- | --- | ---
構成要素 | `ItemReader`, `ItemProcessor`, `ItemWriter`の3つに分割する。 | `Tasklet`の1つに集約する。
トランザクション | 一定件数で中間コミットを発行しながら処理することが基本となる。一括コミットはできない。 処理対象データ件数に依らず一定のマシンリソースで処理できる。 処理途中でエラーが発生すると未処理データと処理済データが混在する。 | 全体で一括コミットにて処理することが基本となる。中間コミットはユーザにて実装する必要がある。 処理対象データが大量になると、マシンリソースが枯渇する恐れがある。 処理途中でエラーが発生すると未処理データのみにロールバックされる。
リスタート | 件数ベースのリスタートができる。 | 件数ベースのリスタートはできない。

#### 3.4.2. チャンクモデルとタスクレットモデルを使い分ける例

以上を踏まえて、以下にそれぞれを使い分ける例をいくつか紹介する。

リカバリを限りなくシンプルにしたい

  エラーとなったジョブは対象のジョブをリランするのみで復旧したい場合など、 リカバリをシンプルにしたい時はタスクレットモデルを選択するとよい。
  チャンクモデルでは処理済データをジョブ実行前の状態に戻したり、 未処理データのみ処理するようジョブをあらかじめ作りこんでおいたり、 といった対処が必要となる。

処理の内容をまとめたい

  1ジョブ1クラスなど、ジョブの見通しを優先したい場合はタスクレットを選択するとよい。

大量のデータを安定して処理したい

  1000万件など、一括処理するとリソースに影響する件数を対象とする際はチャンクモデルを活用するか検討するとよい。 これは中間コミットによって安定させることを意味する。 タスクレットモデルでも中間コミットを打つことが可能だが、チャンクモデルの方がシンプルな実装になる可能性がある。

エラー後の復旧は件数ベースリスタートとしたい

  バッチウィンドウがシビアであり、エラーとなったデータ以降から再開したい場合に、 Spring Batchが提供する件数ベースリスタートを活用するときは、チャンクモデルを選択する必要がある。 これにより、個々のジョブでその仕組を作りこむ必要がなくなる。

| チャンクモデルとタスクレットモデルは、併用することが基本である。 バッチシステム内のジョブすべてをどちらかのモデルでのみ実装する必要はない。 システム全体のジョブがもつ特性を踏まえて、一方のモデルを基本とし、状況に応じてもう一方のモデルを使うことは自然である。 たとえば、大部分は処理件数や処理時間に余裕があるならばタスクレットモデルを基本とし、 極少数の大量件数を処理するジョブはチャンクモデルを選択する、といったことは自然といえる。

## 4. ジョブの起動

### 4.1. 同期実行

#### 4.1.1. Overview

同期実行について説明する。 同期実行とは、ジョブスケジューラなどによりシェルを介して新規プロセスとして起動し、ジョブの実行結果を呼び出しもとに返却する実行方法である。

図 16. 同期実行の概要
図 17. 同期実行の流れ

本機能は、チャンクモデルとタスクレットモデルとで同じ使い方になる。

利用前提

ジョブの実行結果を呼び出しもとに返却するため、`JobRepository`を前提とした再処理(処理の再実行を参照)を 利用しない限り`JobRepository`によるジョブの実行状況および結果の参照は必須ではない。 そのため、`JobRepository`に永続化が不要なインメモリデータベースをデフォルトで使用することを前提とする。

#### 4.1.2. How to use

`CommandLineJobRunner`によってジョブを起動する方法を説明する。

なお、アプリケーションのビルドや実行については、プロジェクトの作成を参照。 また、起動パラメータの指定方法や活用方法については、ジョブの起動パラメータを参照。 これらと本節の説明は一部重複するが、同期実行の要素に注目して説明する。

##### 4.1.2.1. 実行方法

Macchinetta Batch 2.xにおいて、同期実行は Spring Batch が提供する`CommandLineJobRunner`によって実現する。 `CommandLineJobRunner`は、以下の要領にてjavaコマンドを発行することで起動する。

CommandLineJobRunnerの構文

    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner <jobPath> <options> <jobIdentifier> <jobParameters>

表 25. 引数にて指定する項目
指定する項目 | 説明 | 必須
--- | --- | ---
jobPath | 起動するジョブの設定を記述したBean定義ファイルのパス。classpathからの相対パスにて指定する。 |
options | 起動する際の各種オプション(停止、リスタートなど)を指定する。 |
jobIdentifier | ジョブの識別子として、Bean定義上のジョブ名、もしくはジョブを実行後のジョブ実行IDを指定する。 通常はジョブ名を指定する。ジョブ実行IDは停止やリスタートの際にのみ指定する。 |
jobParameters | ジョブの引数を指定する。指定は`key=value`形式となる。 |

以下に、必須項目のみを指定した場合の実行例を示す。

JavaConfig
コマンドプロンプトでのCommandLineJobRunnerの実行例

    C:\xxx>java -cp "target\[artifactId]-[version].jar;lib\*" ^   # (1)
        org.springframework.batch.core.launch.support.CommandLineJobRunner ^ # (2)
        xxxxxx.yyyyyy.zzzzzz.projectName.jobs.Job01Config job01 # (3)

BashでのCommandLineJobRunnerの実行例

    $ java -cp 'target/[artifactId]-[version].jar:lib/*' \ # (1)
        org.springframework.batch.core.launch.support.CommandLineJobRunner \ # (2)
        xxxxxx.yyyyyy.zzzzzz.projectName.jobs.Job01Config job01 # (3)

Bean定義の設定(抜粋)

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ListItemReader<Employee> employeeReader,
                       EmployeeProcessor employeeProcessor,
                       FlatFileItemWriter<Employee> employeeWriter) {
        return new StepBuilder("job01.step01",
                jobRepository)
                .<Employee, Employee> chunk(10, transactionManager)
                .reader(employeeReader)
                .processor(employeeProcessor)
                .writer(employeeWriter)
                .build();
    }

    @Bean
    public Job job01(JobRepository jobRepository,
                                            Step step01) {
        return new JobBuilder("job01",jobRepository)
                .start(step01)
                .build();
    }

XMLConfig
コマンドプロンプトでのCommandLineJobRunnerの実行例

    C:\xxx>java -cp "target\[artifactId]-[version].jar;lib\*" ^   # (1)
        org.springframework.batch.core.launch.support.CommandLineJobRunner ^ # (2)
        META-INF/jobs/job01.xml job01 # (3)

BashでのCommandLineJobRunnerの実行例

    $ java -cp 'target/[artifactId]-[version].jar:lib/*' \ # (1)
        org.springframework.batch.core.launch.support.CommandLineJobRunner \ # (2)
        META-INF/jobs/job01.xml job01 # (3)

Bean定義の設定(抜粋)

    <batch:job id="job01" job-repository="jobRepository"> <!-- (3) -->
        <batch:step id="job01.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="employeeReader"
                             processor="employeeProcessor"
                             writer="employeeWriter" commit-interval="10" />
            </batch:tasklet>
        </batch:step>
    </batch:job>

表 26. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `java`コマンドを実行する際に、バッチアプリケーションのjarと、依存するjarを`classpath`に指定する。 ここではコマンド引数で指定しているが、環境変数等を用いてもよい。
(2) | 起動するクラスに、`CommandLineJobRunner`をFQCNで指定する。
(3) | `CommandLineJobRunner`に沿って、起動引数を渡す。 ここでは、`jobPath`と`jobIdentifier`としてジョブ名の2つを指定している。

次に、任意項目として起動パラメータを指定した場合の実行例を示す。

JavaConfig
コマンドプロンプトでのCommandLineJobRunnerの実行例

    C:\xxx>java -cp "target\[artifactId]-[version].jar;lib\*" ^
        org.springframework.batch.core.launch.support.CommandLineJobRunner ^
        xxxxxx.yyyyyy.zzzzzz.projectName.jobs.SetupJobConfig setupJob target=server1 outputFile=/tmp/result.csv # (1)

BashでのCommandLineJobRunnerの実行例

    $ java -cp 'target/[artifactId]-[version].jar:lib/*' \
        org.springframework.batch.core.launch.support.CommandLineJobRunner \
        xxxxxx.yyyyyy.zzzzzz.projectName.jobs.SetupJobConfig setupJob target=server1 outputFile=/tmp/result.csv # (1)

XMLConfig
コマンドプロンプトでのCommandLineJobRunnerの実行例

    C:\xxx>java -cp "target\[artifactId]-[version].jar;lib\*" ^
        org.springframework.batch.core.launch.support.CommandLineJobRunner ^
        META-INF/jobs/setupJob.xml setupJob target=server1 outputFile=/tmp/result.csv # (1)

BashでのCommandLineJobRunnerの実行例

    $ java -cp 'target/[artifactId]-[version].jar:lib/*' \
        org.springframework.batch.core.launch.support.CommandLineJobRunner \
        META-INF/jobs/setupJob.xml setupJob target=server1 outputFile=/tmp/result.csv # (1)

表 27. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | ジョブの起動パラメータとして、`target=server1`と`outputFile=/tmp/result.csv`を指定している。

##### 4.1.2.2. 任意オプション

CommandLineJobRunnerの構文で示した任意のオプションについて補足する。

`CommandLineJobRunner`では以下の4つの起動オプションが使用できる。 ここでは個々の説明は他に委ねることとし、概要のみ説明する。

-restart

  失敗したジョブを再実行する。詳細は、処理の再実行を参照。

-stop

  実行中のジョブを停止する。詳細は、ジョブの管理を参照。

-abandon

  停止されたジョブを放棄する。放棄されたジョブは再実行不可となる。 Macchinetta Batch 2.xでは、このオプションを活用するシーンがないため、説明を割愛する。

-next

  過去に一度実行完了したジョブを再度実行する。ただし、Macchinetta Batch 2.xでは、このオプションを利用しない。
  なぜなら、Macchinetta Batch 2.xでは、Spring Batchのデフォルトである「同じパラメータで起動したジョブは同一ジョブとして認識され、同一ジョブは1度しか実行できない」 という制約を回避しているためである。
  詳細はパラメータ変換クラスについてにて説明する。
  また、本オプションを利用するには、`JobParametersIncrementer`というインタフェースの実装クラスが必要だが、 ブランクプロジェクトでは設定を行っていない。
  そのため、本オプションを指定して起動すると、必要なBean定義が存在しないためエラーとなる。

### 4.2. ジョブの起動パラメータ

#### 4.2.1. Overview

本節では、ジョブの起動パラメータ(以降、パラメータ)の利用方法について説明する。

本機能は、チャンクモデルとタスクレットモデルとで同じ使い方になる。

パラメータは、以下のような実行環境や実行タイミングに応じてジョブの動作を柔軟に切替える際に使用する。

- 処理対象のファイルパス

- システムの運用日時

パラメータを与える方法は、以下のとおりである。

1. コマンドライン引数から与える

2. ファイルから標準入力へリダイレクトする

指定したパラメータは、Bean定義やSpring管理下のJavaで参照できる。

#### 4.2.2. How to use

##### 4.2.2.1. パラメータ変換クラスについて

Spring Batchでは、受け取ったパラメータを以下の流れで処理する。

1. `JobParametersConverter`の実装クラスが`JobParameters`に変換する。

2. Bean定義やSpring管理下のJavaにて`JobParameters`からパラメータを参照する。

パラメータ変換クラスの実装クラスについて

Macchinetta Batch 2.xでは、前述した`JobParametersConverter`の実装クラスを複数提供する。 以下にそれぞれの特徴を示す。

- DefaultJobParametersConverter

  - Spring Batchが提供する。

  - パラメータのデータ型は `java.io.Serializable` を実装した任意のJavaBeansを指定できる。

- JobParametersConverterImpl

  - Macchinetta Batch 2.xが提供する。もともとSpring Batchで提供していたJsrJobParametersConverterを、JSR-352の廃止に伴いMacchinetta Batch 2.xへと移植したもの。

  - パラメータのデータ型を指定することができない(Stringのみ)。

  - パラメータにジョブ実行を識別するID(RUN_ID)を`jsr_batch_run_id`という名称で自動的に付与する。

    - RUN_IDは、ジョブが実行される都度増加する。増加は、データベースのSEQUENCE(名称は`JOB_SEQ`となる)を利用するため、重複することがない。

    - Spring Batchでは、同じパラメータで起動したジョブは同一ジョブとして認識され、同一ジョブは1度しか実行できない、という仕様がある。 これに対し、`jsr_batch_run_id`という名称のパラメータを一意な値で付加することにより、別のジョブと認識する仕組みとなっている。 詳細は、Spring Batchのアーキテクチャを参照。

Spring BatchではBean定義で使用する`JobParametersConverter`の実装クラスを指定しない場合、`DefaultJobParametersConverter`が使用される。
しかし、Macchinetta Batch 2.xでは以下の理由により`DefaultJobParametersConverter`は採用しない。

- 1つのジョブを同じパラメータによって、異なるタイミングで起動することは一般的である。

- 起動時刻のタイムスタンプなどを指定し、異なるジョブとして管理することも可能だが、それだけのためにジョブパラメータを指定するのは煩雑である。

- `DefaultJobParametersConverter`はパラメータに対しデータ型を指定することができるが、型変換に失敗した場合のハンドリングが煩雑になる。

Macchinetta Batch 2.xでは、`JobParametersConverterImpl`を利用することで、ユーザが意識することなく自動的にRUN_IDを付与している。 この仕組みにより、ユーザから見ると同一ジョブをSpring Batchとしては異なるジョブとして扱っている。

パラメータ変換クラスの設定について

ブランクプロジェクトでは、あらかじめ`TerasolunaBatchConfiguration.java/launch-context.xml`にて`JobParametersConverterImpl`を使用するように設定している。
そのためMacchinetta Batch 2.xを推奨設定で使用する場合は`JobParametersConverter`の設定を行う必要はない。

JavaConfig
com.example.batch.config.LaunchContextConfig.java

    @Configuration
    @Import(TerasolunaBatchConfiguration.class)
    public class LaunchContextConfig {
    // omitted
    @Bean
    public JobParametersConverter jobParametersConverter(
            @Qualifier("adminDataSource") DataSource adminDataSource) {
        return new JobParametersConverterImpl(adminDataSource);
    }

com.example.batch.config.TerasolunaBatchConfiguration.java

    @Override
    @Bean
    public JobOperator jobOperator(JobRepository jobRepository, JobExplorer jobExplorer, JobRegistry jobRegistry, JobLauncher jobLauncher) throws BatchConfigurationException {
        JobOperatorFactoryBean jobOperatorFactoryBean = new JobOperatorFactoryBean();
        jobOperatorFactoryBean.setTransactionManager(this.getTransactionManager());
        jobOperatorFactoryBean.setJobRepository(jobRepository);
        jobOperatorFactoryBean.setJobExplorer(jobExplorer);
        jobOperatorFactoryBean.setJobRegistry(jobRegistry);
        jobOperatorFactoryBean.setJobLauncher(jobLauncher);
        JobParametersConverterImpl jobParametersConverter = new JobParametersConverterImpl(
                getDataSource());
        jobOperatorFactoryBean.setJobParametersConverter(
                jobParametersConverter); // (1)

        try {
            jobParametersConverter.afterPropertiesSet();
            jobOperatorFactoryBean.afterPropertiesSet();
            return jobOperatorFactoryBean.getObject();

        } catch (BatchConfigurationException e) {
            throw e;
        } catch (Exception e) {
            throw new BatchConfigurationException(
                    "Unable to configure the customized job operator", e);
        }
    }

XMLConfig
META-INF\spring\launch-context.xml

    <bean id="jobParametersConverter"
          class="org.terasoluna.batch.converter.JobParametersConverterImpl"
          c:dataSource-ref="adminDataSource" />

    <bean id="jobOperator"
          class="org.springframework.batch.core.launch.support.SimpleJobOperator"
          p:jobRepository-ref="jobRepository"
          p:jobRegistry-ref="jobRegistry"
          p:jobExplorer-ref="jobExplorer"
          p:jobParametersConverter-ref="jobParametersConverter" <!-- (1) -->
          p:jobLauncher-ref="jobLauncher" />

項番 | 説明
--- | ---
(1) | jobOperatorのjobParametersConverterの設定 XMLConfigでは`jobParametersConverter-ref`属性にBean定義した`jobParametersConverter`を指定している。 JavaConfigでは`JobOperator`の`JobParametersConverter`の実装クラスがデフォルトでは`DefaultJobParametersConverter`のため、Bean定義をオーバーライドして`JobParametersConverterImpl`を設定するようにしている。

以降は`JobParametersConverterImpl`を利用する前提で説明する。

##### 4.2.2.2. コマンドライン引数から与える

まず、もっとも基本的な、コマンドライン引数から与える方法について説明する。

パラメータの付与

コマンドライン引数として`CommandLineJobRunner`の第3引数以降に`<パラメータ名>=<値>`形式で列挙する。

パラメータの個数や長さは、Spring BatchやMacchinetta Batch 2.xにおいては制限がない。 しかし、OSにはコマンド引数の長さに制限がある。
そのため、あまりに大量の引数が必要な場合は、ファイルから標準入力へリダイレクトするや パラメータとプロパティの併用などの方法を活用すること。

JavaConfig
コマンドライン引数としてパラメータを設定する例

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        com.example.batch.jobs.JobDefinedConfig.JobDefinedConfig JOBID param1=abc outputFileName=/tmp/result.csv

XMLConfig
コマンドライン引数としてパラメータを設定する例

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        JobDefined.xml JOBID param1=abc outputFileName=/tmp/result.csv

パラメータの参照

以下のように、Bean定義またはJavaで参照することができる。

- Bean定義で参照する

  - `#{jobParameters['xxx']}`で参照可能

- Javaで参照する

  - `@Value("#{jobParameters['xxx']}")`で参照可能

| JobParametersを参照するBeanのスコープはStepスコープでなければならない `JobParameters`を参照する際は、参照するBeanのスコープを`Step`スコープとする必要がある。 これは、`JobParameters`を参照する際に、Spring Batchのlate bindingという仕組みを使用しているためである。 late bindingとはその名のとおり、遅延して値を設定することである。 Spring Frameworkの`ApplicationContext`は、デフォルトでは各種Beanのプロパティを解決してから`ApplicationContext`のインスタンスを生成する。 Spring Batchでは`ApplicationContext`のインスタンスを生成する時にはプロパティを解決せず、 各種Beanが必要になった際にプロパティを解決する機能をもつ。これが遅延という言葉が意味することである。 この機能により、Spring Batch自体の実行に必要な`ApplicationContext`を生成し実行した後に、 パラメータに応じて各種Beanの振る舞いを切替えることが可能となる。 なお、`Step`スコープはSpring Batch独自のスコープであり、Stepの実行ごとに新たなインスタンスが生成される。 また、late bindingによる値の解決は、Bean定義においてSpEL式を用いることで可能となる。

| Stepスコープの定義方法 Spring Batchのlate bindingを使用してJobParametersを参照するreaderやwriterは`Step`スコープとする必要があるが、それ等を参照するコンポーネント(`ItemProcessor`、`Tasklet`)も`Step`スコープとする必要がある。 スコープを統一する理由はチャンクモデルのコンポーネントを利用するTasklet実装で説明しているスコープの統一についてを参照。 ジョブのBean定義にはジョブの構成要素による定義とジョブから呼ばれるコンポーネントの2種類あり、それぞれの定義場所や定義方法は異なる。 `Step`スコープの定義方法も、それぞれのBean定義方法に応じて異なる。 Bean定義に対する`Step`スコープの定義方法を以下に示す。
表 28. Bean定義とStepスコープの定義まとめ
ジョブのBean定義 | Beanの定義場所 | Beanの定義方法 | `Step`スコープの定義方法(定義場所はBeanの定義場所と同じ) | 実装例
--- | --- | --- | --- | ---
ジョブの構成要素(job, step, reader, writer等) | JavaConfigの場合 ジョブのBean定義ファイル(.java) | `@Bean`を付与したメソッド | 対象の`Bean`メソッドに`@StepScope`を設定 `@ComponentScan`に以下の属性を追加 `scopedProxy = ScopedProxyMode.TARGET_CLASS` | コマンドライン引数で与えたパラメータをBean定義で参照する例
XMLConfigの場合 ジョブのBean定義ファイル(.xml) | `<bean>`タグ | 対象の`<bean>`タグの属性に以下を設定 `scope="step"`
ジョブのBean定義ファイルから参照されるコンポーネント(ItemprocessorやTasklet) | Java実装クラス(.java) | `@Component`を付与 | `@Scope("step")`を付与 | コマンドライン引数で与えたパラメータをJavaで参照する例
現状、コンポーネントに`@StepScope`を付与したときの挙動はJavaConfigとXMLConfigで非互換があるため、コンポーネントの`Step`スコープ定義では`@StepScope`は定義してはならない。

コマンドライン引数で与えたパラメータをBean定義で参照する例
JavaConfig

    @Bean
    @StepScope // (1)
    public FlatFileItemReader<SalesPlanDetail> reader(
        @Value("#{jobParameters['inputFile']}") File inputFile) { // (2)
        // omitted
    }

XMLConfig

    <!-- (1) -->
    <bean id="reader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}">  <!-- (2) -->
        <property name="lineMapper">
            <!-- omitted settings -->
        </property>
    </bean>

表 29. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `@Beanを付与するメソッドに@StepScopeを付与する/<bean>要素のscope属性でスコープを指定する`
(2) | 参照するパラメータを指定する。

コマンドライン引数で与えたパラメータをJavaで参照する例

    @Component
    @Scope("step")  // (1)
    public class ParamRefInJavaTasklet implements Tasklet {

        /**
         * Holds a String type value
         */
        @Value("#{jobParameters['str']}")  // (2)
        private String str;

        // omitted execute()
    }

表 30. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | クラスに`@Scope`アノテーションを付与してスコープを指定する。
(2) | `@Value`アノテーションを使用して参照するパラメータを指定する。

##### 4.2.2.3. ファイルから標準入力へリダイレクトする

ファイルから標準入力へリダイレクトする方法について説明する。

パラメータを定義するファイルの作成

パラメータは下記のようにファイルに定義する。

params.txt

    param1=abc
    outputFile=/tmp/result.csv

パラメータを定義したファイルを標準入力へリダイレクトする

コマンドライン引数としてパラメータを定義したファイルをリダイレクトする。

JavaConfig
実行方法

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        com.example.batch.jobs.JobDefinedConfig.JobDefinedConfig JOBID < params.txt

XMLConfig
実行方法

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        JobDefined.xml JOBID < params.txt

パラメータの参照

パラメータの参照方法はコマンドライン引数から与える方法と同様である。

##### 4.2.2.4. パラメータのデフォルト値を設定する

パラメータを任意とした場合、以下の形式でデフォルト値を設定することができる。

- `#{jobParameters['パラメータ名'] ?: デフォルト値}`

ただし、パラメータを使用して値を設定している項目であるということは、デフォルト値もパラメータと同様に環境や実行タイミングによって異なる可能性がある。

まずは、デフォルト値をソースコード上にハードコードをする方法を説明する。 しかし、後述のパラメータとプロパティの併用を活用する方が適切なケースが多いため、合わせて参照。

デフォルト値を設定したパラメータの参照

該当するパラメータが設定されなかった場合にデフォルト値に設定した値が参照される。

JavaConfig
コマンドライン引数で与えたパラメータをBean定義で参照する例

    @Bean
    @StepScope // (1)
    public FlatFileItemReader<SalesPlanDetail> reader(
        @Value("#{jobParameters['inputFile'] ?: '/input/sample.csv'}") File inputFile) { // (2)
        // omitted
    }

XMLConfig
コマンドライン引数で与えたパラメータをBean定義で参照する例

    <!-- (1) -->
    <bean id="reader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile'] ?: '/input/sample.csv'}">  <!-- (2) -->
        <property name="lineMapper">
            <!-- omitted settings -->
        </property>
    </bean>

表 31. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `@Beanを付与するメソッドに@StepScopeを付与する/<bean>要素のscope属性でスコープを指定する`
(2) | 参照するパラメータを指定する。 デフォルト値に`/input/sample.csv`を設定している。

コマンドライン引数で与えたパラメータをJavaで参照する例

    @Component
    @Scope("step")  // (1)
    public class ParamRefInJavaTasklet implements Tasklet {

        /**
         * Holds a String type value
         */
        @Value("#{jobParameters['str'] ?: 'xyz'}")  // (2)
        private String str;

        // omitted execute()
    }

表 32. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | クラスに`@Scope`アノテーションを付与してスコープを指定する。
(2) | `@Value`アノテーションを使用して参照するパラメータを指定する。 デフォルト値に`xyz`を設定している。

##### 4.2.2.5. パラメータの妥当性検証

オペレーションミスや意図しない挙動を防ぐために、ジョブの起動時にパラメータの妥当性検証が必要となる場合もある。
パラメータの妥当性検証はSpring Batchが提供する`JobParametersValidator`を活用することで実現可能である。

パラメータはItemReader/ItemProcessor/ItemWriterといった様々な場所で参照するため、 ジョブの起動直後に妥当性検証が行われる。

パラメータの妥当性を検証する方法は2つあり、検証の複雑度によって異なる。

- 簡易な妥当性検証

  - 適用例

    - 必須パラメータが設定されていることの検証

    - 意図しないパラメータが設定されていないことの検証

  - 使用するバリデータ

    - Spring Batchが提供している`DefaultJobParametersValidator`

- 複雑な妥当性検証

  - 適用例

    - 数値の範囲検証やパラメータ間の相関チェックなどの複雑な検証

    - Spring Batchが提供している`DefaultJobParametersValidator`にて実現不可能な検証

  - 使用するバリデータ

    - `JobParametersValidator`を自作で実装したクラス

簡易な妥当性検証および複雑な妥当性検証の妥当性を検証する方法についてそれぞれ説明する。

###### 4.2.2.5.1. 簡易な妥当性検証

Spring Batchは`JobParametersValidator`のデフォルト実装として、`DefaultJobParametersValidator`を提供している。
このバリデータでは設定により以下を検証することができる。

- 必須パラメータが設定されていること

- 必須または任意パラメータ以外のパラメータが指定されていないこと

以下に定義例を示す。

JavaConfig
DefaultJobParametersValidatorを使用する妥当性検証の定義

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan(value = "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch04.jobparameter", scopedProxy = ScopedProxyMode.TARGET_CLASS)
    public class JobDefaultJobParametersValidatorConfig {

        // (1)
        @Bean
        public DefaultJobParametersValidator jobParametersValidator() {
            return new DefaultJobParametersValidator(
                    new String[] {"inputFileName", "outputFileName"}, // (2)
                    new String[] {"param1", "param2"}); // (3)
        }

        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           RefWithDefaultInJavaTasklet tasklet) {
            return new StepBuilder("jobDefaultJobParametersValidator.step01", jobRepository)
                    .tasklet(tasklet, transactionManager)
                    .build();
        }

        @Bean
        public Job jobDefaultJobParametersValidator(JobRepository jobRepository,
                                           Step step01,
                                           DefaultJobParametersValidator jobParametersValidator) {
            return new JobBuilder("jobDefaultJobParametersValidator",
                    jobRepository)
                    .start(step01)
                    .validator(jobParametersValidator) // (4)
                    .build();
        }

    }

XMLConfig
DefaultJobParametersValidatorを使用する妥当性検証の定義

    <!-- (1) -->
    <bean id="jobParametersValidator"
          class="org.springframework.batch.core.job.DefaultJobParametersValidator">
      <property name="requiredKeys">  <!-- (2) -->
        <list>
            <value>inputFileName</value>
            <value>outputFileName</value>
        </list>
      </property>
      <property name="optionalKeys">  <!-- (3) -->
        <list>
            <value>param1</value>
            <value>param2</value>
        </list>
      </property>
    </bean>

    <batch:job id="jobUseDefaultJobParametersValidator" job-repository="jobRepository">
      <batch:step id="jobUseDefaultJobParametersValidator.step01">
        <batch:tasklet ref="sampleTasklet" transaction-manager="jobTransactionManager"/>
      </batch:step>
      <batch:validator ref="jobParametersValidator"/>  <!-- (4) -->
    </batch:job>

表 33. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `DefaultJobParametersValidator`のBeanを定義する。
(2) | 必須パラメータは`requiredKeys`に設定する。 `String配列/<list>要素`を使用して必須パラメータのパラメータ名を複数指定できる。
(3) | 任意パラメータは`optionalKeys`に設定する。 `String配列/<list>要素`を使用して任意パラメータのパラメータ名を複数指定できる。
(4) | `JobBuilderのvalidatorメソッド/<batch:job>要素内の<batch:validator>要素`を使用してジョブにバリデータを適用する。

DefaultJobParametersValidatorを使用した場合のOKケースとNGケース

`DefaultJobParametersValidator`にて検証可能な条件の理解を深めるため、検証結果がOKとなる場合とNGとなる場合の例を示す。

JavaConfig
DefaultJobParametersValidator定義例

    @Bean
    public DefaultJobParametersValidator jobParametersValidator() {
        return new DefaultJobParametersValidator(
                new String[] {"outputFileName"},
                new String[] {"param1"});
    }

XMLConfig
DefaultJobParametersValidator定義例

    <bean id="jobParametersValidator"
        class="org.springframework.batch.core.job.DefaultJobParametersValidator"
        p:requiredKeys="outputFileName"
        p:optionalKeys="param1"/>

JavaConfig
NGケース1（任意パラメータのみ指定）

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        com.example.batch.jobs.JobDefinedConfig JOBID param1=aaa

XMLConfig
NGケース1（任意パラメータのみ指定）

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        JobDefined.xml JOBID param1=aaa

必須パラメータ`outputFileName`が設定されていないためNGとなる。

JavaConfig
OKケース1（必須パラメータのみ指定）

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        com.example.batch.jobs.JobDefinedConfig.JobDefinedConfig JOBID outputFileName=/tmp/result.csv

XMLConfig
OKケース1（必須パラメータのみ指定）

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        JobDefined.xml JOBID outputFileName=/tmp/result.csv

必須パラメータ`outputFileName`が設定されるためOKとなる。任意パラメータ`param1`は設定されていなくてもよい。

JavaConfig
OKケース2（必須パラメータと任意パラメータ指定）

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        com.example.batch.jobs.JobDefinedConfig.JobDefinedConfig JOBID param1=aaa outputFileName=/tmp/result.csv

XMLConfig
OKケース2（必須パラメータと任意パラメータ指定）

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        JobDefined.xml JOBID param1=aaa outputFileName=/tmp/result.csv

必須パラメータ`outputFileName`が設定されるためOKとなる。

JavaConfig
OKケース3（必須パラメータとBean定義にないパラメータ指定）

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        com.example.batch.jobs.JobDefinedConfig.JobDefinedConfig JOBID fileoutputFilename=/tmp/result.csv param2=aaa

XMLConfig
OKケース3（必須パラメータとBean定義にないパラメータ指定）

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        JobDefined.xml JOBID fileoutputFilename=/tmp/result.csv param2=aaa

必須パラメータ`outputFileName`が設定されるためOKとなる。Bean定義にないパラメータ`param2`が設定されてもよい。

###### 4.2.2.5.2. 複雑な妥当性検証

`JobParametersValidator`インタフェースの実装を自作することで、 要件に応じたパラメータの検証を実現することができる。

`JobParametersValidator`クラスは以下の要領で実装する。

- `JobParametersValidator`クラスを実装し、`validate`メソッドをオーバーライドする

- `validate`メソッドは以下の要領で実装する

  - `JobParameters`から各パラメータを取得し検証する

    - 検証の結果がOKである場合には、何もする必要はない

    - 検証の結果がNGである場合には、`JobParametersInvalidException`をスローする

`JobParametersValidator`クラスの実装例を示す。 ここでは、`str`で指定された文字列の長さが、`num`で指定された数値以下であることを検証している。

JobParametersValidatorインタフェースの実装例

    public class ComplexJobParametersValidator implements JobParametersValidator {  // (1)
        @Override
        public void validate(JobParameters parameters) throws JobParametersInvalidException {
            Map<String, JobParameter> params = parameters.getParameters();  // (2)

            String str = params.get("str").getValue().toString();  // (3)
            int num = Integer.parseInt(params.get("num").getValue().toString()); // (4)

            if(str.length() > num){
                throw new JobParametersInvalidException(
                "The str must be less than or equal to num. [str:"
                        + str + "][num:" + num + "]");  // (5)
            }
        }
    }

表 34. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `JobParametersValidator`クラスを実装し`validate`メソッドをオーバーライドする。
(2) | パラメータは`JobParameters`型で引数として受ける。 `parameters.getParameters()`とすることで、Map形式で取得することでパラメータの参照が容易になる。
(3) | keyを指定してパラメータを取得する。
(4) | パラメータをint型へ変換する。String型以外を扱う場合は適宜変換を行うこと。
(5) | パラメータ`str`の文字列長がパラメータ`num`の値を超えている場合に妥当性検証結果NGとしている。

JavaConfig
ジョブの定義例

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       RefWithDefaultInJavaTasklet tasklet) {
        return new StepBuilder("jobDefaultJobParametersValidator.step01", jobRepository)
                .tasklet(tasklet, transactionManager)
                .build();
    }

    @Bean
    public Job jobDefaultJobParametersValidator(JobRepository jobRepository,
                                       Step step01,
                                       DefaultJobParametersValidator jobParametersValidator) {
        return new JobBuilder("jobDefaultJobParametersValidator",
                jobRepository)
                .start(step01)
                .validator(jobParametersValidator) // (1)
                .build();
    }

XMLConfig
ジョブの定義例

    <batch:job id="jobUseComplexJobParametersValidator" job-repository="jobRepository">
        <batch:step id="jobUseComplexJobParametersValidator.step01">
            <batch:tasklet ref="sampleTasklet" transaction-manager="jobTransactionManager"/>
        </batch:step>
        <batch:validator>  <!-- (1) -->
            <bean class="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch04.jobparameter.ComplexJobParametersValidator"/>
        </batch:validator>
    </batch:job>

表 35. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `JobBuilderのvalidatorメソッド/<batch:job>要素内の<batch:validator>要素`を使用してジョブにバリデータを適用する。

| 非同期起動時におけるパラメータの妥当性検証について 非同期起動方式(DBポーリングやWebコンテナ)でも、同様にジョブ起動時に検証することは可能だが、 以下のようなタイミングでジョブを起動する前に検証することが望ましい。 DBポーリング ジョブ要求テーブルへのINSERT前 Webコンテナ Controller呼び出し時(@Validatedを付与する) 非同期起動の場合、結果は別途確認する必要が生じるため、パラメータ設定ミスのような 場合は早期にエラーを応答し、ジョブの要求をリジェクトすることが望ましい。 また、この時の妥当性検証において、`JobParametersValidator`を使う必要はない。 ジョブ要求テーブルへINSERTする機能や、Webコンテナ上のControllerは 多くの場合Spring Batchに依存していないはずであり、 `JobParametersValidator`を使用するためだけにSpring Batchに依存することは避けた方がよい。

#### 4.2.3. How to extend

##### 4.2.3.1. パラメータとプロパティの併用

Spring BatchのベースであるSpring Frameworkには、プロパティ管理の機能が備わっており、 環境変数やプロパティファイルに設定した値を扱うことができる。 詳細は、Macchinetta Server 1.x 開発ガイドラインの プロパティ管理 を参照。

プロパティとパラメータを組み合わせることで、大部分のジョブに共通的な設定をプロパティファイルに行ったうえで、一部をパラメータで上書きするといったことが可能になる。

| パラメータとプロパティが解決されるタイミングについて 前述のとおり、パラメータとプロパティは、機能を提供するコンポーネントが異なる。 Spring Batchはパラメータ管理の機能をもち、Spring Frameworkはプロパティ管理の機能をもつ。 この差は記述方法の差に現れている。 Spring Batchがもつ機能の場合 `#{jobParameters[xxx]}` Spring Frameworkがもつ機能の場合 `@Value("${xxx}")` また、それぞれの値が解決されるタイミングが異なる。 Spring Batchがもつ機能の場合 Application Contextを生成後、ジョブを実行するタイミングで設定される。 Spring Frameworkがもつ機能の場合 Application Contextの生成時に設定される。 よって、Spring Batchによるパラメータの値が優先される結果になる。 この点を念頭におくと、組み合わせる際に応用が効くため両者を区別して扱うこと。

以降、プロパティとパラメータを組み合わせて設定する方法について説明する。

環境変数による設定に加えて、コマンドライン引数で追加設定する場合

環境変数による設定に加えて、コマンドライン引数を使用してパラメータを設定する方法を説明する。
Bean定義においても同様に参照可能である。

JavaConfig
環境変数に加えてコマンドライン引数でパラメータを設定する例

    $ # Set environment variables
    $ export env1=aaa
    $ export env2=bbb

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        com.example.batch.jobs.JobDefinedConfig.JobDefinedConfig JOBID param3=ccc outputFile=/tmp/result.csv

XMLConfig
環境変数に加えてコマンドライン引数でパラメータを設定する例

    $ # Set environment variables
    $ export env1=aaa
    $ export env2=bbb

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        JobDefined.xml JOBID param3=ccc outputFile=/tmp/result.csv

Javaにおいて環境変数とパラメータを参照する例

    @Value("${env1}")  // (1)
    private String param1;

    @Value("${env2}")  // (1)
    private String param2;

    private String param3;

    @Value("#{jobParameters['param3']")  // (2)
    public void setParam3(String param3) {
        this.param3 = param3;
    }

表 36. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `@Value`アノテーションを使用して参照する環境変数を指定する。 参照する際の形式は`${環境変数名}`である。
(2) | `@Value`アノテーションを使用して参照するパラメータを指定する。 参照する際の形式は`#{jobParameters['パラメータ名']`である。

JavaConfig
環境変数をデフォルトとする場合の例

    $ # Set environment variables
    $ export env1=aaa

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        com.example.batch.jobs.JobDefinedConfig.JobDefinedConfig JOBID param1=bbb outputFile=/tmp/result.csv

XMLConfig
環境変数をデフォルトとする場合の例

    $ # Set environment variables
    $ export env1=aaa

    $ # Execute job
    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        JobDefined.xml JOBID param1=bbb outputFile=/tmp/result.csv

Javaにおいて環境変数をデフォルト値としてパラメータを参照する例

    @Value("#{jobParameters['param1'] ?: '${env1}'}")  // (1)
    public void setParam1(String param1) {
        this.param1 = param1;
    }

表 37. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | 環境変数をデフォルト値として`@Value`アノテーションを使用して参照するパラメータを指定する。 パラメータが設定されなかった場合、環境変数の値が設定される。

| 誤ったデフォルト値の設定方法 以下の要領で定義した場合、コマンドライン引数からparam1を設定しない場合に、 env1の値が設定されてほしいにも関わらず、param1にnullが設定されてしまうため注意すること。 誤ったデフォルト値の設定方法例
    @Value("${env1}")
    private String param1;

    @Value("#{jobParameters['param1']}")
    public void setParam1(String param1) {
      this.param1 = param1;
    }

### 4.3. 非同期実行(DBポーリング)

#### 4.3.1. Overview

DBポーリングによるジョブ起動について説明をする。

本機能は、チャンクモデルとタスクレットモデルとで同じ使い方になる。

##### 4.3.1.1. DBポーリングによるジョブの非同期実行とは

非同期実行させたいジョブを登録する専用のテーブル(以降、ジョブ要求テーブル)を一定周期で監視し、登録された情報をもとにジョブを非同期実行することをいう。
Macchinetta Batch 2.xでは、テーブルを監視しジョブを起動するモジュールを非同期バッチデーモンという名称で定義する。 非同期バッチデーモンは1つのJavaプロセスとして稼働し、1ジョブごとにプロセス内のスレッドを割り当てて実行する。

###### 4.3.1.1.1. TERASOLUNA Batch 5.xが提供する機能

TERASOLUNA Batch 5.xは、以下の機能を非同期実行(DBポーリング)として提供する。

表 38. 非同期実行(DBポーリング)の機能一覧
機能 | 説明
--- | ---
非同期バッチデーモン機能 | ジョブ要求テーブルポーリング機能を常駐実行させる機能
ジョブ要求テーブルポーリング機能 | ジョブ要求テーブルに登録された情報にもとづいてジョブを非同期実行する機能。 ジョブ要求テーブルのテーブル定義も合わせて提供する。

利用前提

ジョブ要求テーブルでは、ジョブ要求のみを管理する。要求されたジョブの実行状況および結果は、`JobRepository`に委ねる。 これら2つを通じてジョブのステータスを管理することを前提としている。

また、`JobRepository`にインメモリデータベースを使用すると、非同期バッチデーモン停止後に`JobRepository`がクリアされ、ジョブの実行状況および結果を参照できない。 そのため、`JobRepository`には永続性が担保されているデータベースを使用することを前提とする。

| インメモリデータベースの使用 `JobRepository`を参照せずにジョブ実行結果の成否を得る手段がある場合、インメモリデータベースで運用するケースも考えられる。 インメモリデータベースで長期連続運用をする場合、メモリリソースを大量消費してジョブ実行に悪影響を及ぼす可能性がある。 つまり、インメモリデータベースは、長期連続運用するには向かず、定期的に再起動する運用が望ましい。 それでも長期連続運用で利用したい場合は、定期的に`JobRepository`からデータを削除するなどのメンテナンス作業が必須である。 再起動する場合は、初期化を有効にしておけば再起動時に再作成されるため、メンテナンスは不要である。 初期化については、データベース関連の設定を参照。

###### 4.3.1.1.2. 活用シーン

非同期実行(DBポーリング)を活用するシーンを以下にいくつか示す。

表 39. 活用シーン一覧
活用シーン | 説明
--- | ---
ディレード処理 | オンライン処理と連携して、即時に完了する必要がなく、かつ、時間がかかる処理をジョブとして切り出したい場合。
処理時間が短いジョブの連続実行 | 1ジョブあたり数秒～数十秒の処理を連続実行する場合。 非同期実行(DBポーリング)を活用することで、1ジョブごとにJavaプロセスの起動・終了によるリソースの圧迫を回避できる。 また、起動・終了処理を割愛することに繋がるためジョブの実行時間を短縮することが可能となる。
大量にあるジョブの集約 | 処理時間が短いジョブの連続実行と同様である。

| 非同期実行(DBポーリング)と非同期実行(Webコンテナ)を使い分けるポイント 以下に該当する場合は非同期実行(DBポーリング)の利用が想定できる。 バッチ処理にWebAPサーバを導入することにハードルがある 可用性を担保する際に、データベースのみを考慮すればよい ただし、非同期実行(DBポーリング)では、データベースにアクセスが集中するため、非同期実行(Webコンテナ)ほど性能が出ない可能性がある。 データベースへのアクセス集中が懸念材料になる場合は、非同期実行(Webコンテナ)の利用も検討してほしい。

| Spring Batch Integrationを採用しない理由 Spring Batch Integrationを利用して同様の機能を実現することは可能である。 しかし、Spring Batch Integrationを使用すると非同期実行以外の要素も含めた技術要素の理解・取得が必要となる。 それにより、本機能の理解/活用/カスタマイズが難しくなるのを避けるため、Spring Batch Integrationの適用は見送っている。

| 非同期実行(DBポーリング)での注意点 1ジョブあたり数秒にも満たない超ショートバッチを大量に実行する場合、`JobRepository`も含めてデータベースへのアクセスが都度発生する。 この点に起因する性能劣化もあり得るため、超ショートバッチの大量処理は、非同期実行(DBポーリング)には向いていない。 本機能を利用する際はこの点を踏まえ、目標性能を満たせるか十分に検証をすること。

#### 4.3.2. Architecture

##### 4.3.2.1. DBポーリングの処理シーケンス

DBポーリングの処理シーケンスについて説明する。

図 18. DBポーリングのシーケンス図

1. `AsyncBatchDaemon`をshなどから起動する。

2. `AsyncBatchDaemon`は、起動時にジョブを定義したBean定義ファイルをすべて読み込む。

3. `AsyncBatchDaemon`は、一定間隔でポーリングするために`TaskScheduler`を起動する。

   - `TaskScheduler`は、一定間隔で特定の処理を起動する。

4. `TaskScheduler`は、`JobRequestPollTask`(ジョブ要求テーブルをポーリングする処理)を起動する。

5. `JobRequestPollTask`は、ジョブ要求テーブルからポーリングステータスが未実行(INIT)のレコードを取得する。

   - 一定件数をまとめて取得する。デフォルトは3件。

   - 対象のレコードが存在しない場合は、一定間隔を空けて再度ポーリングを行う。デフォルトは10秒間隔。

6. `JobRequestPollTask`は、レコードの情報にもとづいて、ジョブをスレッドに割り当てて実行する。

7. `JobRequestPollTask`は、ジョブ要求テーブルのポーリングステータスをポーリング済み(POLLED)へ更新する。

   - ジョブの同時実行数に達している場合は、取得したレコードから起動できないレコードを破棄し、次回ポーリング処理時にレコードを再取得する。

8. スレッドに割り当てられたジョブは、`JobOperator`によりジョブを開始する。

9. 実行したジョブのジョブ実行ID(Job execution id)を取得する。

10. `JobRequestPollTask`は、ジョブ実行時に取得したジョブ実行IDにもとづいて、ジョブ要求テーブルのポーリングステータスをジョブ実行済み(EXECUTED)に更新する。

| 処理シーケンスの補足 Spring Batchのリファレンスでは、`JobLauncher`に`AsyncTaskExecutor`を設定することで非同期実行が実現できることを示している。 しかし、この方法を採用すると`AsyncTaskExecutor`がジョブ実行が出来ない状態を検知できない。 これは、ジョブに割り当てられるスレッドがない時などに発生し、その結果以下の事象に繋がる可能性がある。 ジョブが実行できないにも関わらず、ジョブの起動をしようとし続け不要な処理をしてしまう スレッドが空いたタイミングによっては、ポーリングした順番にジョブが起動せず、ジョブ要求テーブル上ランダムに起動するように見えてしまう この事象を回避するため前述の処理シーケンスとなっている。

##### 4.3.2.2. ポーリングするテーブルについて

非同期実行(DBポーリング)でポーリングを行うテーブルについて説明する。

以下データベースオブジェクトを必要とする。

- ジョブ要求テーブル(必須)

- ジョブシーケンス(データベース製品によっては必須)

  - データベースがカラムの自動採番に対応していない場合に必要となる。

###### 4.3.2.2.1. ジョブ要求テーブルの構造

以下に、TERASOLUNA Batch 5.xが対応しているデータベース製品のうち、PostgreSQLの場合を示す。 その他のデータベースについては、TERASOLUNA Batch 5.xのjarに同梱されているDDLを参照。

| ジョブ要求テーブルへ格納する文字列について メタデータテーブルと同様にジョブ要求テーブルのカラムは、明示的に文字データ型を文字数定義に設定するDDLを提供する。

表 40. batch_job_request (PostgreSQLの場合)
カラム名 | データ型 | 制約 | 説明
--- | --- | --- | ---
job_seq_id | bigserial (別途シーケンスを定義する場合は、bigintとする) | NOT NULL PRIMARY KEY | ポーリング時に実行するジョブの順序を決める番号。 データベースの自動採番機能を利用。
job_name | varchar(100) | NOT NULL | 実行するジョブ名。 ジョブ実行時の必須パラメータ。
job_parameter | varchar(200) | - | 実行するジョブに渡すパラメータ。 複数パラメータを指定する場合は、同期型実行と同様に各パラメータを空白区切り(下記参照)にする必要がある。 {パラメータ名}={パラメータ値} {パラメータ名}={パラメータ値}…​
job_execution_id | bigint | - | ジョブ実行時に払い出されるID。 このIDをキーにして`JobRepository`を参照する。
polling_status | varchar(10) | NOT NULL | ポーリング処理状況。 INIT : 未実行 POLLED: ポーリング済み EXECUTED : ジョブ実行済み
create_date | TIMESTAMP | NOT NULL | ジョブ要求のレコードを登録した日時。
update_date | TIMESTAMP | - | ジョブ要求のレコードを更新した日時。

| ジョブパラメータを複数指定する場合の留意点 Spring Batchが提供するSimpleJobOperatorの仕様変更に伴い、Macchinetta Batch 2.xでもジョブパラメータを複数指定する場合の区切り文字を変更している。 既にカンマ区切りが使用されている場合、空白区切りに変更すること。 詳細は、Spring Batch/BATCH-1461を参照されたい。

DDLは以下のとおり。

    CREATE TABLE IF NOT EXISTS batch_job_request (
        job_seq_id bigserial PRIMARY KEY,
        job_name varchar(100) NOT NULL,
        job_parameter varchar(200),
        job_execution_id bigint,
        polling_status varchar(10) NOT NULL,
        create_date timestamp NOT NULL,
        update_date timestamp
    );

###### 4.3.2.2.2. ジョブ要求シーケンスの構造

データベースがカラムの自動採番に対応していない場合は、シーケンスによる採番が必要になる。
以下に、TERASOLUNA Batch 5.xが対応しているデータベース製品のうち、PostgreSQLの場合を示す。
その他のデータベースについては、TERASOLUNA Batch 5.xのjarに同梱されているDDLを参照。

DDLは以下のとおり。

    CREATE SEQUENCE batch_job_request_seq MAXVALUE 9223372036854775807 NO CYCLE;

| カラムの自動採番に対応しているデータベースについては、TERASOLUNA Batch 5.xのjarに同梱されているDDLにジョブ要求シーケンスは定義されていない。 シーケンスの最大値を変更したい場合などには`job_seq_id`のデータ型を自動採番の定義から数値型 (PostgreSQL場合だと、`bigserial`から`bigint`)に変更した上で、 ジョブ要求シーケンスを定義すると良い。

###### 4.3.2.2.3. ポーリングステータス(polling_status)の遷移パターン

ポーリングステータスの遷移パターンを下表に示す。

表 41. ポーリングステータスの遷移パターン一覧
遷移元 | 遷移先 | 説明
--- | --- | ---
INIT | INIT | 同時実行数に達して、ジョブの実行を拒否された場合はステータスの変更はない。 次回ポーリング時にポーリング対象のレコードとなる。
INIT | POLLED | ポーリングしたジョブがスレッドに割り当てられてた直後に遷移する。 このステータスに更新された後、ジョブが起動される。
POLLED | EXECUTED | ジョブの実行が終了した時に遷移する。 ジョブの正常終了、異常終了に関係なくこのステータスに更新される。

###### 4.3.2.2.4. ジョブ要求取得SQL

ジョブの同時実行数分のジョブ要求を取得するため、ジョブ要求取得SQLでは取得する件数を制限している。
ジョブ要求取得SQLは使用するデータベース製品およびバージョンによって異なる記述になる。 そのため、TERASOLUNA Batch 5.xが提供しているSQLでは対応できない場合がある。
その場合はジョブ要求テーブルのカスタマイズを参考に、 `BatchJobRequestRepository.xml`のSQLMapを再定義する必要がある。
提供しているSQLについては、TERASOLUNA Batch 5.xのjarに同梱されている`BatchJobRequestRepository.xml`を参照。

##### 4.3.2.3. ジョブの起動について

ジョブの起動方法について説明をする。

TERASOLUNA Batch 5.xのジョブ要求テーブルポーリング機能内部では、 Spring Batchから提供されている`JobOperator`の`start`メソッドでジョブを起動する。

Macchinetta Batch 2.xでは、非同期実行(DBポーリング)で起動したジョブのリスタートは、 コマンドラインからの実行をガイドしている。 そのため、`JobOperator`には`start`以外にも`restart`などの起動メソッドがあるが、 `start`メソッド以外は使用していない。

`start`メソッドの引数

jobName

  ジョブ要求テーブルの`job_name`に登録した値を設定する。

jobParametrers

  ジョブ要求テーブルの`job_parameters`に登録した値を設定する。

##### 4.3.2.4. DBポーリング処理で異常が発生した場合について

DBポーリング処理で異常が発生した場合について説明する。

###### 4.3.2.4.1. データベース接続障害

障害が発生した時点で行われていた処理別に振る舞いを説明する。

ジョブ要求テーブルからのレコード取得時

  - `JobRequestPollTask`はエラーとなるが、次回のポーリングにて`JobRequestPollTask`が再実行される。

ポーリングステータスをINITからPOLLEDに変更する間

  - `JobOperator`によるジョブ実行前に`JobRequestPollTask`はエラー終了する。ポーリングステータスは、INITのままになる。

  - 接続障害回復後に行われるポーリング処理では、ジョブ要求テーブルに変更がないため実行対象となり、次回ポーリング時にジョブが実行される。

ポーリングステータスをPOLLEDからEXECUTEDに変更する間

  - `JobRequestPollTask`は、ジョブ実行IDをジョブ要求テーブルに更新することができずにエラー終了する。ポーリングステータスは、POLLEDのままになる。

  - 接続障害回復後に行われるポーリング処理の対象外となり、障害時のジョブは実行されない。

  - ジョブ要求テーブルからジョブ実行IDを知ることができないため、ジョブの最終状態をログや`JobRepository`から判断し、必要に応じてジョブの再実行など回復処理を行う。

| `JobRequestPollTask`で例外が発生しても、即座に自動復旧しようとはしない。以下に理由を示す。 `JobRequestPollTask`は、一定間隔で起動するため、これに委ねることで(即座ではないが)自動復旧できる。 障害発生時に即座にリトライしても回復できるケースは稀であり、かえってリトライにより負荷を発生してしまう可能性がある。

###### 4.3.2.4.2. 非同期バッチデーモンのプロセス異常終了

非同期バッチデーモンのプロセスが異常終了した場合は、実行中ジョブのトランザクションは暗黙的にロールバックされる。
ポーリングステータスによる状態はデータベース接続障害と同じになる。

##### 4.3.2.5. DBポーリング処理の停止について

非同期バッチデーモン(`AsyncBatchDaemon`)は、ファイルの生成によって停止する。 ファイルが生成されたことを確認後、ポーリング処理を空振りさせ、起動中ジョブの終了を可能な限り待ってから停止する。

##### 4.3.2.6. 非同期実行特有のアプリケーション構成となる点について

非同期実行における特有の構成を説明する。

###### 4.3.2.6.1. ApplicationContextの構成

非同期バッチデーモンは、非同期実行専用の`AsyncBatchDaemonConfig.java/async-batch-daemon.xml`をApplicationContextとして読み込む。 同期実行でも使用している`LaunchContextConfig.java/launch-context.xml`の他に次の構成を追加している。

非同期実行設定

  `JobRequestPollTask`などの非同期実行に必要なBeanを定義している。

ジョブ登録設定

  非同期実行として実行するジョブは、`org.springframework.batch.core.configuration.support.AutomaticJobRegistrar`で登録を行う。 `AutomaticJobRegistrar`を用いることで、ジョブ単位にコンテキストのモジュール化を行うことができる。

| モジュール化とは モジュール化とは、DIコンテナのコンテキストを「共通定義-各ジョブ定義」といった階層構造に分離することで、 各ジョブ定義にあるBeanをコンテキスト間で独立させることである。 この階層構造により、各ジョブ定義内からのBean参照は、まず同一ジョブのBean定義を参照し、 ジョブ定義のBean定義にない場合、次いで共通定義で定義されたBeanを参照することになる。

| 非同期実行では同期実行と異なり、1つのバッチプロセス上に複数の異なるジョブ定義が準備され、さらに各ジョブのコンテキストは競合することなく並列実行できることが求められる。 ジョブ定義によるコンテキストのモジュール化により、各ジョブ定義から生成されるBeanをコンテキストごとに独立させることができる。 このため非同期実行では`AutomaticJobRegistrar`を明示的に使用し、ジョブ定義のモジュール化を行うこと。

###### 4.3.2.6.2. Bean定義の構成

ジョブのBean定義は、同期実行のBean定義と同じ構成でよい。ただし、以下の注意点がある。

- `AutomaticJobRegistrar`でジョブを登録する際、ジョブのBeanIDはアプリケーションコンテキストのモジュール化によってDIコンテナ全体としては重複が許されるが、 目的のジョブを起動する際に一意に識別することができなくなるため、ジョブのBeanIDの重複は避けること。

- ステップのBeanIDも重複しないことが望ましい。

  - 指針として設計時にBeanIDの命名規則を`{ジョブID}.{ステップID}`とすることで、BeanIDとしての一意性を保つことができる。

| ジョブのBean定義における`JobBaseContextConfig.java/job-base-context.xml`のインポートは、同期実行と非同期実行で挙動が異なる。 同期実行では、`JobBaseContextConfig.java/job-base-context.xml`から更に`LaunchContextConfig.java/launch-context.xml`をインポートする。 非同期実行では、`JobBaseContextConfig.java/job-base-context.xml`から`LaunchContextConfig.java/launch-context.xml`をインポートしない。 その代わりに`AsyncBatchDaemon`がロードする`AsyncBatchDaemonConfig.java/async-batch-daemon.xml`にて、 `LaunchContextConfig.java/launch-context.xml`をインポートする。 これは、Spring Batchを起動する際に必要な各種Beanは各ジョブごとにインスタンス化する必要はないことに起因する。 Spring Batchの起動に必要な各種Beanは各ジョブの親となる共通定義(`AsyncBatchDaemonConfig.java/async-batch-daemon.xml`)にて1つだけ生成すればよい。

#### 4.3.3. How to use

##### 4.3.3.1. 各種設定

###### 4.3.3.1.1. ポーリング処理の設定

非同期実行に必要な設定は、`batch-application.properties`で行う。

batch-application.properties

    # (1)
    # Admin DataSource settings.
    admin.jdbc.driver=org.postgresql.Driver
    admin.jdbc.url=jdbc:postgresql://localhost:5432/postgres
    admin.jdbc.username=postgres
    admin.jdbc.password=postgres

    # TERASOLUNA AsyncBatchDaemon settings.
    # (2)
    async-batch-daemon.scheduler.size=1
    # (3)
    async-batch-daemon.schema.script=classpath:org/terasoluna/batch/async/db/schema-postgresql.sql
    # (4)
    async-batch-daemon.job-concurrency-num=3
    # (5)
    async-batch-daemon.job-await-termination-seconds=600
    # (6)
    async-batch-daemon.polling-interval=10000
    # (7)
    async-batch-daemon.polling-initial-delay=1000
    # (8)
    async-batch-daemon.polling-stop-file-path=/tmp/stop-async-batch-daemon

表 42. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | ジョブ要求テーブルが格納されているデータベースへの接続設定。 デフォルトでは`JobRepository`の設定を使用する。
(2) | DBポーリング処理で起動される`TaskScheduler`のスレッドプールサイズ設定。 デフォルトは1(シングルスレッド)。 (6)の説明にあるように、前のポーリング処理の完了から、次のポーリング処理の開始まで待ち時間を設けるようにしているため、ここで`TaskScheduler`のスレッド数を上げても並列には実行されない。そのため、基本的にはデフォルトのままとするほうが良い。
(3) | ジョブ要求テーブルを定義するDDLのパス。 非同期バッチデーモン起動時にジョブ要求テーブルがない場合は、自動生成される。 これは主に試験用機能であり、`batch-application.properties`内の `data-source.initialize.enabled`で実行可否を設定できる。 詳細な定義は`AsyncBatchDaemonConfig.java`内の`dataSourceInitializer`メソッド、もしくは`async-batch-daemon.xml`内の`<jdbc:initialize-database>`を参照。
(4) | ポーリング時に一括で取得する件数の設定。 この設定値は同時並行数(`ThreadPoolTaskExecutor`のスレッドプールサイズ)としても用いる。 動作するマシンのコア数に応じて適切に設定しないと、性能問題等を引き起こす可能性がある。
(5) | 非同期バッチデーモンの停止要求からジョブが終了(コンテナの破棄)するまで待機する最大秒数の設定。 実行中の非同期実行ジョブがなければ即時終了となる。
(6) | ポーリング周期の設定。単位はミリ秒。 前回タスクの実行完了時点から指定時間後にタスクを実行する。
(7) | ポーリング初回起動遅延時間の設定。単位はミリ秒。
(8) | 終了ファイルパスの設定。

| プロパティアクセスの仕様変更への対応 デフォルトのプロパティアクセスの優先順位は、下記である。 プロパティファイル＞システムプロパティ 以前のバージョンでは、`<context:property-placeholder>`タグの属性`system-properties-mode`の値でプロパティアクセスの優先順位を制御可能だった。 たとえば`"OVERRIDE"`を指定すると、システムプロパティ（環境変数等）を優先的に読み込むようになり、プロパティファイルのプロパティを上書きすることができた。ところが、バージョン`5.5.0.RELEASE`から`system-properties-mode`による制御が不可となったばかりか、設定値によっては`@Value`によるプロパティ値取得が機能しなくなった。 上記の仕様変更にともなう、意図せぬ動作不良を回避するために、下記を実施すること。 属性`system-properties-mode`の値を`"ENVIRONMENT"`に変更、もしくは属性そのものを削除する。これをしないと、`@Value`によるプロパティ値取得に失敗する 以前のバージョンで属性`system-properties-mode`に`"OVERRIDE"`を指定している場合、プロパティアクセスの優先順位が逆となるので、`5.5.0.RELEASE`へマイグレーションする場合はアプリケーションの修正が必要な場合がある。もしプロパティファイルとシステムプロパティに同一のプロパティを設定している場合は、プロパティファイルとシステムプロパティのプロパティ値を入れ替えるか、プロパティ名が重複しないように修正する等の対応が必要となる

###### 4.3.3.1.2. ジョブの設定

JavaConfig

非同期実行する対象のジョブは、`AsyncBatchDaemonConfig.java`の`automaticJobRegistrar`に設定する。
`ApplicationContextFactoryHelper` は JavaConfigで `ApplicationContextFactory` の生成を補助するヘルパクラスである。コンストラクタには各ジョブで親となる共通定義の `ApplicationContext` を指定する。
以下に初期設定を示す。

com.example.batch.tutorial.config.AsyncBatchDaemonConfig.java

    @Bean
    public AutomaticJobRegistrar automaticJobRegistrar(JobRegistry jobRegistry,
            ApplicationContextFactory[] applicationContextFactories) throws Exception {
        final AutomaticJobRegistrar automaticJobRegistrar = new AutomaticJobRegistrar();
        final DefaultJobLoader defaultJobLoader = new DefaultJobLoader();
        defaultJobLoader.setJobRegistry(jobRegistry);
        automaticJobRegistrar.setApplicationContextFactories(
                applicationContextFactories);
        automaticJobRegistrar.setJobLoader(defaultJobLoader);
        automaticJobRegistrar.afterPropertiesSet();
        return automaticJobRegistrar;
    }

    @Bean
    public ApplicationContextFactory[] applicationContextFactories(
            final ApplicationContext ctx) throws IOException {
        return new ApplicationContextFactoryHelper(ctx).load(
                "classpath:org/terasoluna/batch/jobs/**/*.class", // For all (1)
                "classpath:org/terasoluna/batch/jobs/async/**/*.class", // For the async directory and below (2)
                "classpath:org/terasoluna/batch/jobs/CASE100/SpecialJob.class"); // For a specific job (3)
    }

表 43. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | jarの中に存在するジョブ定義がすべて非同期実行することを前提に設計・実装される場合は、このように指定できる。
(2) | jarの中に非同期で実行することを想定していないジョブが存在する場合は、非同期実行を想定しているジョブのみが読み込まれるように指定すること。
(3) | 特定のジョブのみが非同期実行対象の場合はジョブ定義単体で指定すること。

XMLConfig

非同期実行する対象のジョブは、`async-batch-daemon.xml`の`automaticJobRegistrar`に設定する。
以下に初期設定を示す。

META-INF/spring/async-batch-daemon.xml

    <bean id="automaticJobRegistrar"
          class="org.springframework.batch.core.configuration.support.AutomaticJobRegistrar">
        <property name="applicationContextFactories">
            <bean class="org.springframework.batch.core.configuration.support.ClasspathXmlApplicationContextsFactoryBean">
                <property name="resources">
                    <list>
                        <!-- For all -->
                        <value>classpath:/META-INF/jobs/**/*.xml</value> <!-- (1) -->
                        <!-- For the async directory and below -->
                        <value>classpath:/META-INF/jobs/async/**/*.xml</value> <!-- (2) -->
                        <!-- For a specific job -->
                        <value>classpath:/META-INF/jobs/CASE100/SpecialJob.xml</value> <!-- (3) -->
                    </list>
                </property>
            </bean>
        </property>
        <property name="jobLoader">
            <bean class="org.springframework.batch.core.configuration.support.DefaultJobLoader"
                  p:jobRegistry-ref="jobRegistry" />
        </property>
    </bean>

表 44. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | jarの中に存在するジョブ定義がすべて非同期実行することを前提に設計・実装される場合は、このように指定できる。
(2) | jarの中に非同期で実行することを想定していないジョブが存在する場合は、非同期実行を想定しているジョブのみが読み込まれるように指定すること。
(3) | 特定のジョブのみが非同期実行対象の場合はジョブ定義単体で指定すること。

| ジョブパラメータの入力値検証 `JobPollingTask`は、ジョブ要求テーブルから取得したレコードについて妥当性検証をしない。 よって、テーブルに登録する側にてジョブ名やジョブパラメータについて検証することが望ましい。 ジョブ名が誤っていると、ジョブを起動するが見つからず、例外が発生してしまう。 ジョブパラメータが誤っていると、ジョブは起動するが誤動作してしまう。 ジョブパラメータに限っては、ジョブ起動後に検証を行うことができる。ジョブパラメータの検証については、 "パラメータの妥当性検証"を参照。

| ジョブ設計上の留意点 非同期実行(DBポーリング)の特性上、同一ジョブの並列実行が可能になっているので、並列実行した場合に同一ジョブが影響を与えないようにする必要がある。

##### 4.3.3.2. 非同期処理の起動から終了まで

非同期バッチデーモンの起動と終了、ジョブ要求テーブルへの登録方法について説明する。

###### 4.3.3.2.1. 非同期バッチデーモンの起動

TERASOLUNA Batch 5.xが提供する、`AsyncBatchDaemon`を起動する。

非同期バッチデーモンは、org.terasoluna.batch.async.db.AsyncBatchDaemonの第一引数にBean定義ファイルのパス(`AsyncBatchDaemonConfig.java/async-batch-daemon.xml`)を指定して起動する。

JavaConfig
AsyncBatchDaemonの起動

    $ # Start AsyncBatchDaemon
    $ java -cp dependency/* org.terasoluna.batch.async.db.AsyncBatchDaemon com.example.batch.tutorial.config.AsyncBatchDaemonConfig

この場合、`com.example.batch.tutorial.config.AsyncBatchDaemonConfig.java`を読み込み各種Beanを生成する。

また、別途カスタマイズした`AsyncBatchDaemonConfig.java`を利用したい場合は第一引数に指定して`AsyncBatchDaemon`を起動することで実現できる。

カスタマイズしたcom.example.batch.tutorial.config.CustomizedAsyncBatchDaemonConfigを利用する場合

    $ # Start AsyncBatchDaemon
    $ java -cp dependency/* org.terasoluna.batch.async.db.AsyncBatchDaemon com.example.batch.tutorial.config.CustomizedAsyncBatchDaemonConfig

`AsyncBatchDaemonConfig.java`のカスタマイズは、ごく一部の設定を変更する場合は直接修正してよい。
しかし、大幅な変更を加える場合や、後述する複数起動にて複数の設定を管理する場合は、 別途ファイルを作成して管理するほうが扱いやすい。
ユーザの状況に応じて選択すること。

XMLConfig
AsyncBatchDaemonの起動

    $ # Start AsyncBatchDaemon
    $ java -cp dependency/* org.terasoluna.batch.async.db.AsyncBatchDaemon

XMLConfigでは、第一引数は省略可能である。その場合、`META-INF/spring/async-batch-daemon.xml`を読み込み各種Beanを生成する。

また、別途カスタマイズした`async-batch-daemon.xml`を利用したい場合は第一引数に指定して`AsyncBatchDaemon`を起動することで実現できる。
引数に指定するBean定義ファイルは、クラスパスからの相対パスで指定すること。
なお、第二引数以降は無視される。

カスタマイズしたMETA-INF/spring/customized-async-batch-daemon.xmlを利用する場合

    $ # Start AsyncBatchDaemon
    $ java -cp dependency/* org.terasoluna.batch.async.db.AsyncBatchDaemon META-INF/spring/customized-async-batch-daemon.xml

`async-batch-daemon.xml`のカスタマイズは、ごく一部の設定を変更する場合は直接修正してよい。
しかし、大幅な変更を加える場合や、後述する複数起動にて複数の設定を管理する場合は、 別途ファイルを作成して管理するほうが扱いやすい。
ユーザの状況に応じて選択すること。

| dependency配下には、実行に必要なjar一式が格納されている前提とする。

| 第一引数を省略した場合のBean定義ファイルパス 第一引数を省略した場合、Bean定義ファイルパスはXMLConfigのBean定義ファイルである`META-INF/spring/async-batch-daemon.xml`をデフォルトのパスとして起動する。 JavaConfigの場合、Bean定義ファイルはjavaパッケージ階層の中(`src/main/java`)に配置されるため、プロジェクトによってパスは異なり、デフォルト値としてあらかじめ決定することができない。 よって、JavaConfigにおいては第一引数のBean定義ファイルは省略せず必ず指定する必要がある。

###### 4.3.3.2.2. ジョブの要求

INSERT文のSQLを発行することでジョブ要求テーブルに登録を行う。

PostgreSQLの場合

    INSERT INTO batch_job_request(job_name,job_parameter,polling_status,create_date)
    VALUES ('job01', 'param1=dummy param2=100', 'INIT', current_timestamp);

###### 4.3.3.2.3. 非同期バッチデーモンの停止

`batch-application.properties`に設定した終了ファイルを置く。

    $ touch /tmp/stop-async-batch-daemon

| 非同期バッチデーモン起動前に終了ファイルがある場合 非同期バッチデーモン起動前に終了ファイルがある場合、非同期バッチデーモンは即時終了する。 非同期バッチデーモンは、終了ファイルがない状態で起動する必要がある。

##### 4.3.3.3. ジョブのステータス確認

ジョブの状態管理はSpring Batchから提供される`JobRepository`で行い、ジョブ要求テーブルではジョブのステータスを管理しない。 ジョブ要求テーブルでは`job_execution_id`のカラムをもち、このカラムに格納される値により個々の要求に対するジョブのステータスを確認できるようにしている。 ここでは、SQLを直接発行してジョブのステータスを確認する簡単な例を示す。 ジョブステータス確認の詳細は、"状態の確認"を参照。

PostgreSQLの場合

    SELECT job_execution_id FROM batch_job_request WHERE job_seq_id = 1;

    job_execution_id
    ----------------
                  2
    (1 row)

    SELECT * FROM batch_job_execution WHERE job_execution_id = 2;

    job_execution_id | version | job_instance_id |       create_time       |       start_time        |        end_time         |  status   | exit_code | exit_message |
    ocation
    ------------------+---------+-----------------+-------------------------+-------------------------+-------------------------+-----------+-----------+--------------+-
    --------
                  2 |       2 |               2 | 2017-02-06 20:54:02.263 | 2017-02-06 20:54:02.295 | 2017-02-06 20:54:02.428 | COMPLETED | COMPLETED |              |
    (1 row)

##### 4.3.3.4. ジョブが異常終了した後のリカバリ

異常終了したジョブのリカバリに関する基本事項は、"処理の再実行"を参照。 ここでは、非同期実行特有の事項について説明をする。

###### 4.3.3.4.1. リラン

異常終了したジョブのリランは、ジョブ要求テーブルに別レコードとしてINSERTすることで行う。

###### 4.3.3.4.2. リスタート

異常終了したジョブをリスタートする場合は、コマンドラインから同期実行ジョブとして実行する。 コマンドラインからの実行する理由は、「意図したリスタート実行なのか意図しない重複実行であるかの判断が難しいため、運用で混乱をきたす可能性がある」ためである。
リスタート方法は"ジョブのリスタート"を参照。

###### 4.3.3.4.3. 停止

1. 処理時間が想定を超えて停止していない場合は、コマンドラインからの停止を試みる。 停止方法は"ジョブの停止"を参照。

2. コマンドラインからの停止も受け付けない場合は、非同期バッチデーモンの停止により、非同期バッチデーモンを終了させる。

3. 非同期バッチデーモンも終了できない状態になっている場合は、非同期バッチデーモンのプロセスを強制終了させる。

| 非同期バッチデーモンを終了させる場合は、他のジョブに影響がないように十分に注意して行う。

##### 4.3.3.5. 環境配備について

ジョブのビルドとデプロイは同期実行と同じである。ただし、ジョブの設定にもあるとおり非同期実行するジョブの絞込みをしておくことが重要である。

##### 4.3.3.6. 累積データの退避について

非同期バッチデーモンを長期運用していると`JobRepository`とジョブ要求テーブルに膨大なデータが累積されていく。以下の理由によりこれらの累積データを退避させる必要がある。

- 膨大なデータ量に対してデータを検索/更新する際の性能劣化

- IDの採番用シーケンスが周回することによるIDの重複

テーブルデータの退避やシーケンスのリセットについては、利用するデータベースのマニュアルを参照。 また、`JobRepository`への検索/更新で性能劣化を懸念している場合は、"IndexによるJobRepositoryの性能改善"を参照。

以下に退避対象のテーブルおよびシーケンスの一覧を示す。

表 45. 退避対象一覧
テーブル/シーケンス | 提供しているフレームワーク
--- | ---
batch_job_request | TERASOLUNA Batch 5.x
batch_job_request_seq
batch_job_instance | Spring Batch
batch_job_execution
batch_job_execution_params
batch_job_execution_context
batch_step_execution
batch_step_execution_context
batch_job_seq
batch_job_execution_seq
batch_step_execution_seq

| 自動採番カラムのシーケンス 自動採番のカラムに対して自動的にシーケンスが作成されている場合があるので、忘れずにそのシーケンスも退避対象に含める。

#### 4.3.4. How to extend

##### 4.3.4.1. ジョブ要求テーブルのカスタマイズ

ジョブ要求テーブルは、取得レコードの抽出条件を変更するためにカラム追加をしてカスタマイズすることができる。 ただし、`JobRequestPollTask`からSQLを発行する際に渡せる項目は、 `BatchJobRequest`の項目のみである。

ジョブ要求テーブルのカスタマイズによる拡張手順は以下のとおり。

1. ジョブ要求テーブルのカスタマイズ

2. `BatchJobRequestRepository`インタフェースの拡張インタフェースの作成

3. カスタマイズしたテーブルを使用したSQLMapの定義

4. `AsyncBatchDaemonConfig.java/async-batch-daemon.xml`のBean定義の修正

カスタマイズ例として以下のようなものがある。

- 優先度カラムによるジョブ実行順序の制御の例

- グループIDによる複数プロセスによる分散処理

以降、この2つの例について、拡張手順を説明する。

###### 4.3.4.1.1. 優先度カラムによるジョブ実行順序の制御の例

1. ジョブ要求テーブルのカスタマイズ

ジョブ要求テーブルに優先度カラム(priority)を追加する。

優先度カラムの追加 (PostgreSQLの場合)

    CREATE TABLE IF NOT EXISTS batch_job_request (
        job_seq_id bigserial PRIMARY KEY,
        job_name varchar(100) NOT NULL,
        job_parameter varchar(200),
        priority int NOT NULL,
        job_execution_id bigint,
        polling_status varchar(10) NOT NULL,
        create_date timestamp NOT NULL,
        update_date timestamp
    );

2. `BatchJobRequestRepository`インタフェースの拡張インタフェースの作成

`BatchJobRequestRepository`インタフェースを拡張したインタフェースを作成する。

拡張インタフェース

    // (1)
    public interface CustomizedBatchJobRequestRepository extends BatchJobRequestRepository {
        // (2)
    }

表 46. 拡張ポイント
項番 | 説明
--- | ---
(1) | `BatchJobRequestRepository`を拡張する。
(2) | メソッドは追加しない。

3. カスタマイズしたテーブルを使用したSQLMapの定義

優先度を順序条件にしたSQLをSQLMapに定義する。

SQLMap定義(CustomizedBatchJobRequestRepository.xml)

    <!-- (1) -->
    <mapper namespace="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch04.asyncjobwithdb.repository.CustomizedBatchJobRequestRepository">

        <select id="find" resultType="org.terasoluna.batch.async.db.model.BatchJobRequest">
            SELECT
                job_seq_id AS jobSeqId,
                job_name AS jobName,
                job_parameter AS jobParameter,
                job_execution_id AS jobExecutionId,
                polling_status AS pollingStatus,
                create_date AS createDate,
                update_date AS updateDate
            FROM
                batch_job_request
            WHERE
                polling_status = 'INIT'
            ORDER BY
                priority ASC,   <!--(2) -->
                job_seq_id ASC
            LIMIT #{pollingRowLimit}
        </select>

        <!-- (3) -->
        <update id="updateStatus">
            UPDATE
                batch_job_request
            SET
                polling_status = #{batchJobRequest.pollingStatus},
                job_execution_id = #{batchJobRequest.jobExecutionId},
                update_date = #{batchJobRequest.updateDate}
            WHERE
                job_seq_id = #{batchJobRequest.jobSeqId}
            AND
                polling_status = #{pollingStatus}
        </update>

    </mapper>

表 47. 拡張ポイント
項番 | 説明
--- | ---
(1) | `BatchJobRequestRepository`の拡張インタフェースをFQCNでnamespaceに設定する。
(2) | priorityをORDER句へ追加する。
(3) | 更新SQLは変更しない。

4. `AsyncBatchDaemonConfig.java/async-batch-daemon.xml`のBean定義の修正

(2)で作成した拡張インタフェースを`batchJobRequestRepository`に設定する。

JavaConfig
jp.co.ntt.fw.macchinetta.batch.config.AsyncBatchDaemonConfig.java

    @Bean
    public MapperFactoryBean<CustomizedBatchJobRequestRepository> batchJobRequestRepository(
            @Qualifier("adminSqlSessionFactory") SqlSessionFactory adminSqlSessionFactory) {
        final MapperFactoryBean<CustomizedBatchJobRequestRepository> mapperFactoryBean = new MapperFactoryBean<>();
        mapperFactoryBean.setMapperInterface(CustomizedBatchJobRequestRepository.class); // (1)
        mapperFactoryBean.setSqlSessionFactory(adminSqlSessionFactory);
        return mapperFactoryBean;
    }

表 48. 拡張ポイント
項番 | 説明
--- | ---
(1) | `BatchJobRequestRepository`の拡張インタフェースを`setMapperInterface`メソッドで設定する。

XMLConfig
META-INF/spring/async-batch-daemon.xml

     <!--(1) -->
    <bean id="batchJobRequestRepository"
          class="org.mybatis.spring.mapper.MapperFactoryBean"
          p:mapperInterface="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch04.asyncjobwithdb.repository.CustomizedBatchJobRequestRepository"
          p:sqlSessionFactory-ref="adminSqlSessionFactory" />

表 49. 拡張ポイント
項番 | 説明
--- | ---
(1) | `BatchJobRequestRepository`の拡張インタフェースをFQCNで`mapperInterface`属性に設定する。

###### 4.3.4.1.2. グループIDによる複数プロセスによる分散処理

`AsyncBatchDaemon`起動時に環境変数でグループIDを指定して、対象のジョブを絞り込む。

1. ジョブ要求テーブルのカスタマイズ

ジョブ要求テーブルにグループIDカラム(group_id)を追加する。

グループIDカラムの追加 (PostgreSQLの場合)

    CREATE TABLE IF NOT EXISTS batch_job_request (
        job_seq_id bigserial PRIMARY KEY,
        job_name varchar(100) NOT NULL,
        job_parameter varchar(200),
        group_id varchar(10) NOT NULL,
        job_execution_id bigint,
        polling_status varchar(10) NOT NULL,
        create_date timestamp NOT NULL,
        update_date timestamp
    );

2. `BatchJobRequestRepository`インタフェースの拡張インタフェース作成

   - 優先度カラムによるジョブ実行順序の制御の例と同じ

3. カスタマイズしたテーブルを使用したSQLMapの定義

グループIDを抽出条件にしたSQLをSQLMapに定義する。

SQLMap定義(CustomizedBatchJobRequestRepository.xml)

    <!-- (1) -->
    <mapper namespace="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch04.asyncjobwithdb.repository.CustomizedBatchJobRequestRepository">

        <select id="find" resultType="org.terasoluna.batch.async.db.model.BatchJobRequest">
            SELECT
                job_seq_id AS jobSeqId,
                job_name AS jobName,
                job_parameter AS jobParameter,
                job_execution_id AS jobExecutionId,
                polling_status AS pollingStatus,
                create_date AS createDate,
                update_date AS updateDate
            FROM
                batch_job_request
            WHERE
                polling_status = 'INIT'
            AND
                group_id = #{groupId}  <!--(2) -->
            ORDER BY
                job_seq_id ASC
            LIMIT #{pollingRowLimit}
        </select>

        <!-- omitted -->
    </mapper>

表 50. 拡張ポイント
項番 | 説明
--- | ---
(1) | `BatchJobRequestRepository`の拡張インタフェースをFQCNでnamespaceに設定する。
(2) | groupIdを検索条件に追加。

4. `AsyncBatchDaemonConfig.java/async-batch-daemon.xml`のBean定義の修正

(2)で作成した拡張インタフェースを`batchJobRequestRepository`に設定し、 `jobRequestPollTask`に環境変数で与えられたグループIDをクエリパラメータとして設定する。

JavaConfig
jp.co.ntt.fw.macchinetta.batch.functionaltest.config.AsyncBatchDaemonConfig.java

    @Bean
    public MapperFactoryBean<CustomizedBatchJobRequestRepository> batchJobRequestRepository(
            @Qualifier("adminSqlSessionFactory") SqlSessionFactory adminSqlSessionFactory) {
        final MapperFactoryBean<CustomizedBatchJobRequestRepository> mapperFactoryBean = new MapperFactoryBean<>();
        mapperFactoryBean.setMapperInterface(CustomizedBatchJobRequestRepository.class); // (1)
        mapperFactoryBean.setSqlSessionFactory(adminSqlSessionFactory);
        return mapperFactoryBean;
    }

    @Bean
    public JobRequestPollTask jobRequestPollTask(@Qualifier("adminTransactionManager") DataSourceTransactionManager adminTransactionManager,
                                                 JobOperator jobOperator,
                                                 MapperFactoryBean<CustomizedBatchJobRequestRepository> batchJobRequestRepository,
                                                 @Qualifier("daemonTaskExecutor") ThreadPoolTaskExecutor daemonTaskExecutor,
                                                 AutomaticJobRegistrar automaticJobRegistrar)  throws Exception {

        JobRequestPollTask jobRequestPollTask = new JobRequestPollTask(batchJobRequestRepository.getObject(), adminTransactionManager, daemonTaskExecutor, jobOperator,
                automaticJobRegistrar);
        final Map<String, Object> pollingQueryParam = new LinkedHashMap<>();
        pollingQueryParam.put("groupId", groupId); // (2)

        jobRequestPollTask.setOptionalPollingQueryParams(pollingQueryParam); // (3)
        return jobRequestPollTask;
    }

XMLConfig
META-INF/spring/async-batch-daemon.xml

     <!--(1) -->
    <bean id="batchJobRequestRepository"
          class="org.mybatis.spring.mapper.MapperFactoryBean"
          p:mapperInterface="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch04.asyncjobwithdb.repository.CustomizedBatchJobRequestRepository"
          p:sqlSessionFactory-ref="adminSqlSessionFactory" />

    <bean id="jobRequestPollTask"
          class="org.terasoluna.batch.async.db.JobRequestPollTask"
          c:transactionManager-ref="adminTransactionManager"
          c:jobOperator-ref="jobOperator"
          c:batchJobRequestRepository-ref="batchJobRequestRepository"
          c:daemonTaskExecutor-ref="daemonTaskExecutor"
          c:automaticJobRegistrar-ref="automaticJobRegistrar"
          p:optionalPollingQueryParams-ref="pollingQueryParam" /> <!-- (3) -->

    <bean id="pollingQueryParam"
          class="org.springframework.beans.factory.config.MapFactoryBean">
          <property name="sourceMap">
                <map>
                    <entry key="groupId" value="${GROUP_ID}"/>  <!-- (2) -->
                </map>
          </property>
    </bean>

表 51. 拡張ポイント
項番 | 説明
--- | ---
(1) | `BatchJobRequestRepository`の拡張インタフェースを`MapperInterface`に設定する。
(2) | 環境変数で与えれられたグループID(GROUP_ID)を、クエリパラメータのグループID(groupId)に設定する。
(3) | `JobRequestPollTask`の`optionalPollingQueryParams`に、(2)のMapを設定する。

5. 環境変数にグループIDを設定後、`AsyncBatchDaemon`を起動する。

AsyncBatchDaemonの起動

    $ # Set environment variables
    $ export GROUP_ID=G1

    $ # Start AsyncBatchDaemon
    $ java -cp dependency/* org.terasoluna.batch.async.db.AsyncBatchDaemon

##### 4.3.4.2. タイムスタンプに用いるクロックのカスタマイズ

タイムスタンプに用いるクロックは、デフォルト設定では`systemDefaultZone`から取得している。
しかし、ある特定の時間帯はポーリングをキャンセルするといった、ジョブ要求の取得条件をシステム日時に依存した非同期バッチデーモンに拡張したい場合など、ある特定の日時を指定したり、使用するシステムと異なるタイムゾーンを使用して試験を実施したい場合がある。そのため非同期実行では、用途に合わせてカスタマイズしたクロックを設定できる機能を備えている。
また、ジョブ要求テーブルからのリクエスト取得をカスタマイズしていないときのデフォルト設定において、クロックを変更したとき影響を受けるのはジョブ要求テーブルの`update_date`のみである。

クロックのカスタマイズ手順は以下のとおり。

1. `AsyncBatchDaemonConfig.java/async-batch-daemon.xml`のコピーを作成

2. ファイル名を`AsyncBatchDaemonClockConfig.java/async-batch-daemon-clock.xml`に変更

3. `AsyncBatchDaemonClockConfig.java/async-batch-daemon-clock.xml`のBean定義を修正

4. カスタマイズした`AsyncBatchDaemonClockConfig.java/async-batch-daemon-clock.xml`でAsyncBatchDaemonを起動
   詳細は、非同期バッチデーモンの起動を参照。

下記に日時を固定し、タイムゾーンを変更するための設定例を示す。

JavaConfig
jp.co.ntt.fw.macchinetta.batch.functionaltest.config.ch04.asyncjobwithdb.AsyncBatchDaemonClockConfig.java

    @Bean
    public JobRequestPollTask jobRequestPollTask(@Qualifier("adminTransactionManager") DataSourceTransactionManager adminTransactionManager,
                                                 JobOperator jobOperator,
                                                 MapperFactoryBean<BatchJobRequestRepository> batchJobRequestRepository,
                                                 @Qualifier("daemonTaskExecutor") ThreadPoolTaskExecutor daemonTaskExecutor,
                                                 AutomaticJobRegistrar automaticJobRegistrar)  throws Exception {

        JobRequestPollTask jobRequestPollTask = new JobRequestPollTask(batchJobRequestRepository.getObject(), adminTransactionManager, daemonTaskExecutor, jobOperator,
                automaticJobRegistrar);
        jobRequestPollTask.setClock(Clock.fixed(ZonedDateTime.parse("2016-12-31T16:00-08:00[America/Los_Angeles]").toInstant(), ZoneId.of("PST", ZoneId.SHORT_IDS))); // (1)
        return jobRequestPollTask;
    }

表 52. 説明
項番 | 説明
--- | ---
(1) | 日時を2016年12月31日16時0分0秒に固定し、タイムゾーンをロサンゼルス時間とした`java.time.Clock`を、`JobRequestPollTask`の`setClock`メソッドで設定する。 ロサンゼルス時間のタイムゾーンIDは`PST`である。

XMLConfig
META-INF/ch04/asyncjobwithdb/async-batch-daemon-clock.xml

    <bean id="jobRequestPollTask"
          class="org.terasoluna.batch.async.db.JobRequestPollTask"
          c:transactionManager-ref="adminTransactionManager"
          c:jobOperator-ref="jobOperator"
          c:batchJobRequestRepository-ref="batchJobRequestRepository"
          c:daemonTaskExecutor-ref="daemonTaskExecutor"
          c:automaticJobRegistrar-ref="automaticJobRegistrar"
          p:clock-ref="clock" />　<!-- (1) -->

    <!-- (2) -->
    <bean id="clock" class="java.time.Clock" factory-method="fixed"
          c:fixedInstant="#{T(java.time.ZonedDateTime).parse('2016-12-31T16:00-08:00[America/Los_Angeles]').toInstant()}"
          c:zone="#{T(java.time.ZoneId).of('PST', T(java.time.ZoneId).SHORT_IDS)}"/>

表 53. 説明
項番 | 説明
--- | ---
(1) | `JobRequestPollTask`の`clock-ref`属性に(2)で定義するBeanを設定する。
(2) | 日時を2016年12月31日16時0分0秒に固定し、タイムゾーンをロサンゼルス時間とした`java.time.Clock`のBeanを定義する。 ロサンゼルス時間のタイムゾーンIDは`PST`である。

##### 4.3.4.3. 複数起動

以下の様な目的で、複数サーバ上で非同期バッチデーモンを起動させる場合がある。

- 可用性向上

  - 非同期バッチジョブがいずれかのサーバで実行できればよく、ジョブが起動できないという状況をなくしたい場合

- 性能向上

  - 複数サーバでバッチ処理の負荷を分散させたい場合

- リソースの有効利用

  - サーバ性能に差がある場合に特定のジョブを最適なリソースのサーバに振り分ける場合

    - ジョブ要求テーブルのカスタマイズで提示したグループIDによるジョブノードの分割に相当

上記に示す観点のいずれかにもとづいて利用するのかを意識して運用設計を行うことが必要となる。

図 19. 複数起動の概略図

| 複数の非同期バッチデーモンが同一ジョブ要求レコードを取得した場合 `JobRequestPollTask`は、楽観ロックによる排他制御を行っているため、ポーリングステータスをINITからPOLLEDへ更新できた非同期バッチデーモンが取得したレコードのジョブを実行できる。 排他された他の非同期バッチデーモンは、次のジョブ要求レコードを処理する。

### 4.4. 非同期実行(Webコンテナ)

#### 4.4.1. Overview

Webコンテナ内でジョブを非同期で実行するための方法について説明する。

本機能は、チャンクモデルとタスクレットモデルとで同じ使い方になる。

Webコンテナによるジョブの非同期実行とは

ジョブを含めたWebアプリケーションをWebコンテナにデプロイし、 送信されたリクエストの情報をもとにジョブを実行することを指す。
ジョブの実行ごとに1つのスレッドを割り当てた上で並列に動作するため、 他のジョブやリクエストに対する処理とは独立して実行できる。

提供機能

Macchinetta Batch 2.xでは、非同期実行(Webコンテナ)向けの実装は提供しない。
本ガイドラインにて実現方法を提示するのみとする。
これは、Webアプリケーションの起動契機はHTTP/SOAP/MQなど多様であるため、 ユーザにて実装することが適切と判断したためである。

利用前提

- アプリケーションの他にWebコンテナが必要となる。

- ジョブの実装以外に必要となる、Webアプリケーション、クライアントは動作要件に合わせて別途実装する。

- ジョブの実行状況および結果は`JobRepository`に委ねる。
  また、Webコンテナ停止後にも`JobRepository`からジョブの実行状況および結果を参照可能とするため、インメモリデータベースではなく、永続性が担保されているデータベースを使用する。

活用シーン

"非同期実行(DBポーリング) - Overview"と同様である。

| 非同期実行(DBポーリング)との違い アーキテクチャ上、非同期実行時の即時性と、要求管理テーブルの有無、の2点が異なる。 "非同期実行(DBポーリング)"は要求管理テーブルに登録された複数のジョブが一定の周期で非同期実行される。 それに対し、本機能は要求管理テーブルを必要とせず代わりにWebコンテナ上で非同期実行を受け付ける。 Webリクエスト送信により直ちに実行するため、起動までの即時性が求められるショートバッチに向いている。

#### 4.4.2. Architecture

本方式による非同期ジョブはWebコンテナ上にデプロイされたアプリケーション(war)として動作するが、 ジョブ自身はWebコンテナのリクエスト処理とは非同期(別スレッド)で動作する。

図 20. 非同期実行(Webコンテナ)のシーケンス図
ジョブの起動

1. Webクライアントは実行対象のジョブをWebコンテナに要求する。

2. `JobController`はSpring Batchの`JobOperator`に対しジョブの実行開始を依頼する。

3. `ThreadPoolTaskExecutor`によって非同期でジョブを実行する。

4. 実行された対象のジョブを一意に判別するためのジョブ実行ID(`job execution id`)を返却する。

5. `JobController`はWebクライアントに対し、ジョブ実行IDを含むレスポンスを返却する。

6. 目的のジョブを実行する。

   - ジョブの結果は`JobRepository`に反映される。

7. `Job`が実行結果を返却する。これはクライアントへ直接通知できない。

ジョブの実行結果確認

8. Webクライアントはジョブ実行IDを`JobController`をWebコンテナに送信する。

9. `JobController`はジョブ実行IDを用い`JobExplorer`にジョブの実行結果を問い合わせる。

10. `JobExplorer`はジョブの実行結果を返却する。

11. `JobController`はWebクライアントに対しレスポンスを返却する。

    - レスポンスにはジョブ実行IDを設定する。

Webコンテナによるリクエスト受信後、ジョブ実行ID払い出しまでがリクエスト処理と同期するが、 以降のジョブ実行はWebコンテナとは別のスレッドプールで非同期に行われる。
これは再度リクエストで問い合わせを受けない限り、Webクライアント側では非同期ジョブの 実行状態が検知できないことを意味する。

このためWebクライアント側では1回のジョブ実行で、リクエストを「ジョブの起動」で1回、 「結果の確認」が必要な場合は加えてもう1回、Webコンテナにリクエストを送信する必要がある。
特に初回の「ジョブの起動」時に見え方が異なる異常検知については、 後述のジョブ起動時における異常発生の検知についてで説明する。

| `JobRepository`、`JobExplorer`を使用して直接RDBMSを参照し、ジョブの実行状態を確認することもできる。 ジョブの実行状態・結果を参照する機能の詳細については、ジョブの管理を参照。

| ジョブ実行ID(job execution id)の取り扱いについて ジョブ実行IDは起動対象が同じジョブ、同じジョブパラメータであっても、ジョブ起動ごとに異なるシーケンス値が払い出される。 リクエスト送信により受付が行われたジョブ実行IDは`JobRepository`により外部RDBMSで永続化される。 しかし、Webクライアントの障害などによりこのIDが消失した場合、ジョブ実行状況の特定・追跡が困難となる。 このため、Webクライアント側ではレスポンスとして返却されたジョブ実行IDをログに記録するなど、ジョブ実行IDの消失に備えておくこと。

##### 4.4.2.1. ジョブ起動時における異常発生の検知について

Webクライアントからジョブの起動リクエストを送信後、ジョブ実行ID払い出しを境にして異常検知の見え方が異なる。

- ジョブ起動時のレスポンスにて異常がすぐ検知できるもの

  - 起動対象のジョブが存在しない。

  - ジョブパラメータの形式誤り。

- ジョブ起動後、Webコンテナに対しジョブ実行状態・結果の問い合わせが必要となるもの

  - ジョブの実行ステータス

  - 非同期ジョブ実行で使用されるスレッドプールが枯渇したことによるジョブの起動失敗

| 「ジョブ起動時の異常」は Spring MVCコントローラ内で発生する例外として検知できる。 ここでは説明を割愛するので、別途 Macchinetta Server 1.x 開発ガイドラインの 例外のハンドリングの実装を参照。 また、ジョブパラメータとして利用するリクエストの入力チェックは必要に応じて Spring MVC のコントローラ内で行うこと。 具体的な実装方法については、Macchinetta Server 1.x 開発ガイドラインの 入力チェックを参照。

| スレッドプール枯渇によるジョブの起動失敗はジョブ起動時に捕捉できない スレッドプール枯渇によるジョブの起動失敗は、`JobOperator`から例外があがってこないため、別途確認する必要がある。 確認方法の1つは、ジョブの実行状態確認時に`JobExplorer`を用い、以下の条件に合致しているかどうかである。 ステータスが`FAILED`である `jobExecution.getExitStatus().getExitDescription()`にて、 `org.springframework.core.task.TaskRejectedException`の例外スタックトレースが記録されている

##### 4.4.2.2. 非同期実行(Webコンテナ)のアプリケーション構成

本機能は"非同期実行(DBポーリング)"と同様、 非同期実行特有の構成としてSpring プロファイルの`async`と`AutomaticJobRegistrar`を使用している。

一方で、これら機能を非同期実行(Webコンテナ)使用する上で、いくつかの事前知識と設定が必要となる。 "ApplicationContextの構成"を参照。
具体的な`async`プロファイルと`AutomaticJobRegistrar`の設定方法については "非同期実行(Webコンテナ)によるアプリケーションの実装方法について"で後述する。

###### 4.4.2.2.1. ApplicationContextの構成

上述のとおり、非同期実行(Webコンテナ)のアプリケーション構成として、複数のアプリケーションモジュールが含まれている。
それぞれのアプリケーションコンテキストとBean定義についての種類、および関係性を把握しておく必要がある。

JavaConfig
図 21. ApplicationContextの構成
図 22. Bean定義ファイルの構成
XMLConfig
図 23. ApplicationContextの構成
図 24. Bean定義ファイルの構成

非同期実行(Webコンテナ)における`ApplicationContext`では、 バッチアプリケーションの`ApplicationContext`はWebのコンテキスト内に取り込まれる。
個々のジョブコンテキストはこのWebコンテキストから`AutomaticJobRegistrar`によりモジュール化され、 Webコンテキストの子コンテキストとして動作する。

以下、それぞれのコンテキストを構成するBean定義ファイルについて説明する。

表 54. Bean定義ファイル一覧
項番 | 説明
--- | ---
(1) | 共通Bean定義ファイル。 アプリケーション内では親コンテキストとなり、子コンテキストであるジョブ間で一意に共有される。
(2) | ジョブBean定義から必ずインポートされるBean定義ファイル。 Spring プロファイルが非同期実行時に指定される`async`の場合は(1)の`LaunchContextConfig.java/launch-context.xml`を読み込まない。
(3) | ジョブごとに作成するBean定義ファイル。 `AutomaticJobRegistrar`によりモジュラー化され、アプリケーション内ではそれぞれ独立した子コンテキストとして使用される。
(4) | `DispatcherServlet`から読み込まれる。 ジョブBean定義のモジュラー化を行う`AutomaticJobRegistrar`や、ジョブの非同期・並列実行で使用されるスレッドプールである`taskExecutor`など、非同期実行特有のBeanを定義する。 また、非同期実行では(1)の`LaunchContextConfig.java/launch-context.xml` を直接インポートし親コンテキストとして一意に共有化される。
(5) | `ContextLoaderListener`により、Webアプリケーション内で共有される親コンテキストとなる。
(6) | `JavaConfig`のみ、共通Bean定義ファイルからインポートされる定義ファイル。

#### 4.4.3. How to use

ここでは、Webアプリケーション側の実装例として、Macchinetta Server Framework (1.x)を用いて説明する。
あくまで説明のためであり、Macchinetta Server 1.xは非同期実行(Webコンテナ)の必須要件ではないことに留意してほしい。

##### 4.4.3.1. 非同期実行(Webコンテナ)によるアプリケーションの実装概要

以下の構成を前提とし説明する。

- Webアプリケーションプロジェクトとバッチアプリケーションプロジェクトは独立し、 webアプリケーションからバッチアプリケーションを参照する。

  - Webアプリケーションプロジェクトから生成するwarファイルは、 バッチアプリケーションプロジェクトから生成されるjarファイルを含むこととなる

非同期実行の実装はArchitectureに従い、Webアプリケーション内の Spring MVCコントローラが、`JobOperator`によりジョブを起動する。

| Web/バッチアプリケーションプロジェクトの分離について アプリケーションビルドの最終成果物はWebアプリケーションのwarファイルであるが、 開発プロジェクトはWeb/バッチアプリケーションで分離して実装を行うとよい。 これはバッチアプリケーション単体で動作可能なライブラリとなるため、開発プロジェクト上の試験を 容易にする他、作業境界とライブラリ依存関係を明確にする効果がある。

以降、Web/バッチの開発について、以下2つを利用する前提で説明する。

- Macchinetta Batch 2.xによるバッチアプリケーションプロジェクト

- Macchinetta Server 1.xによるWebアプリケーションプロジェクト

バッチアプリケーションプロジェクトの作成および具体的なジョブの実装方法については、 プロジェクトの作成、 チャンクモデルジョブの作成、 タスクレットモデルジョブの作成を参照。 ここでは、Webアプリケーションからバッチアプリケーションを起動することに終始する。

以下のバッチアプリケーションプロジェクトは、Maven archetype:generate を用いてを作成しているものとして説明する。

表 55. ジョブプロジェクト作成例
名称 | 値
--- | ---
groupId | jp.co.ntt.fw.macchinetta.batch.sample
artifactId | asyncbatch
version | 1.0-SNAPSHOT
package | jp.co.ntt.fw.macchinetta.batch.sample

また説明の都合上、ブランクプロジェクトに初めから登録されているジョブを使用する。

表 56. 説明に用いるジョブ
名称 | 説明
--- | ---
ジョブ名 | job01

| 非同期実行(Webコンテナ)ジョブ設計の注意点 非同期実行(Webコンテナ)の特性として個々のジョブは短時間で完了しWebコンテナ上でステートレスに 動作するケースが適している。 また複雑さを避ける上では、ジョブ定義を単一のステップのみで構成し、ステップの終了コードによるフローの分岐や 並列処理・多重処理を定義しないことが望ましい。

ジョブ実装を含むjarファイルが作成可能な状態として、Webアプリケーションの作成を行う。

Webアプリケーションの実装

Macchinetta Server 1.xが提供するブランクプロジェクトを用い、Webアプリケーションの実装方法を説明する。 詳細は、Macchinetta Server 1.x 開発ガイドラインの Webアプリケーション向け開発プロジェクトの作成を参照。

ここでは非同期実行アプリケーションプロジェクトと同様、以下の名称で作成したものとして説明する。

表 57. Webコンテナプロジェクト作成例
名称 | 値
--- | ---
groupId | jp.co.ntt.fw.macchinetta.batch.sample
artifactId | asyncapp
version | 1.0-SNAPSHOT
package | jp.co.ntt.fw.macchinetta.batch.sample

| groupIdの命名について プロジェクトの命名は任意であるが、Maven マルチプロジェクトとしてバッチアプリケーションを Webアプリケーションの子モジュールとする場合、`groupId`は統一しておくと管理しやすい。 ここでは両者の`groupId`を`jp.co.ntt.fw.macchinetta.batch.sample`としている。

##### 4.4.3.2. 各種設定

バッチアプリケーションをWebアプリケーションの一部に含める

pom.xmlを編集し、バッチアプリケーションをWebアプリケーションの一部に含める。

| バッチアプリケーションを`jar` としてNEXUSやMavenローカルリポジトリに登録し、 Webアプリケーションとは別プロジェクトとする場合はこの手順は不要である。 ただし、Mavenによりビルドされる対象が別プロジェクトとなり、バッチアプリケーションの修正を行ってもWebアプリケーションのビルド時に反映されないため注意すること。 バッチアプリケーションの修正をWebアプリケーションに反映させるためには同リポジトリに登録する必要がある。

図 25. ディレクトリ構成
asyncapp/pom.xml

    <project>
      <!-- omitted -->
      <modules>
        <module>asyncapp-domain</module>
        <module>asyncapp-env</module>
        <module>asyncapp-initdb</module>
        <module>asyncapp-web</module>
        <module>asyncapp-selenium</module>
        <module>asyncbatch</module> <!-- (1) -->
      </modules>
    </project>

asyncapp/asyncbatch/pom.xml

    <project>
      <modelVersion>4.0.0</modelVersion>
      <groupId>jp.co.ntt.fw.macchinetta.batch.sample</groupId> <!-- (2) -->
      <artifactId>asyncbatch</artifactId>
      <version>1.0-SNAPSHOT</version> <!-- (2) -->
      <!-- (1) -->
      <parent>
        <groupId>jp.co.ntt.fw.macchinetta.batch.sample</groupId>
        <artifactId>asyncapp</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
      </parent>
      <!-- omitted -->
    </project>

表 58. 削除・追加内容
項番 | 説明
--- | ---
(1) | Webアプリケーションを親とし、バッチアプリケーションを子とするための設定を追記する。
(2) | 子モジュール化にともない、不要となる記述を削除する。

依存ライブラリの追加

バッチアプリケーションをWebアプリケーションの依存ライブラリとして追加する。

asyncapp/async-web/pom.xml

    <project>
      <!-- omitted -->
      <dependencies>
    　　<!-- (1) -->
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>asyncbatch</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- omitted -->
      </dependencies>
      <!-- omitted -->
    </project>

表 59. 追加内容
項番 | 説明
--- | ---
(1) | バッチアプリケーションをWebアプリケーションの依存ライブラリとして追加する。

##### 4.4.3.3. Webアプリケーションの実装

ここではWebアプリケーションとして、以下Macchinetta Server 1.x 開発ガイドラインを参考に、RESTful Webサービスを作成する。

RESTful Web Serviceで必要となるSpring MVCのコンポーネントを有効化するための設定

###### 4.4.3.3.1. Webアプリケーションの設定

まず、Webアプリケーションのブランクプロジェクトから、各種設定ファイルの追加・削除・編集を行う。

| 説明の都合上、バッチアプリケーションの実装形態としてRESTful Web Service を用いた実装を行っている。 従来のWebアプリケーション(Servlet/JSP)やSOAPを使用した場合でも同様な手順となるので、適宜読み替えること。

JavaConfig
図 26. ブランクプロジェクトから追加・削除するBean定義ファイル
XMLConfig
図 27. ブランクプロジェクトから追加・削除するBean定義ファイル

表 60. 追加・削除するBean定義ファイル
項番 | 説明
--- | ---
(1) | (2)を作成するため、不要となるので削除する。
(2) | `RESTful Web Service`用の`SpringMvcRestConfig.java/spring-mvc-rest.xml`を作成する。必要となる定義の記述例を以下に示す。

JavaConfig
asyncapp/asyncapp-web/src/main/java/jp/co/ntt/fw/macchinetta/batch/sample/config/web/SpringMvcRestConfig.java

    @Configuration
    @Import(LaunchContextConfig.class) // (1)
    @EnableAspectJAutoProxy
    @ComponentScan("jp.co.ntt.fw.macchinetta.batch.sample.app.api") // (2)
    @EnableWebMvc
    public class SpringMvcRestConfig implements WebMvcConfigurer {

        @Bean("jsonMessageConverter")
        public MappingJackson2HttpMessageConverter jsonMessageConverter(
                ObjectMapper objectMapper) {
            MappingJackson2HttpMessageConverter bean = new MappingJackson2HttpMessageConverter();
            bean.setObjectMapper(objectMapper);
            return bean;
        }

        @Bean("objectMapper")
        public ObjectMapper objectMapper() {
            Jackson2ObjectMapperFactoryBean bean = new Jackson2ObjectMapperFactoryBean();
            bean.setDateFormat(stdDateFormat());
            bean.afterPropertiesSet();
            return bean.getObject();
        }

        @Bean
        public StdDateFormat stdDateFormat() {
            return new StdDateFormat();
        }

        // (3)
        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            converters.add(jsonMessageConverter(objectMapper()));
        }

        // (4)
        @Bean
        public AutomaticJobRegistrar automaticJobRegistrar(JobRegistry jobRegistry,
               ApplicationContextFactory[] applicationContextFactories) throws Exception {
            final AutomaticJobRegistrar automaticJobRegistrar = new AutomaticJobRegistrar();
            final DefaultJobLoader defaultJobLoader = new DefaultJobLoader();
            defaultJobLoader.setJobRegistry(jobRegistry);
            automaticJobRegistrar.setApplicationContextFactories(applicationContextFactories);
            automaticJobRegistrar.setJobLoader(defaultJobLoader);
            automaticJobRegistrar.afterPropertiesSet();
            return automaticJobRegistrar;
        }

        @Bean
        public ApplicationContextFactory[] applicationContextFactories(
            final ApplicationContext ctx) throws IOException {
                return new ApplicationContextFactoryHelper(ctx).load(
                       "classpath:jp/co/ntt/fw/macchinetta/batch/sample/jobs/*.class");
        }

        // (5)
        @Bean
        public TaskExecutor taskExecutor() {
            ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
            taskExecutor.setCorePoolSize(3);
            taskExecutor.setMaxPoolSize(3);
            taskExecutor.setQueueCapacity(10);
            return taskExecutor;
        }

        // (6)
        @Bean
        public JobLauncher jobLauncher(JobRepository jobRepository,
            TaskExecutor taskExecutor) {
            TaskExecutorJobLauncher jobLauncher = new TaskExecutorJobLauncher();
            jobLauncher.setJobRepository(jobRepository);
            jobLauncher.setTaskExecutor(taskExecutor);
            return jobLauncher;
        }
    }

XMLConfig
asyncapp/asyncapp-web/src/main/resources/META-INF/spring/spring-mvc-rest.xml の記述例

    <!-- omitted -->
    <!-- (1) -->
    <import resource="classpath:META-INF/spring/launch-context.xml"/>

    <bean id="jsonMessageConverter"
          class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"
          p:objectMapper-ref="objectMapper"/>

    <bean id="objectMapper"
          class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
      <property name="dateFormat">
          <bean class="com.fasterxml.jackson.databind.util.StdDateFormat"/>
      </property>
    </bean>

    <!-- (3) -->
    <mvc:annotation-driven>
      <mvc:message-converters register-defaults="false">
        <ref bean="jsonMessageConverter"/>
      </mvc:message-converters>
    </mvc:annotation-driven>

    <mvc:default-servlet-handler/>

    <!-- (2) -->
    <context:component-scan base-package="jp.co.ntt.fw.macchinetta.batch.sample.app.api"/>

    <!-- (4) -->
    <bean class="org.springframework.batch.core.configuration.support.AutomaticJobRegistrar">
        <property name="applicationContextFactories">
            <bean class="org.springframework.batch.core.configuration.support.ClasspathXmlApplicationContextsFactoryBean">
                <property name="resources">
                    <list>
                      <value>classpath:/META-INF/jobs/**/*.xml</value>
                    </list>
                </property>
            </bean>
        </property>
        <property name="jobLoader">
            <bean class="org.springframework.batch.core.configuration.support.DefaultJobLoader"
                  p:jobRegistry-ref="jobRegistry"/>
        </property>
    </bean>

    <!-- (5) -->
    <task:executor id="taskExecutor" pool-size="3" queue-capacity="10"/>

    <!-- (6) -->
    <bean id="jobLauncher" class="org.springframework.batch.core.launch.support.TaskExecutorJobLauncher"
          p:jobRepository-ref="jobRepository"
          p:taskExecutor-ref="taskExecutor"/>
    <!-- omitted -->

JavaConfig
asyncapp/asyncapp-web/src/main/webapp/WEB-INF/web.xml の記述例

    <!-- omitted -->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <!-- Root ApplicationContext -->
        <param-value>
            jp.co.ntt.fw.macchinetta.batch.sample.config.app.ApplicationContextConfig
        </param-value>
    </context-param>

    <servlet>
        <servlet-name>restApiServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
              org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <!-- (7) -->
            <param-value>
              jp.co.ntt.fw.macchinetta.batch.sample.config.web.SpringMvcRestConfig
            </param-value>
        </init-param>
        <!-- (8) -->
        <init-param>
            <param-name>spring.profiles.active</param-name>
            <param-value>async</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>restApiServlet</servlet-name>
        <url-pattern>/api/v1/*</url-pattern>
    </servlet-mapping>
    <!-- omitted -->

XMLConfig
asyncapp/asyncapp-web/src/main/webapp/WEB-INF/web.xml の記述例

    <!-- omitted -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <!-- Root ApplicationContext -->
        <param-value>
            classpath*:META-INF/spring/applicationContext.xml
        </param-value>
    </context-param>

    <servlet>
        <servlet-name>restApiServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <!-- (7) -->
            <param-value>classpath*:META-INF/spring/spring-mvc-rest.xml</param-value>
        </init-param>
        <!-- (8) -->
        <init-param>
            <param-name>spring.profiles.active</param-name>
            <param-value>async</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>restApiServlet</servlet-name>
        <url-pattern>/api/v1/*</url-pattern>
    </servlet-mapping>
    <!-- omitted -->

表 61. RESTful Web Service の有効化例
項番 | 説明
--- | ---
(1) | バッチアプリケーション内にある`LaunchContextConfig.java/launch-context.xml`をimportし、必須となるBean定義を取り込む。
(2) | コントローラを動的にスキャンするためのパッケージを記述する。
(3) | `RESTful Web Service`を提供するために必要となる`Spring MVC`のフレームワークコンポーネントをbean登録する。 本設定を行うことで、リソースのフォーマットとしてJSONを使用する事ができる。 上記例では、`configureMessageConverters/<mvc:message-converters>タグ`を実装することでデフォルトの`MessageConverter`を登録させないようにしており、リソースの形式はJSONに限定される。 リソースのフォーマットとしてXMLを使用する場合は、XXE 対策が行われているXML用の`MessageConverter`を指定すること。指定方法は、「XXE 対策の有効化」を参照されたい。
(4) | 個々のジョブBean定義ファイルをモジュラー化することにより子コンテキストとして動的ロードを行う`AutomaticJobRegistrar`のBean定義を記述する。
(5) | 非同期で実行するジョブが用いる`TaskExecutor`を定義する。 JobLauncherの`TaskExecutor`に`AsyncTaskExecutor`実装クラスを設定することで非同期実行が可能となる。 `AsyncTaskExecutor`実装クラスの1つである`ThreadPoolTaskExecutor`を利用する。 また、並列起動可能なスレッドの多重度を指定ことができる。 この例では3スレッドがジョブの実行に割り当てられ、それを超えたリクエストは10までがキューイングされる。 キューイングされたジョブは未開始の状態ではあるが、REST リクエストは成功とみなされる。 さらにキューイングの上限を超えたジョブのリクエストは`org.springframework.core.task.TaskRejectedException`が発生し、 ジョブの起動要求が拒否される。
(6) | (5)の`taskExecutor`を有効化するため、`LaunchContextConfig.java/launch-context.xml`で定義されている`jobLauncher`をオーバーライドする。
(7) | `DispatcherServlet`が読み込むBean定義として、 上述で記載した`SpringMvcRestConfig.java/spring-mvc-rest.xml`を指定する。
(8) | Spring Framework のプロファイルとして、非同期バッチを表す`async`を明示する。

| asyncプロファイルの指定をしなかった場合 この場合、Webアプリケーション横断で共有すればよい`LaunchContextConfig.java/launch-context.xml`に定義されたBeanが、ジョブごとに重複して生成される。 重複した場合でも機能上動作するため誤りに気づきにくく、予期しないリソース枯渇や性能劣化が発生する恐れがある。 必ず指定すること。

| スレッドプールのサイジング スレッドプールの上限が過剰である場合、膨大なジョブが並走することとなり、 アプリケーション全体のスループットが劣化する恐れがある。 サイジングを行ったうえで適正な上限値を定めること。 非同期実行のスレッドプールとは別に、Webコンテナのリクエストスレッドや 同一筐体内で動作している他のアプリケーションも含めて検討する必要がある。 また、スレッドプール枯渇に伴う`TaskRejectException`発生の確認、および再実行は Webクライアントから別途リクエストを送信する必要がある。 そのため、スレッドプール枯渇時、ジョブ起動を待機させる`queue-capacity`は必ず設定すること。

RESTful Web Service API の定義

REST APIで使用するリクエストの例として、 ここでは「ジョブの起動」、「ジョブの状態確認」の2つを定義する。

表 62. REST API 定義例
項番 | API | パス | HTTPメソッド | 要求/応答 | 電文形式 | 電文の説明
--- | --- | --- | --- | --- | --- | ---
(1) | ジョブの起動 | /api/v1/job/ジョブ名 | POST | リクエスト | JSON | ジョブパラメータ
レスポンス | JSON | ジョブ実行ID ジョブ名 メッセージ
(2) | ジョブの実行状態確認 | /api/v1/job/ジョブ実行ID | GET | リクエスト | N/A | N/A
レスポンス | JSON | ジョブ実行ID ジョブ名 ジョブ実行ステータス ジョブ終了コード ステップ実行ID ステップ名 ステップ終了コード

###### 4.4.3.3.2. コントローラで使用するJavaBeansの実装

JSON電文としてRESTクライアントに返却される以下3クラスを作成する。

- ジョブ起動操作 `JobOperationResource`

- ジョブの実行状態 `JobExecutionResource`

- ステップの実行状態 `StepExecutionResource`

これらクラスは`JobOperationResource`のジョブ実行ID(`job execution id`)を除きあくまで参考実装であり、フィールドの実装は任意である。

ジョブ起動操作情報実装例

    // asyncapp/asyncapp-web/src/main/java/jp/co/ntt/fw/macchinetta/batch/sample/app/api/jobinfo/JobOperationResource.java
    package jp.co.ntt.fw.macchinetta.batch.sample.app.api.jobinfo;

    public class JobOperationResource {

        private String jobName = null;

        private String jobParams = null;

        private Long jobExecutionId = null;

        private String errorMessage = null;

        private Exception error = null;

        // Getter and setter are omitted.
    }

ジョブ実行情報実装例

    // asyncapp/asyncapp-web/src/main/java/jp/co/ntt/fw.macchinetta/batch/sample/app/api/jobinfo/JobExecutionResource.java
    package jp.co.ntt.fw.macchinetta.batch.sample.app.api.jobinfo;

    // omitted.

    public class JobExecutionResource {

        private Long jobExecutionId = null;

        private String jobName = null;

        private Long stepExecutionId = null;

        private String stepName = null;

        private List<StepExecutionResource> stepExecutions = new ArrayList<>();

        private String status = null;

        private String exitStatus = null;

        private String errorMessage;

        private List<String> failureExceptions = new ArrayList<>();

        // Getter and setter are omitted.
    }

ステップ実行情報実装例

    // asyncapp/asyncapp-web/src/main/java/jp/co/ntt/fw/macchinetta/batch/sample/app/api/jobinfo/StepExecutionResource.java
    package jp.co.ntt.fw.macchinetta.batch.sample.app.api.jobinfo;

    public class StepExecutionResource {

      private Long stepExecutionId = null;

      private String stepName = null;

      private String status = null;

      private List<String> failureExceptions = new ArrayList<>();

        // Getter and setter are omitted.
    }

###### 4.4.3.3.3. コントローラの実装

`@RestController`を用い、RESTful Web Service のコントローラを実装する。
ここでは簡単のため、`JobOperator`をコントローラにインジェクションし、ジョブの起動や実行状態の取得を行う。 もちろんMacchinetta Server 1.xに従って、コントローラからServiceをはさんで`JobOperator`を起動してもよい。

コントローラ実装例

    // asyncapp/asyncapp-web/src/main/java/jp/co/ntt/fw/macchinetta/batch/sample/app/api/JobController.java
    package jp.co.ntt.fw.macchinetta.batch.sample.app.api;

    // omitted.

    // (1)
    @RequestMapping("job")
    @RestController
    public class JobController {

        // (2)
        @Inject
        JobOperator jobOperator;

        // (2)
        @Inject
        JobExplorer jobExplorer;

        @RequestMapping(value = "{jobName}", method = RequestMethod.POST)
        public ResponseEntity<JobOperationResource> launch(@PathVariable("jobName") String jobName,
                @RequestBody JobOperationResource requestResource) {

            String jobParams = requestResource.getJobParams();
            JobOperationResource responseResource = new JobOperationResource();
            responseResource.setJobName(jobName);
            responseResource.setJobParams(jobParams);
            Properties properties = new Properties();
            if(jobParams == null){
                requestResource.setJobParams("");
            } else {
                if (jobParams.contains(",")) {
                    requestResource.setJobParams(jobParams.replace(",", " "));
                    jobParams = jobParams.replace(",", " ");
                    requestResource.setJobParams(jobParams);
                }
                if (!jobParams.isEmpty()) {
                    String[] keyValuePairs = jobParams.split(" ");
                    for (String string : keyValuePairs) {
                        String[] keyValuePair = string.split("=");
                        properties.setProperty(keyValuePair[0], keyValuePair[1]);
                    }
                }
            }
            try {
                // (3)
                Long jobExecutionId = jobOperator.start(jobName, properties);
                responseResource.setJobExecutionId(jobExecutionId);
                responseResource.setResultMessage("Job launching task is scheduled.");
                return ResponseEntity.ok().body(responseResource);
            } catch (NoSuchJobException | JobInstanceAlreadyExistsException | JobParametersInvalidException e) {
                responseResource.setError(e);
                return ResponseEntity.badRequest().body(responseResource);
            }
        }

        @RequestMapping(value = "{jobExecutionId}", method = RequestMethod.GET)
        @ResponseStatus(HttpStatus.OK)
        public JobExecutionResource getJob(@PathVariable("jobExecutionId") Long jobExecutionId) {

            JobExecutionResource responseResource = new JobExecutionResource();
            responseResource.setJobExecutionId(jobExecutionId);

            // (4)
            JobExecution jobExecution = jobExplorer.getJobExecution(jobExecutionId);

            if (jobExecution == null) {
                responseResource.setErrorMessage("Job execution not found.");
            } else {
                mappingExecutionInfo(jobExecution, responseResource);
            }

            return responseResource;
        }

        private void mappingExecutionInfo(JobExecution src, JobExecutionResource dest) {
          dest.setJobName(src.getJobInstance().getJobName());
          for (StepExecution se : src.getStepExecutions()) {
              StepExecutionResource ser = new StepExecutionResource();
              ser.setStepExecutionId(se.getId());
              ser.setStepName(se.getStepName());
              ser.setStatus(se.getStatus().toString());
              for (Throwable th : se.getFailureExceptions()) {
                  ser.getFailureExceptions().add(th.toString());
              }
              dest.getStepExecutions().add(ser);
          }
          dest.setStatus(src.getStatus().toString());
          dest.setExitStatus(src.getExitStatus().toString());
        }
    }

表 63. コントローラの実装
項番 | 説明
--- | ---
(1) | `@RestController`を指定する。 さらに`@RequestMapping("job")`により、`web.xml`のサーブレットマッピングとあわせると、 REST APIの基底パスは`contextName/api/v1/job/`となる。
(2) | `JobOperator`、`JobExplorer`のフィールドインジェクションを記述する。
(3) | `JobOperator` を使用して新規に非同期ジョブを起動する。 返り値としてジョブ実行IDを受け取り、REST クライアントに返却する。
(4) | `JobExplorer` を使用し、ジョブ実行IDをもとにジョブの実行状態(`JobExecution`)を取得する。 あらかじめ設計された電文フォーマットに変換した上でRESTクライアントに返却する。

| Spring Batchが提供するSimpleJobOperatorの仕様変更に伴い、 JobControllerの実装も下記のように変更している。 パラメータを複数指定する場合の区切り文字を、カンマから空白文字に変更 パラメータのnullチェックを、別途実装するように変更 詳細は、Spring Batch/BATCH-1461を参照されたい。 パラメータをStringからPropertiesに変換処理を追加し、`start`メソッドに渡す。 詳細は、Spring Batch/BATCH-4304を参照されたい。

###### 4.4.3.3.4. Web/バッチアプリケーションモジュール設定の統合

バッチアプリケーションモジュール(`asyncbatch`)は単体で動作可能なアプリケーションとして動作する。 そのため、バッチアプリケーションモジュール(`asyncbatch`)は、Webアプリケーションモジュール(`asyncapp-web`)との間で競合・重複する設定が存在する。 これらは、必要に応じて統合する必要がある。

1. ログ設定ファイル`logback.xml`の統合
   Web/バッチ間でLogback定義ファイルが複数定義されている場合、正常に動作しない。
   `asyncbatch/src/main/resources/logback.xml`の記述内容は`asyncapp-env/src/main/resources/`の同ファイルに統合した上で削除する。

2. データソース、MyBatis設定ファイルは統合しない
   データソース、MyBatis設定ファイルの定義はWeb/バッチ間では、以下関係によりアプリケーションコンテキストの定義が独立するため、統合しない。

   - バッチの`asyncbatch`モジュールはサーブレットに閉じたコンテキストとして定義される。

   - Webの`asyncapp-domain`、`asyncapp-env`モジュールはアプリケーション全体で使用されるコンテキストとして定義される。

| Webとバッチモジュールによるデータソース、MyBatis設定の相互参照 Webとバッチモジュールによるコンテキストのスコープが異なるため、 特にWebモジュールからバッチのデータソース、MyBatis設定、`Mapper`インタフェースは参照できない。 RDBMSスキーマ初期化もそれぞれ異なるモジュールの設定に応じて独立して行われるため、相互干渉により 意図しない初期化が行われないよう配慮すること。

| REST コントローラ特有のCSRF対策設定 Webブランクプロジェクトの初期設定では、RESTコントローラに対しリクエストを送信するとCSRFエラーとして ジョブの実行が拒否される。 そのため、ここでは以下方法によりCSRF対策を無効化した前提で説明している。 CSRF対策 ここで作成されるWebアプリケーションはインターネット上には公開されず、CSRFを攻撃手段として 悪用しうる第三者からのRESTリクエスト送信が発生しない前提でCSRF対策を無効化している。 実際のWebアプリケーションでは動作環境により要否が異なる点に注意すること。

###### 4.4.3.3.5. ビルド

Mavenコマンドでビルドし、warファイルを作成する。

    $ cd asyncapp
    $ ls
    asyncbatch/  asyncapp-web/  pom.xml
    $ mvn clean package
    [INFO] Scanning for projects...
    [INFO] ------------------------------------------------------------------------
    [INFO] Reactor Build Order:
    [INFO]
    [INFO] Macchinetta Server Framework (1.x) Web Blank Multi Project (MyBatis3)
    [INFO] Macchinetta Batch Framework (2.x) Blank Project
    [INFO] asyncapp-web
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building Macchinetta Server Framework (1.x) Web Blank Multi Project (MyBatis3) 1.0-SNAPSHOT
    [INFO] ------------------------------------------------------------------------

    (omitted)

    [INFO] ------------------------------------------------------------------------
    [INFO] Reactor Summary:
    [INFO]
    [INFO] Macchinetta Server Framework (1.x) Web Blank Multi Project (MyBatis3) SUCCESS [  0.226 s]
    [INFO] Macchinetta Batch Framework (2.x) Blank Project SUCCESS [  6.481s]
    [INFO] asyncapp-web ....................................... SUCCESS [  5.400 s]
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 12.597 s
    [INFO] Finished at: 2017-02-10T22:32:43+09:00
    [INFO] Final Memory: 38M/250M
    [INFO] ------------------------------------------------------------------------
    $

###### 4.4.3.3.6. デプロイ

TomcatなどのWebコンテナを起動し、ビルドで生成された`war`ファイルをデプロイする。 詳細な手順は割愛する。

##### 4.4.3.4. REST Clientによるジョブの起動と実行結果確認

ここではREST クライアントとしてcurlコマンドを使用し、非同期ジョブを起動する。

    $ curl -v \
      -H "Accept: application/json" -H "Content-type: application/json" \
      -d '{}' \
      http://localhost:8088/asyncapp-web/api/v1/job/job01
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
      0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
    * Trying 127.0.0.1:8088...
    * Connected to localhost (127.0.0.1) port 8088 (#0)
    > POST /asyncapp-web/api/v1/job/job01 HTTP/1.1
    > Host: localhost:8088
    > User-Agent: curl/7.88.1
    > Accept: application/json
    > Content-type: application/json
    > Content-Length: 30
    >
    } [30 bytes data]
    < HTTP/1.1 200
    < Content-Type: application/json
    < Transfer-Encoding: chunked
    < Date: Mon, 22 Jan 2024 02:39:04 GMT
    <
    { [94 bytes data]
    100   118    0    88  100    30    112     38 --:--:-- --:--:-- --:--:--   150
    {"jobName":"job01","jobParams":null,"jobExecutionId":1,"errorMessage":null,"error":null}
    * Connection #0 to host localhost left intact
    $

上記より、ジョブ実行ID:`jobExecutionId = 1`として、ジョブが実行されていることが確認できる。
続けてこのジョブ実行IDを使用し、ジョブの実行結果を取得する。

    $ curl -v http://localhost:8088/asyncapp-web/api/v1/job/1
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
      0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
    * Trying 127.0.0.1:8088...
    * Connected to localhost (127.0.0.1) port 8088 (#0)
    > GET /asyncapp-web/api/v1/job/1 HTTP/1.1
    > Host: localhost:8088
    > User-Agent: curl/7.88.1
    > Accept: */*
    >
    < HTTP/1.1 200
    < Content-Type: application/json
    < Transfer-Encoding: chunked
    < Date: Mon, 22 Jan 2024 02:39:26 GMT
    <
    { [310 bytes data]
    100   303    0   303    0     0   2126      0 --:--:-- --:--:-- --:--:--  2118
    {"jobExecutionId":1,"jobName":"job01","stepExecutionId":null,"stepName":null,"stepExecutions":
    [{"stepExecutionId":1,"stepName":"job01.step01","status":"COMPLETED","failureExceptions":[]}],
    "status":"COMPLETED","exitStatus":"exitCode=COMPLETED;exitDescription=","errorMessage":null,
    "failureExceptions":[]}
    * Connection #0 to host localhost left intact
    $

`exitCode=COMPLETED`であることより、ジョブが正常終了していることが確認できる。

| シェルスクリプトなどでcurlの実行結果を判定する場合 上記の例ではREST APIによる応答電文まで表示させている。 curlコマンドでHTTPステータスのみを確認する場合は`curl -s URL -o /dev/null -w "%{http_code}\n"`とすることで、HTTPステータスが標準出力に表示される。 ただし、ジョブ実行IDはレスポンスボディ部のJSONを解析する必要があるため、必要に応じてREST クライアントアプリケーションを作成すること。

#### 4.4.4. How to extend

##### 4.4.4.1. ジョブの停止とリスタート

非同期ジョブの停止・リスタートは複数実行しているジョブの中から停止・リスタートする必要がある。 また、同名のジョブが並走している場合に、問題が発生しているジョブのみを対象にする必要もある。 よって、対象とするジョブ実行が特定でき、その状態が確認できる必要がある。
ここではこの前提を満たす場合、非同期実行の停止・リスタートを行うための実装について説明する。

以降、コントローラの実装の`JobController`に対して、 ジョブの停止(stop)やリスタート(restart)を追加する方法について説明する。

| ジョブの停止・リスタートは`JobOperator`を用いた実装をしなくても実施できる。 詳細はジョブの管理を参照し、目的に合う方式を検討すること。

停止・リスタートの実装例

    // asyncapp/asyncapp-web/src/main/java/jp/co/ntt/fw/macchinetta/batch/sample/app/api/JobController.java
    package jp.co.ntt.fw.macchinetta.batch.sample.app.api;

    // omitted.

    @RequestMapping("job")
    @RestController
    public class JobController {

        // omitted.

        @RequestMapping(value = "stop/{jobExecutionId}", method = RequestMethod.PUT)
        @Deprecated
        public ResponseEntity<JobOperationResource> stop(
                @PathVariable("jobExecutionId") Long jobExecutionId) {

          JobOperationResource responseResource = new JobOperationResource();
          responseResource.setJobExecutionId(jobExecutionId);
          boolean result = false;
          try {
              // (1)
              result = jobOperator.stop(jobExecutionId);
              if (!result) {
                  responseResource.setErrorMessage("stop failed.");
                  return ResponseEntity.badRequest().body(responseResource);
              }
              return ResponseEntity.ok().body(responseResource);
          } catch (NoSuchJobExecutionException | JobExecutionNotRunningException e) {
              responseResource.setError(e);
              return ResponseEntity.badRequest().body(responseResource);
          }
        }

        @RequestMapping(value = "restart/{jobExecutionId}",
                        method = RequestMethod.PUT)
        @Deprecated
        public ResponseEntity<JobOperationResource> restart(
                @PathVariable("jobExecutionId") Long jobExecutionId) {

            JobOperationResource responseResource = new JobOperationResource();
            responseResource.setJobExecutionId(jobExecutionId);
            try {
                // (2)
                Long id = jobOperator.restart(jobExecutionId);
                responseResource.setJobExecutionId(id);
                return ResponseEntity.ok().body(responseResource);
            } catch (JobInstanceAlreadyCompleteException |
                      NoSuchJobExecutionException | NoSuchJobException |
                      JobRestartException | JobParametersInvalidException e) {
                responseResource.setErrorMessage(e.getMessage());
                return ResponseEntity.badRequest().body(responseResource);
            }
        }

        // omitted.
    }

表 64. コントローラによる停止・リスタート実装例
項番 | 説明
--- | ---
(1) | `JobOperator#stop()`を呼び出すことにより、実行中のジョブに対し停止を指示する。
(2) | `JobOperator#restart()`を呼び出すことにより、異常終了・停止したステップから再実行させる。

##### 4.4.4.2. 複数起動

ここでの複数起動とは、Webコンテナを複数起動し、それぞれがジョブ要求を待ち受けることを指す。

非同期ジョブの実行管理は外部RDBMSによって行われるため、各アプリケーションの接続先となる 外部RDBMSを共有することで、同一筐体あるいは別筐体にまたがって非同期ジョブ起動を待ち受けることができる。

用途としては特定のジョブに対する負荷分散や冗長化などがあげられる。 しかし、Webアプリケーションの実装 で述べたように、Webコンテナを複数起動し並列性を高めるだけでこれらの効果が容易に得られるわけではない。 効果を得るためには、一般的なWebアプリケーションと同様の対処が求められる場合がある。 以下にその一例を示す。

- Webアプリケーションの特性上、1リクエスト処理はステートレスに動作するが、 バッチの非同期実行はジョブの起動と結果の確認を合わせて設計しなければ、かえって障害耐性が低下する恐れもある。
  たとえば、ジョブ起動用Webコンテナを冗長化した場合でもクライアント側の障害によりジョブ起動後にジョブ実行IDを ロストすることでジョブの途中経過や結果の確認は困難となる。

- 複数のWebコンテナにかかる負荷を分散させるために、クライアント側にリクエスト先を振り分ける機能を実装したり、 ロードバランサを導入したりする必要がある。

このように、複数起動の適性は一概に定めることができない。 そのため、目的と用途に応じてロードバランサの利用やWebクライアントによるリクエスト送信制御方式などを検討し、 非同期実行アプリケーションの性能や耐障害性を落とさない設計が必要となる。

### 4.5. リスナー

#### 4.5.1. Overview

リスナーとは、ジョブやステップを実行する前後に処理を挿入するためのインタフェースである。

本機能は、チャンクモデルとタスクレットモデルとで使い方が異なるため、それぞれについて説明する。

リスナーには多くのインタフェースがあるため、それぞれの役割について説明する。 その後に、設定および実装方法について説明をする。

##### 4.5.1.1. リスナーの種類

Spring Batchでは、実に多くのリスナーインタフェースが定義されている。 ここではそのすべてを説明するのではなく、利用頻度が高いものを中心に扱う。

まず、リスナーは2種類に大別される。

JobListener

  ジョブの実行に対して処理を挟み込むためのインタフェース

StepListener

  ステップの実行に対して処理を挟み込むためのインタフェース

| JobListenerについて Spring Batchには、`JobListener`という名前のインタフェースは存在しない。 `StepListener`との対比のため 、本ガイドラインでは便宜的に定義している。 Java Batch(jBatch)には、`javax.batch.api.listener.JobListener`というインタフェースが存在するので、実装時には間違えないように注意すること。 また、`StepListener`もシグネチャが異なる同名インタフェース(`javax.batch.api.listener.StepListener`)が存在するので、同様に注意すること。

###### 4.5.1.1.1. JobListener

`JobListener`のインタフェースは、`JobExecutionListener`の1つのみとなる。

JobExecutionListener

  ジョブの開始前、終了後に処理を挟み込む。

JobExecutionListenerインタフェース

    public interface JobExecutionListener {
      void beforeJob(JobExecution jobExecution);
      void afterJob(JobExecution jobExecution);
    }

###### 4.5.1.1.2. StepListener

`StepListener`のインタフェースは以下のように多くの種類がある。

StepListener

  以降に紹介する各種リスナーのマーカーインタフェース。

StepExecutionListener

  ステップ実行の開始前、終了後に処理を挟み込む。

StepExecutionListenerインタフェース

    public interface StepExecutionListener extends StepListener {
      void beforeStep(StepExecution stepExecution);
      ExitStatus afterStep(StepExecution stepExecution);
    }

ChunkListener

  1つのチャンクを処理する前後と、エラーが発生した場合に処理を挟み込む。

ChunkListenerインタフェース

    public interface ChunkListener extends StepListener {
      static final String ROLLBACK_EXCEPTION_KEY = "sb_rollback_exception";
      void beforeChunk(ChunkContext context);
      void afterChunk(ChunkContext context);
      void afterChunkError(ChunkContext context);
    }

| ROLLBACK_EXCEPTION_KEYの用途 `afterChunkError`メソッドにて、発生した例外を取得したい場合に利用する。 Spring Batchはチャンク処理中にエラーが発生した場合、`ChunkContext`に`sb_rollback_exception`というキー名で 例外を格納した上で`ChunkListener`を呼び出すため、以下の要領でアクセスできる。 使用例
    public void afterChunkError(ChunkContext context) {
        logger.error("Exception occurred while chunk. [context:{}]",
                context.getAttribute(ChunkListener.ROLLBACK_EXCEPTION_KEY));
    }

例外ハンドリングについては、`ChunkListener`インタフェースによる例外ハンドリングを参照。

ItemReadListener

  ItemReaderが1件のデータを取得する前後と、エラーが発生した場合に処理を挟み込む。

ItemReadListenerインタフェース

    public interface ItemReadListener<T> extends StepListener {
      void beforeRead();
      void afterRead(T item);
      void onReadError(Exception ex);
    }

ItemProcessListener

  ItemProcessorが1件のデータを加工する前後と、エラーが発生した場合に処理を挟み込む。

ItemProcessListenerインタフェース

    public interface ItemProcessListener<T, S> extends StepListener {
      void beforeProcess(T item);
      void afterProcess(T item, S result);
      void onProcessError(T item, Exception e);
    }

ItemWriteListener

  ItemWriterが1つのチャンクを出力する前後と、エラーが発生した場合に処理を挟み込む。

ItemWriteListenerインタフェース

    public interface ItemWriteListener<S> extends StepListener {
      void beforeWrite(Chunk<? extends S> items);
      void afterWrite(Chunk<? extends S> items);
      void onWriteError(Exception exception, Chunk<? extends S> items);
    }

| 本ガイドラインでは、以下のリスナーについては説明をしない。 リトライ系リスナー スキップ系リスナー これらのリスナーは例外ハンドリングでの使用を想定したものであるが、 本ガイドラインではこれらのリスナーを用いた例外ハンドリングは行わない方針である。 詳細は、例外ハンドリングを参照。

| `JobExecutionListener`や、`StepExecutionListener`は、Spring Batchにおけるトランザクション制御で説明するフレームワークトランザクションによる制御範囲外となる。 加えて、リスナーでのデータベースアクセスにおける制約があるため、本ガイドラインではリスナーによるデータベース更新は基本的に推奨しない。 前処理でデータベース更新を行う必要がある場合はフロー制御を参照し、データベース更新を行う前処理と後続処理のステップを分けて、`JobExecutionListener`、`StepExecutionListener`ではデータベース更新を行わない設計・実装を行うことを検討してほしい。

#### 4.5.2. How to use

リスナーの実装と設定方法について説明する。

##### 4.5.2.1. リスナーの実装

リスナーの実装と設定方法について説明する。

1. リスナーインタフェースを`implements`して実装する。

2. コンポーネントにメソッドベースでアノテーションを付与して実装する。

どちらで実装するかは、リスナーの役割に応じて選択する。基準は後述する。

###### 4.5.2.1.1. インタフェースを実装する場合

各種リスナーインタフェースを`implements`して実装する。必要に応じて、複数のインタフェースを同時に実装してもよい。 以下に実装例を示す。

JobExecutionListenerの実装例

    @Component
    public class JobExecutionLoggingListener implements JobExecutionListener { // (1)

        private static final Logger logger =
                LoggerFactory.getLogger(JobExecutionLoggingListener.class);

        @Override
        public void beforeJob(JobExecution jobExecution) { // (2)
            logger.info("job started. [JobName:{}]", jobExecution.getJobInstance().getJobName());
        }

        @Override
        public void afterJob(JobExecution jobExecution) { // (3)

            logger.info("job finished.[JobName:{}][ExitStatus:{}]", jobExecution.getJobInstance().getJobName(),
                    jobExecution.getExitStatus().getExitCode());
        }
    }

JavaConfig
リスナーの設定例

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<SalesPlanDetail> reader,
                       ItemWriter<SalesPlanDetail> writer,
                       AmountCheckProcessor processor,
                       @Qualifier("loggingEachProcessInStepListener") StepExecutionListener listener) {
        return new StepBuilder("chunkJobWithListener.step01",
                    jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(10, transactionManager)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .listener(listener)
                .build();
    }

    @Bean
    public Job chunkJobWithListener(JobRepository jobRepository,
                                            Step step01,
                                            @Qualifier("jobExecutionLoggingListener") JobExecutionLoggingListener listener) {
        return new JobBuilder("chunkJobWithListener",
                jobRepository)
                .start(step01)
                .listener(listener) // (4)
                .build();
    }

XMLConfig
リスナーの設定例

    <batch:job id="chunkJobWithListener" job-repository="jobRepository">
         <batch:step id="chunkJobWithListener.step01">
             <batch:tasklet transaction-manager="jobTransactionManager">
                 <batch:chunk reader="reader" processor="processor"
                              writer="writer" commit-interval="10"/>
                 <batch:listeners>
                     <batch:listener ref="loggingEachProcessInStepListener"/>
                 </batch:listeners>
             </batch:tasklet>
         </batch:step>
         <batch:listeners>
             <batch:listener ref="jobExecutionLoggingListener"/> <!-- (4) -->
         </batch:listeners>
     </batch:job>

表 65. 説明
項番 | 説明
--- | ---
(1) | `JobExecutionListener`を`implements`して実装する。
(2) | `JobExecutionListener`が定義している`beforeJob`メソッドを実装する。 この例では、ジョブ開始ログを出力する。
(3) | `JobExecutionListener`が定義している`afterJob`メソッドを実装する。 この例では、ジョブ終了ログを出力する。
(4) | Bean定義の`JobBuilder/<batch:listeners>`要素で、(1)で実装したリスナーを設定する。 設定方法の詳細は、リスナーの設定で説明する。

| リスナーのサポートクラス 複数のリスナーインタフェースを`implements`した場合、処理が不要な部分についても空実装をする必要がある。 この作業を簡略化するため、あらかじめ空実装を施したサポートクラスがSpring Batchには用意されている。 インタフェースではなく、サポートクラスを活用してもよいが、その場合`implements`ではなく`extends`になるため注意すること。 サポートクラス `org.springframework.batch.core.listener.ItemListenerSupport` `org.springframework.batch.core.listener.StepListenerSupport`

###### 4.5.2.1.2. アノテーションを付与する場合

各種リスナーインタフェースに対応したアノテーションを付与する。必要に応じて、複数のアノテーションを同時に実装してもよい。

表 66. リスナーインタフェースとの対応表
リスナーインタフェース | アノテーション
--- | ---
JobExecutionListener | `@BeforeJob` `@AfterJob`
StepExecutionListener | `@BeforeStep` `@AfterStep`
ChunkListener | `@BeforeChunk` `@AfterChunk` `@AfterChunkError`
ItemReadListener | `@BeforeRead` `@AfterRead` `@OnReadError`
ItemProcessListener | `@BeforeProcess` `@AfterProcess` `@OnProcessError`
ItemWriteListener | `@BeforeWrite` `@AfterWrite` `@OnWriteError`

これらアノテーションはコンポーネント化された実装のメソッドに付与することで目的のスコープで動作する。 以下に実装例を示す。

アノテーションを付与したItemProcessorの実装例

    @Component
    public class AnnotationAmountCheckProcessor implements
            ItemProcessor<SalesPlanDetail, SalesPlanDetail> {

        private static final Logger logger =
                LoggerFactory.getLogger(AnnotationAmountCheckProcessor.class);

        @Override
        public SalesPlanDetail process(SalesPlanDetail item) throws Exception {
            if (item.getAmount().signum() == -1) {
                throw new IllegalArgumentException("amount is negative.");
            }
            return item;
        }

        // (1)
        /*
        @BeforeProcess
        public void beforeProcess(Object item) {
            logger.info("before process. [Item :{}]", item);
        }
        */

        // (2)
        @AfterProcess
        public void afterProcess(Object item, Object result) {
            logger.info("after process. [Result :{}]", result);
        }

        // (3)
        @OnProcessError
        public void onProcessError(Object item, Exception e) {
            logger.error("on process error.", e);
        }
    }

JavaConfig
リスナーの設定例

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<SalesPlanDetail> reader,
                       ItemWriter<SalesPlanDetail> writer,
                       AnnotationAmountCheckProcessor annotationAmountCheckProcessor) {
        return new StepBuilder("chunkJobWithListenerAnnotation.step01",
                jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(10, transactionManager)
                .reader(reader)
                .processor(annotationAmountCheckProcessor) // (4)
                .writer(writer)
                .build();
    }

    @Bean
    public Job chunkJobWithListenerAnnotation(JobRepository jobRepository,
                                            Step step01) {
        return new JobBuilder("chunkJobWithListenerAnnotation",
                jobRepository)
                .start(step01)
                .build();
    }

XMLConfig
リスナーの設定例

    <batch:job id="chunkJobWithListenerAnnotation" job-repository="jobRepository">
        <batch:step id="chunkJobWithListenerAnnotation.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="reader"
                             processor="annotationAmountCheckProcessor"
                             writer="writer" commit-interval="10"/>  <! -- (4) -->
            </batch:tasklet>
        </batch:step>
    </batch:job>

表 67. 説明
項番 | 説明
--- | ---
(1) | アノテーションで実装する場合は、処理が必要なタイミングのアノテーションのみを付与すればよい。 この例では、ItemProcessの処理前には何もする必要がないため、`@BeforeProcess`を付与した実装は不要となる。
(2) | ItemProcessの処理後に行う処理を実装する。 この例では処理結果をログを出力している。
(3) | ItemProcessでエラーが発生したときの処理を実装する。 この例では発生した例外をログを出力している。
(4) | アノテーションでリスナー実装がされているItemProcessorを`StepBuilder/<batch:chunk>`に対し設定する。 リスナーインタフェースとは異なり、`listenerメソッド/<batch:listener>`要素で設定しなくても、自動的にリスナーが登録される。

| アノテーションを付与するメソッドの制約 アノテーションを付与するメソッドはどのようなメソッドでもよいわけではない。 対応するリスナーインタフェースのメソッドと、シグネチャを一致させる必要がある。 この点は、各アノテーションのjavadocに明記されている。

| JobExecutionListenerをアノテーションで実装したときの注意 JobExecutionListenerは、他のリスナーとスコープが異なるため、上記の設定では自動的にリスナー登録がされない。 そのため、`<batch:listener>`要素で明示的に設定する必要がある。詳細は、リスナーの設定を参照。

| タスクレットモデルではアノテーションによるリスナー実装が利用できない タスクレットモデルの場合は、インタフェースを実装する場合を参考に、リスナーインタフェースを利用すること。アノテーションによるリスナー実装をタスクレットモデルに設定しても、リスナーを起動することができない。

##### 4.5.2.2. リスナーの設定

リスナーを設定する方法は、下記となる。

- JavaConfig : Builderの`listener()`メソッド

- XMLConfig : `<batch:listener>`要素

上記のメソッドもしくは要素を用いてリスナーを設定できる位置は複数あるが、設定可能なリスナーは各位置で決められている。

リスナーの設定位置は以下を参照のこと。

JavaConfig
リスナーを設定する位置

    // for chunk mode
    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<SalesPlanDetail> reader,
                       ItemWriter<SalesPlanDetail> writer,
                       AmountCheckProcessor amountCheckProcessor,
                       StepExecutionListener stepExecutionListener,
                       ChunkListener chunkListener) {
        return new StepBuilder("chunkJob.step01", jobRepository)
                .listener(stepExecutionListener) // (5)
                .<SalesPlanDetail, SalesPlanDetail> chunk(10, transactionManager)
                .listener(chunkListener) // (1)
                .reader(reader) // (2)
                .processor(amountCheckProcessor) // (3)
                .writer(writer) // (4)
                .build();
    }

    @Bean
    public Job chunkJob(JobRepository jobRepository,
                        Step step01,
                        JobExecutionListener listener) {
        return new JobBuilder("chunkJob", jobRepository)
                .start(step01)
                .listener(listener) // (6)
                .build();
    }

    // for tasklet mode
    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       SalesPlanDetailRegisterTasklet tasklet,
                       StepExecutionListener listener) {
        return new StepBuilder("taskletJob.step01", jobRepository)
                .listener(listener) // (5)
                .tasklet(tasklet, transactionManager)
                .build();
    }

    @Bean
    public Job taskletJob(JobRepository jobRepository,
                          Step step01,
                          @Qualifier("jobExecutionListener") JobExecutionListener listener) {
        return new JobBuilder("taskletJob", jobRepository)
                .start(step01)
                .listener(listener) // (6)
                .build();
    }

表 68. 設定値の説明
項番 | 説明
--- | ---
(1) | ChunkListenerに属するアノテーションによる実装を含んだコンポーネントを設定する。
(2) | ItemReadListenerに属するアノテーションによる実装を含んだコンポーネントを設定する。
(3) | ItemProcessListenerに属するアノテーションによる実装を含んだコンポーネントを設定する。
(4) | ItemWriteListenerに属するアノテーションによる実装を含んだコンポーネントを設定する。
(5) | StepExecutionListenerに属するリスナーインタフェース実装を設定する。
(6) | JobListenerに属するリスナーを設定する。

XMLConfig
リスナーを設定する位置

    <!-- for chunk mode -->
    <batch:job id="chunkJob" job-repository="jobRepository">
        <batch:step id="chunkJob.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="(1)"
                             processor="(1)"
                             writer="(1)" commit-interval="10"/>
                <batch:listeners>
                    <batch:listener ref="(2)"/>
                </batch:listeners>
            </batch:tasklet>
        </batch:step>
        <batch:listeners>
            <batch:listener ref="(3)"/>
        </batch:listeners>
    </batch:job>

    <!-- for tasklet mode -->
    <batch:job id="taskletJob" job-repository="jobRepository">
        <batch:step id="taskletJob.step01">
            <batch:tasklet transaction-manager="jobTransactionManager" ref="tasklet">
                <batch:listeners>
                    <batch:listener ref="(2)"/>
                </batch:listeners>
            </batch:tasklet>
        </batch:step>
        <batch:listeners>
            <batch:listener ref="(3)"/>
        </batch:listeners>
    </batch:job>

表 69. 設定値の説明
項番 | 説明
--- | ---
(1) | StepListenerに属するアノテーションによる実装を含んだコンポーネントを設定する。 アノテーションの場合、必然的にこの場所に設定することになる。
(2) | StepListenerに属するリスナーインタフェース実装を設定する。 タスクレットモデルの場合、`ItemReadListener`,`ItemProcessListener`,`ItemWriteListener`は利用できない。
(3) | JobListenerに属するリスナーを設定する。 インタフェースとアノテーション、どちらの実装でもここに設定する必要がある。

###### 4.5.2.2.1. 複数リスナーの設定

`<batch:listeners>`要素には複数のリスナーを設定することができる。

複数のリスナーを登録したときに、リスナーがどのような順番で起動されるかを以下に示す。

- ItemProcessListener実装

  - listenerA, listenerB

- JobExecutionListener実装

  - listenerC, listenerD

JavaConfig
複数リスナーの設定例

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<SalesPlanDetail> reader,
                       ItemWriter<SalesPlanDetail> writer,
                       AmountCheckProcessor processor,
                       @Qualifier("listenerA") ItemProcessListener listenerA,
                       @Qualifier("listenerB") ItemProcessListener listenerB) {
        return new StepBuilder("chunkJob.step01", jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(10, transactionManager)
                .listener(listenerA)
                .listener(listenerB)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .build();
    }

    @Bean
    public Job taskletJob(JobRepository jobRepository,
                          Step step01,
                          @Qualifier("listenerC") JobExecutionListener listenerC,
                          @Qualifier("listenerD") JobExecutionListener listenerD) {
        return new JobBuilder("taskletJob", jobRepository)
                .start(step01)
                .listener(listenerC)
                .listener(listenerD)
                .build();
    }

XMLConfig
複数リスナーの設定例

    <batch:job id="chunkJob" job-repository="jobRepository">
        <batch:step id="chunkJob.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="reader"
                             processor="pocessor"
                             writer="writer" commit-interval="10"/>
                <batch:listeners>
                    <batch:listener ref="listenerA"/>
                    <batch:listener ref="listenerB"/>
                </batch:listeners>
            </batch:tasklet>
        </batch:step>
        <batch:listeners>
            <batch:listener ref="listenerC"/>
            <batch:listener ref="listenerD"/>
        </batch:listeners>
    </batch:job>

図 28. リスナーの起動順序

- 前処理に該当する処理は、リスナーの登録順に起動される。

- 後処理またはエラー処理に該当する処理は、リスナー登録の逆順に起動される。

##### 4.5.2.3. インタフェースとアノテーションの使い分け

リスナーインタフェースとアノテーションによるリスナーの使い分けを説明する。

リスナーインタフェース

  job、step、chunkにおいて共通する横断的な処理の場合に利用する。

アノテーション

  ビジネスロジック固有の処理を行いたい場合に利用する。
  原則として、ItemProcessorに対してのみ実装する。

##### 4.5.2.4. StepExecutionListenerでの前処理における例外発生

前処理(`beforeStep`メソッド)で例外が発生した場合、モデルによりリソースのオープン/クローズの実行有無が変わる。それぞれのモデルにおいて前処理で例外が発生した場合について説明する。

チャンクモデル

  リソースのオープン前に前処理が実行されるため、リソースのオープンは行われない。
  リソースのクローズは、リソースのオープンがされていない場合でも実行されるため、`ItemReader`/`ItemWriter`を実装する場合にはこのことに注意する必要がある。

タスクレットモデル

  タスクレットモデルでは、`execute`メソッド内で明示的にリソースのオープン/クローズを行う。
  前処理で例外が発生すると、`execute`メソッドは実行されないため、当然リソースのオープン/クローズも行われない。

##### 4.5.2.5. 前処理(StepExecutionListener#beforeStep())でのジョブの打ち切り

ジョブを実行する条件が整っていない場合、ジョブを実行する前に処理を打ち切りたい場合がある。

そのような場合は、前処理(`beforeStep`メソッド)にて例外をスローすることでジョブ実行前に処理を打ち切ることができる。
ここでは以下の要件を実装する場合を例に説明する。

1. `StepExecutionListener`が定義している`beforeStep`メソッドで入力ファイルと出力ファイルの起動パラメータの妥当性検証を行う。

2. 起動パラメータのいずれかが未指定の場合、例外をスローする。

しかし、Macchinetta Batch 2.xでは起動パラメータの妥当性検証は、`JobParametersValidator`の使用を推奨している。 ここでは、あくまでも前処理中断のサンプルとしてわかりやすい妥当性検証を利用しているため、実際に起動パラメータの妥当性検証を行う場合は"パラメータの妥当性検証"を参照。

以下に実装例を示す。

起動パラメータの妥当性検証を行うStepExecutionListenerの実装例

    @Component
    @Scope("step")
    public class CheckingJobParameterErrorStepExecutionListener implements StepExecutionListener {

        @Value("#{jobParameters['inputFile']}") // (1)
        private File inputFile;

        @Value("#{jobParameters['outputFile']}") // (1)
        private File outputFile;

        @Override
        public void beforeStep(StepExecution stepExecution) {
            if (inputFile == null) {
                throw new BeforeStepException("The input file must be not null."); // (2)
            }
            else if (outputFile == null) {
                throw new BeforeStepException("The output file must be not null."); // (2)
            }
        }

        @Override
        public ExitStatus afterStep(StepExecution stepExecution) {
            // omitted.
        }
    }

JavaConfig
リスナーの設定例

    @Bean
    @StepScope
    public LoggingReader reader(
        @Value("#{jobParameters['inputFile']}") File inputFile) { // (3)
        LoggingReader reader = new LoggingReader();
        reader.setResource(new FileSystemResource(inputFile));
        return reader;
    }

    @Bean
    @StepScope
    public LoggingWriter writer(
        @Value("#{jobParameters['outputFile']}") File outputFile) { // (3)
        LoggingWriter writer = new LoggingWriter();
        writer.setResource(new FileSystemResource(outputFile));
        return writer;
    }

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       LoggingReader reader,
                       LoggingWriter writer,
                       CheckingJobParameterErrorStepExecutionListener listener) {
        return new StepBuilder("chunkJobWithAbortListener.step01",
                jobRepository)
                .listener(listener) // (4)
                .<LoggingReader, LoggingWriter> chunk(10, transactionManager)
                .reader(reader)
                .writer(writer)
                .build();
    }

    @Bean
    public Job chunkJobWithAbortListener(JobRepository jobRepository,
                                        Step step01) {
        return new JobBuilder("chunkJobWithAbortListener",
                jobRepository)
                .start(step01)
                .build();
    }

XMLConfig
リスナーの設定例

    <bean id="reader" class="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch04.listener.LoggingReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}"/> <!-- (3) -->
    <bean id="writer" class="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch04.listener.LoggingWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"/> <!-- (3) -->

    <batch:job id="chunkJobWithAbortListener" job-repository="jobRepository">
        <batch:step id="chunkJobWithAbortListener.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="reader" writer="writer" commit-interval="10"/>
            </batch:tasklet>
            <batch:listeners>
                <batch:listener ref="checkingJobParameterErrorStepExecutionListener"/> <!-- (4) -->
            </batch:listeners>
        </batch:step>
    </batch:job>

表 70. 説明
項番 | 説明
--- | ---
(1) | @Valueアノテーションを使用して参照するパラメータを指定する。
(2) | 例外をスローする。 この例では`RuntimeException`クラスを継承し、自作した例外クラスを使用している。
(3) | 参照するパラメータを指定する。 パラメータをセットしているクラスはそれぞれ`ItemStreamReader`、`ItemStreamWriter`を実装した独自クラスである。
(4) | リスナーインタフェース実装を設定する。

## 5. データの入出力

### 5.1. トランザクション制御

#### 5.1.1. Overview

本節では、ジョブにおけるトランザクション制御について以下の順序で説明する。

1. 一般的なバッチ処理におけるトランザクション制御のパターンについて

2. Spring Batchにおけるトランザクション制御

3. "データベースやファイルといったリソースをトランザクショナルに処理するための方法"

本機能は、チャンクモデルとタスクレットモデルとで使い方が異なるため、それぞれについて説明する。

##### 5.1.1.1. 一般的なバッチ処理におけるトランザクション制御のパターンについて

一般的に、バッチ処理は大量件数を処理するため、処理の終盤で何かしらのエラーが発生した場合に全件処理しなおしとなってしまうと バッチシステムのスケジュールに悪影響を与えてしまう。
これを避けるために、1ジョブの処理内で一定件数ごとにトランザクションを確定しながら処理を進めていくことで、 エラー発生時の影響を局所化することが多い。
(以降、一定件数ごとにトランザクションを確定する方式を「中間コミット方式」、コミット単位にデータをひとまとめにしたものを「チャンク」と呼ぶ。)

中間コミット方式のポイントを以下にまとめる。

1. エラー発生時の影響を局所化する。

   - 更新時にエラーが発生しても、エラー箇所直前のチャンクまでの処理が確定している。

2. リソースを一定量しか使わない。

   - 処理対象データの大小問わず、チャンク分のリソースしか使用しないため安定する。

ただし、中間コミット方式があらゆる場面で有効な方法というわけではない。
システム内に一時的とはいえ処理済みデータと未処理データが混在することになる。 その結果、リカバリ処理時に未処理データを識別することが必要となるため、リカバリが複雑になる可能性がある。 これを避けるには、中間コミット方式ではなく、全件を1トランザクションで確定させるしかない。
(以降、全件を1トランザクションで確定する方式を「一括コミット方式」と呼ぶ。)

とはいえ、何千万件というような大量件数を一括コミット方式で処理してしまうと、 コミットを行った際に全件をデータベース反映しようとして高負荷をかけてしまうような事態が発生する。 そのため、一括コミット方式は小規模なバッチ処理には向いているが、大規模バッチで採用するには注意が必要となる。 よって、この方法も万能な方法というわけではない。

つまり、「影響の局所化」と「リカバリの容易さ」はトレードオフの関係にある。 「中間コミット方式」と「一括コミット方式」のどちらを使うかは、ジョブの性質に応じてどちらを優先すべきかを決定してほしい。
もちろん、バッチシステム内のジョブすべてをどちらか一方で実現する必要はない。 基本的には「中間コミット方式」を採用するが、特殊なジョブのみ「一括コミット方式」を採用する(または、その逆とする)ことは自然である。

以下に、「中間コミット方式」と「一括コミット方式」のメリット・デメリット、採用ポイントをまとめる。

表 71. 方式別特徴一覧
コミット方式 | メリット | デメリット | 採用ポイント
--- | --- | --- | ---
中間コミット方式 | エラー発生時の影響を局所化する | リカバリ処理が複雑になる可能性がある | 大量データを一定のマシンリソースで処理したい場合
一括コミット方式 | データの整合性を担保する | 大量件数処理時に高負荷になる可能性がある | 永続化リソースに対する処理結果をAll or Nothingとしたい場合 小規模のバッチ処理に向いている

| データベースの同一テーブルへ入出力する際の注意点 データベースの仕組み上、コミット方式を問わず、 同一テーブルへ入出力する処理で大量データを取り扱う際に注意が必要な点がある。 読み取り一貫性を担保するための情報が出力(UPDATEの発行)により失われた結果、 入力(SELECT)にてエラーが発生することがある。 これを回避するには、以下の対策がある。 情報を確保する領域を大きくする。 拡張する際には、リソース設計にて十分検討の上実施してほしい。 拡張方法は使用するデータベースに依存するため、マニュアルを参照。 入力データを分割し多重処理を行う。 多重処理については、"Partitioning Step (多重処理)"を参照。

#### 5.1.2. Architecture

##### 5.1.2.1. Spring Batchにおけるトランザクション制御

ジョブのトランザクション制御はSpring Batchがもつ仕組みを活用する。

以下に2種類のトランザクションを定義する。

フレームワークトランザクション

  Spring Batchが制御するトランザクション

ユーザトランザクション

  ユーザが制御するトランザクション

###### 5.1.2.1.1. チャンクモデルにおけるトランザクション制御の仕組み

チャンクモデルにおけるトランザクション制御は、中間コミット方式のみとなる。 一括コミット方式は実現できない。

| チャンクモデルにおける一括コミット方式についてはJIRAにレポートされている。 Spring Batch/BATCH-647 結果、`chunk completion policy`をカスタマイズしてチャンクサイズを動的に変更することで解決している。 しかし、この方法では全データを1チャンクに格納してしまいメモリを圧迫してしまうため、方式として採用することはできない。

この方式の特徴は、チャンク単位にトランザクションが繰り返し行われることである。

正常系でのトランザクション制御

  正常系でのトランザクション制御を説明する。

図 29. 正常系のシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

   - 入力データがなくなるまで2から8までの処理を繰り返す。

   - チャンク単位で、フレームワークトランザクションを開始する。

   - チャンクサイズに達するまで2から5までの処理を繰り返す。

2. ステップは、`ItemReader`から入力データを取得する。

3. `ItemReader`は、ステップに入力データを返却する。

4. ステップは、`ItemProcessor`で入力データに対して処理を行う。

5. `ItemProcessor`は、ステップに処理結果を返却する。

6. ステップはチャンクサイズ分のデータを`ItemWriter`で出力する。

7. `ItemWriter`は、対象となるリソースへ出力を行う。

8. ステップはフレームワークトランザクションをコミットする。

異常系でのトランザクション制御

  異常系でのトランザクション制御を説明する。

図 30. 異常系のシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

   - 入力データがなくなるまで2から7までの処理を繰り返す。

   - チャンク単位で、フレームワークトランザクションを開始する。

   - チャンクサイズに達するまで2から5までの処理を繰り返す。

2. ステップは、`ItemReader`から入力データを取得する。

3. `ItemReader`は、ステップに入力データを返却する。

4. ステップは、`ItemProcessor`で入力データに対して処理を行う。

5. `ItemProcessor`は、ステップに処理結果を返却する。

6. ステップはチャンクサイズ分のデータを`ItemWriter`で出力する。

7. `ItemWriter`は、対象となるリソースへ出力を行う。

   - 2から7までの処理過程で例外が発生すると、その時点で実行中の処理を中断し、以降の処理を行う。

8. ステップはフレームワークトランザクションをロールバックする。

###### 5.1.2.1.2. タスクレットモデルにおけるトランザクション制御の仕組み

タスクレットモデルにおけるトランザクション制御は、下記のいずれかの方式を利用できる。

- タスクレットモデルにおける一括コミット方式

- タスクレットモデルにおける中間コミット方式

以下、これらを順に説明する。

###### 5.1.2.1.3. タスクレットモデルにおける一括コミット方式

一括コミット方式では、Spring Batchがタスクレット起動時に開始されるトランザクション制御の仕組みをそのまま利用する。
この方式の特徴は、1つのトランザクション内で繰り返しデータ処理を行うことである。

正常系でのトランザクション制御

  正常系でのトランザクション制御を説明する。

図 31. 正常系のシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

   - ステップはフレームワークトランザクションを開始する。

2. ステップはタスクレットを実行する。

   - 入力データがなくなるまで3から7までの処理を繰り返す。

3. タスクレットは、`Repository`から入力データを取得する。

4. `Repository`は、タスクレットに入力データを返却する。

5. タスクレットは、入力データを処理する。

6. タスクレットは、`Repository`へ出力データを渡す。

7. `Repository`は、対象となるリソースへ出力を行う。

8. タスクレットはステップへ処理終了を返却する。

9. ステップはフレームワークトランザクションをコミットする。

| タスクレットモデルにおける一括コミット方式での注意点 タスクレットモデルで一括コミット方式を利用する場合、Taskletはフレームワークトランザクションの管理下で実行されるため、 StepがTasklet実行前にフレームワークトランザクションを開始し、TaskletがStepに処理終了を返すことでコミットまたはロールバックされ、トランザクションが終了する。 そのため、たとえば、常駐型のバッチプロセスを作る目的で、Tasklet実装内に無限ループを起こすような処理を記述すると、フレームワークトランザクションが終了しなくなる。 利用するDBMSの仕様によっては、上記のようなトランザクションの長期間滞留は性能劣化を引き起こす原因となり得るため、プロセス常駐を目的としたTaskletの実装は避けるべきである。 プロセス常駐型のジョブ実行方式として、非同期実行方式(非同期実行(DBポーリング)、非同期実行(Webコンテナ))が適用可能であるかを検討してほしい。

異常系でのトランザクション制御

  異常系でのトランザクション制御を説明する。

図 32. 異常系のシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

   - ステップはフレームワークトランザクションを開始する。

2. ステップはタスクレットを実行する。

   - 入力データがなくなるまで3から7までの処理を繰り返す。

3. タスクレットは、`Repository`から入力データを取得する。

4. `Repository`は、タスクレットに入力データを返却する。

5. タスクレットは、入力データを処理する。

6. タスクレットは、`Repository`へ出力データを渡す。

7. `Repository`は、対象となるリソースへ出力を行う。

   - 2から7までの処理過程で例外が発生すると、その時点で実行中の処理を中断し、以降の処理を行う。

8. タスクレットはステップへ例外をスローする。

9. ステップはフレームワークトランザクションをロールバックする。

###### 5.1.2.1.4. タスクレットモデルにおける中間コミット方式

中間コミット方式では、ユーザにてトランザクションを直接操作する。
この方式の特徴は、リソースの操作を行えないフレームワークトランザクションを利用することで、ユーザトランザクションのみにリソースの操作を行わせることである。
`transaction-manager`属性に、リソースが紐づかない`org.springframework.batch.support.transaction.ResourcelessTransactionManager`を指定する。

正常系でのトランザクション制御

  正常系でのトランザクション制御を説明する。

図 33. 正常系のシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

   - ステップはフレームワークトランザクションを開始する。

2. ステップはタスクレットを実行する。

   - 入力データがなくなるまで3から10までの処理を繰り返す。

3. タスクレットは、`TransactionManager`よりユーザトランザクションを開始する。

   - チャンクサイズに達するまで4から8までの処理を繰り返す。

4. タスクレットは、`Repository`から入力データを取得する。

5. `Repository`は、タスクレットに入力データを返却する。

6. タスクレットは、入力データを処理する。

7. タスクレットは、`Repository`へ出力データを渡す。

8. `Repository`は、対象となるリソースへ出力を行う。

9. タスクレットは、`TransactionManager`によりユーザトランザクションのコミットを実行する。

10. `TransactionManager`は、対象となるリソースへコミットを発行する。

11. タスクレットはステップへ処理終了を返却する。

12. ステップはフレームワークトランザクションをコミットする。

| ここでは1件ごとにリソースへ出力しているが、 チャンクモデルと同様に、チャンク単位で一括更新し処理スループットの向上を狙うことも可能である。 その際に、`SqlSessionTemplate`の`executorType`を`BATCH`に設定することで、BatchUpdateを利用することもできる。 これは、MyBatisのItemWriterを利用する場合と同様の動作になるため、MyBatisのItemWriterを利用して更新してもよい。 MyBatisのItemWriterについて、詳細は MyBatisBatchItemWriterを参照。

異常系でのトランザクション制御

  異常系でのトランザクション制御を説明する。

図 34. 異常系のシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

   - ステップはフレームワークトランザクションを開始する。

2. ステップはタスクレットを実行する。

   - 入力データがなくなるまで3から11までの処理を繰り返す。

3. タスクレットは、`TransactionManager`よりユーザトランザクションを開始する。

   - チャンクサイズに達するまで4から8までの処理を繰り返す。

4. タスクレットは、`Repository`から入力データを取得する。

5. `Repository`は、タスクレットに入力データを返却する。

6. タスクレットは、入力データを処理する。

7. タスクレットは、`Repository`へ出力データを渡す。

8. `Repository`は、対象となるリソースへ出力を行う。

   - 3から8までの処理過程で例外が発生すると、その時点で実行中の処理を中断し、以降の処理を行う。

9. タスクレットは、発生した例外に対する処理を行う。

10. タスクレットは、`TransactionManager`によりユーザトランザクションのロールバックを実行する。

11. `TransactionManager`は、対象となるリソースへロールバックを発行する。

12. タスクレットはステップへ例外をスローする。

13. ステップはフレームワークトランザクションをロールバックする。

| 処理の継続について ここでは、例外をハンドリングして処理をロールバック後、処理を異常終了しているが、 継続して次のチャンクを処理することも可能である。 いずれの場合も、途中でエラーが発生したことをステップのステータス・終了コードを変更することで後続の処理に通知する必要がある。

| フレームワークトランザクションについて ここでは、ユーザトランザクションをロールバック後に例外をスローしてジョブを異常終了させているが、 ステップへ処理終了を返却しジョブを正常終了させることも出来る。 この場合、フレームワークトランザクションは、コミットされる。

###### 5.1.2.1.5. モデル別トランザクション制御の選定方針

Macchinetta Batch 2.xの基盤となるSpring Batchでは、チャンクモデルでは中間コミット方式しか実現できない。 しかし、タスクレットモデルでは、中間コミット方式、一括コミット方式のいずれも実現できる。

よって、Macchinetta Batch 2.xでは、一括コミット方式が必要な際は、タスクレットモデルにて実装する。

##### 5.1.2.2. 起動方式ごとのトランザクション制御の差

起動方式によってはジョブの起動前後にSpring Batchの管理外となるトランザクションが発生する。 ここでは、2つの非同期実行処理方式におけるトランザクションについて説明する。

###### 5.1.2.2.1. DBポーリングのトランザクションについて

DBポーリングが行うジョブ要求テーブルへの処理については、Spring Batch管理外のトランザクション処理が行われる。 また、ジョブで発生した例外については、ジョブ内で対応が完結するため、`JobRequestPollTask`が行うトランザクションには影響を与えない。

下図にトランザクションに焦点を当てた簡易的なシーケンス図を示す。

図 35. DBポーリング処理のトランザクション
シーケンス図の説明

1. 非同期バッチデーモンで`JobRequestPollTask`が周期実行される。

2. `JobRequestPollTask`は、Spring Batch管理外のトランザクションを開始する。

3. `JobRequestPollTask`は、ジョブ要求テーブルから非同期実行対象ジョブを取得する。

4. `JobRequestPollTask`は、Spring Batch管理外のトランザクションをコミットする。

5. `JobRequestPollTask`は、Spring Batch管理外のトランザクションを開始する。

6. `JobRequestPollTask`は、ジョブ要求テーブルのポーリングステータスをINITからPOLLEDへ更新する。

7. `JobRequestPollTask`は、Spring Batch管理外のトランザクションをコミットする。

8. `JobRequestPollTask`は、ジョブを実行する。

9. ジョブ内では、管理用データベース(`JobRepository`)へのトランザクション管理はSpring Batchが行う。

10. ジョブ内では、ジョブ用データベースへのトランザクション管理はSpring Batchが行う。

11. `JobRequestPollTask`にjob_execution_idが返却される。

12. `JobRequestPollTask`は、Spring Batch管理外のトランザクションを開始する。

13. `JobRequestPollTask`は、ジョブ要求テーブルのポーリングステータスをPOLLEDからEXECUTEへ更新する。

14. `JobRequestPollTask`は、Spring Batch管理外のトランザクションをコミットする。

| SELECT発行時のコミットについて データベースによっては、SELECT発行時に暗黙的にトランザクションを開始する場合がある。 そのため、明示的にコミットを発行することでトランザクションを確定させ、他のトランザクションと明確に区別し影響を与えないようにしている。

###### 5.1.2.2.2. WebAPサーバ処理のトランザクションについて

WebAPが対象とするリソースへの処理については、Spring Batch管理外のトランザクション処理が行われる。 また、ジョブで発生した例外については、ジョブ内で対応が完結するため、WebAPが行うトランザクションには影響を与えない。

下図にトランザクションに焦点を当てた簡易的なシーケンス図を示す。

図 36. WebAPサーバ処理のトランザクション
シーケンス図の説明

1. クライアントからリクエストによりWebAPの処理が実行される。

2. WebAPは、Spring Batch管理外のトランザクションを開始する。

3. WebAPは、ジョブ実行前にWebAPでのリソースに対して読み書きを行う。

4. WebAPは、ジョブを実行する。

5. ジョブ内では、管理用データベース(`JobRepository`)へのトランザクション管理はSpring Batchが行う。

6. ジョブ内では、ジョブ用データベースへのトランザクション管理はSpring Batchが行う。

7. WebAPにjob_execution_idが返却される。

8. WebAPは、ジョブ実行後にWebAPでのリソースに対して読み書きを行う。

9. WebAPは、Spring Batch管理外のトランザクションをコミットする。

10. WebAPは、クライアントにレスポンスを返す。

#### 5.1.3. How to use

ここでは、1ジョブにおけるトランザクション制御について、以下の場合に分けて説明する。

- 単一データソースの場合

- 複数データソースの場合

データソースとは、データの格納先(データベース、ファイル等)を指す。 単一データソースとは1つのデータソースを、複数データソースとは2つ以上のデータソースを指す。

単一データソースを処理するケースは、データベースのデータを加工するケースが代表的である。
複数データソースを処理するケースは、以下のようにいくつかバリエーションがある。

- 複数のデータベースの場合

- データベースとファイルの場合

##### 5.1.3.1. 単一データソースの場合

1つのデータソースに対して入出力するジョブのトランザクション制御について説明する。

以下にMacchinetta Batch 2.xでの設定例を示す。

JavaConfig
データソースの設定(com.example.batch.config.LaunchContextConfig.java)

    // Job-common definitions
    public BasicDataSource jobDataSource(@Value("${jdbc.driver}") String driverClassName,
                                         @Value("${jdbc.url}") String url,
                                         @Value("${jdbc.username}") String username,
                                         @Value("${jdbc.password}") String password) {
        final BasicDataSource basicDataSource = new BasicDataSource();
        basicDataSource.setDriverClassName(driverClassName);
        basicDataSource.setUrl(url);
        basicDataSource.setUsername(username);
        basicDataSource.setPassword(password);
        basicDataSource.setMaxTotal(10);
        basicDataSource.setMinIdle(1);
        basicDataSource.setMaxWait(Duration.ofMillis(5000L));
        basicDataSource.setDefaultAutoCommit(false);
        return basicDataSource;
    }

トランザクションマネージャの設定(com.example.batch.config.LaunchContextConfig.java)

    // (1)
    @Bean
    public PlatformTransactionManager jobTransactionManager(@Qualifier("jobDataSource") DataSource jobDataSource) {
        final DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(jobDataSource);
        dataSourceTransactionManager.setRollbackOnCommitFailure(true);
        return dataSourceTransactionManager;
    }

XMLConfig
データソースの設定(META-INF/spring/launch-context.xml)

    <!-- Job-common definitions -->
    <bean id="jobDataSource" class="org.apache.commons.dbcp2.BasicDataSource"
          destroy-method="close"
          p:driverClassName="${jdbc.driver}"
          p:url="${jdbc.url}"
          p:username="${jdbc.username}"
          p:password="${jdbc.password}"
          p:maxTotal="10"
          p:minIdle="1"
          p:defaultAutoCommit="false">
        <property name="maxWait">
            <bean class="java.time.Duration" factory-method="ofMillis">
                <constructor-arg name="millis" type="long" value="5000" />
            </bean>
        </property>
    </bean>

トランザクションマネージャの設定(META-INF/spring/launch-context.xml)

    <!-- (1) -->
    <bean id="jobTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
          p:dataSource-ref="jobDataSource"
          p:rollbackOnCommitFailure="true" />

項番 | 説明
--- | ---
(1) | トランザクションマネージャのBean定義 データソースは上記で定義した`jobDataSource`を設定する。 コミットに失敗した場合はロールバックをするように設定済み。

###### 5.1.3.1.1. トランザクション制御の実施

ジョブモデルおよびコミット方式により制御方法が異なる。

チャンクモデルの場合

チャンクモデルの場合は、中間コミット方式となり、Spring Batchにトランザクション制御を委ねる。 ユーザにて制御することは一切行わないようにする。

JavaConfig

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<SalesPlanDetail> reader,
                       ItemWriter<SalesPlanDetail> writer,
                       LoggingItemReaderListener listener) {
        return new StepBuilder("jobSalesPlan01.step01", jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(10, // (2)
                        transactionManager) // (1)
                .reader(reader)
                .writer(writer)
                .listener(listener)
                .build();
    }

    @Bean
    public Job jobSalesPlan01(JobRepository jobRepository, Step step01,
                              LoggingItemReaderListener listener) {
        return new JobBuilder("jobSalesPlan01", jobRepository)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig

    <batch:job id="jobSalesPlan01" job-repository="jobRepository">
        <batch:step id="jobSalesPlan01.step01">
            <batch:tasklet transaction-manager="jobTransactionManager"> <!-- (1) -->
                <batch:chunk reader="detailCSVReader"
                             writer="detailWriter"
                             commit-interval="10" /> <!-- (2) -->
            </batch:tasklet>
        </batch:step>
    </batch:job>

項番 | 説明
--- | ---
(1) | `StepBuilderのchunkメソッドの引数transactionManager/<batch:tasklet>要素のtransaction-manager属性`に定義済みの`jobTransactionManager`を設定する。 ここに設定したトランザクションマネージャで中間コミット方式のトランザクションを制御する。
(2) | `StepBuilderのchunkメソッドの引数chunkSize/commit-interval属性`にチャンクサイズを設定する。この例では10件処理するごとに1回コミットが発行される。

タスクレットモデルの場合

タスクレットモデルの場合は、一括コミット方式、中間コミット方式でトランザクション制御の方法が異なる。

一括コミット方式

  Spring Batchにトランザクション制御を委ねる。

JavaConfig

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       Tasklet salesPlanSingleTranTask) {
        return new StepBuilder("jobSalesPlan01.step01", jobRepository)
                .tasklet(salesPlanSingleTranTask, transactionManager) // (1)
                .build();
    }

    @Bean
    public Job jobSalesPlan01(JobRepository jobRepository, Step step01,
                              LoggingItemReaderListener listener) {
        return new JobBuilder("jobSalesPlan01", jobRepository)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig

    <batch:job id="jobSalesPlan01" job-repository="jobRepository">
        <batch:step id="jobSalesPlan01.step01">
            <!-- (1) -->
            <batch:tasklet transaction-manager="jobTransactionManager"
                           ref="salesPlanSingleTranTask" />
        </batch:step>
    </batch:job>

項番 | 説明
--- | ---
(1) | `StepBuilderのtaskletメソッドの引数transactionManager/<batch:tasklet>要素のtransaction-manager属性`に定義済みの`jobTransactionManager`を設定する。 ここに設定したトランザクションマネージャで一括コミット方式のトランザクションを制御する。

中間コミット方式

  ユーザにてトランザクション制御を行う。

  - 処理の途中でコミットを発行する場合は、`TransactionManager`をInjectして手動で行う。

JavaConfig

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobResourcelessTransactionManager") PlatformTransactionManager transactionManager,
                       Tasklet salesPlanSingleTranTask) {
        return new StepBuilder("jobSalesPlan01.step01", jobRepository)
                .tasklet(salesPlanSingleTranTask, transactionManager) // (1)
                .build();
    }

    @Bean
    public Job jobSalesPlan01(JobRepository jobRepository, Step step01,
                              LoggingItemReaderListener listener) {
        return new JobBuilder("jobSalesPlan01", jobRepository)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig

    <batch:job id="jobSalesPlan01" job-repository="jobRepository">
        <batch:step id="jobSalesPlan01.step01">
            <!-- (1) -->
            <batch:tasklet transaction-manager="jobResourcelessTransactionManager"
                           ref="salesPlanChunkTranTask" />
        </batch:step>
    </batch:job>

実装例

    @Component
    public class SalesPlanChunkTranTask implements Tasklet {

        @Inject
        ItemStreamReader<SalesPlanDetail> itemReader;

         // (2)
        @Inject
        @Named("jobTransactionManager")
        PlatformTransactionManager transactionManager;

        @Inject
        SalesPlanDetailRepository repository;

        private static final int CHUNK_SIZE = 10;

        @Override
        public RepeatStatus execute(StepContribution contribution,
                                    ChunkContext chunkContext) throws Exception {

            DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
            TransactionStatus status = null;
            int count = 0;

            try {
                // omitted.

                itemReader.open(executionContext);

                while ((item = itemReader.read()) != null) {

                    if (count % CHUNK_SIZE == 0) {
                        status = transactionManager.getTransaction(definition); // (3)
                    }
                    count++;
                    repository.create(item);
                    if (count % CHUNK_SIZE == 0) {
                        transactionManager.commit(status);  // (4)
                    }
                }
                if (status != null && !status.isCompleted()) {
                    transactionManager.commit(status);    // (5)
                }
            } catch (Exception e) {
                logger.error("Exception occurred while reading.", e);
                if (status != null && !status.isCompleted()) {
                        transactionManager.rollback(status);    // (6)
                }
                throw e;
            } finally {
                if (status != null && !status.isCompleted()) {
                    try {
                        transactionManager.rollback(status);    // (7)
                    } catch (TransactionException e2) {
                            logger.error("TransactionException occurred while rollback.", e2);
                    }
                }
                itemReader.close();
            }

            return RepeatStatus.FINISHED;
        }
    }

項番 | 説明
--- | ---
(1) | `StepBuilderのtaskletメソッドの引数transactionManager/<batch:tasklet>要素のtransaction-manager属性`に定義済みの`jobResourcelessTransactionManager`を設定する。
(2) | トランザクションマネージャをInjectする。 @Namedアノテーションで`jobTransactionManager`を指定して利用するBeanを特定させる。
(3) | チャンクの開始時にトランザクションを開始する。
(4) | チャンク終了時にトランザクションをコミットする。
(5) | 最後のチャンクについて、トランザクションをコミットする。
(6) | 例外発生時にはトランザクションをロールバックする。
(7) | トランザクションが終わっていない場合、トランザクションをロールバックする。

| ItemWriterによる更新 上記の例では、Repositoryを使用しているが、ItemWriterを利用してデータを更新することもできる。 ItemWriterを利用することで実装がシンプルになる効果があり、特にファイルを更新する場合はFlatFileItemWriterを利用するとよい。

###### 5.1.3.1.2. 非トランザクショナルなデータソースに対する補足

ファイルの場合はトランザクションの設定や操作は不要である。

`FlatFileItemWriter`を利用する場合、擬似的なトランザクション制御が行える。 これは、リソースへの書き込みを遅延し、コミットタイミングで実際に書き出すことで実現している。 正常時にはチャンクサイズに達したときに、実際のファイルにチャンク分データを出力し、例外が発生するとそのチャンクのデータ出力が行われない。

`FlatFileItemWriter`は、`transactional`プロパティでトランザクション制御の有無を切替えられる。デフォルトはtrueでトランザクション制御が有効になっている。 `transactional`プロパティがfalseの場合、`FlatFileItemWriter`は、トランザクションとは無関係にデータの出力を行う。

一括コミット方式を採用する場合、`transactional`プロパティをfalseにすることを推奨する。 上記の説明にあるとおりコミットのタイミングでリソースへ書き出すため、それまではメモリ内に全出力分のデータを保持することになる。 そのため、データ量が多い場合にはメモリ不足になりエラーとなる可能性が高くなる。

###### 5.1.3.1.3. ファイルしか扱わないジョブにおけるTransactionManagerの設定について

以下に示すジョブ定義のように、`StepBuilderのchunkメソッドの引数transactionManager/batch:taskletのtransaction-manager属性`は必須のため省略できない。

JavaConfig
TransactionManager設定箇所の抜粋

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<SalesPlanDetail> reader,
                       ItemWriter<SalesPlanDetail> writer) {
        return new StepBuilder("jobSalesPlan01.step01", jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(100,
                        transactionManager)
                .reader(reader)
                .writer(writer)
                .build();
    }

XMLConfig
TransactionManager設定箇所の抜粋

    <batch:tasklet transaction-manager="jobTransactionManager">
    <batch:chunk reader="reader" writer="writer" commit-interval="100" />
    </batch:tasklet>

そのため、`jobTransactionManager`を常に指定すること。この時、以下の挙動となる。

- `transactional`がtrueの場合

  - 指定したTransactionManagerに同期してリソースに出力する。

- `transactional`がfalseの場合

  - 指定したTransactionManagerのトランザクション処理は空振りし、トランザクションと関係なくリソースに出力する。

この時、`jobTransactionManager`が参照するリソース(たとえば、データベース)に対してトランザクションが発行されるが、 テーブルアクセスは伴わないため実害がない。

また、実害がある場合や空振りでも参照するトランザクションを発行したくない場合は、リソースを必要としない`ResourcelessTransactionManager`を使用することができる。 `ResourcelessTransactionManager`は`LaunchContextConfig.java/launch-context.xml`に`jobResourcelessTransactionManager`として定義済みである。

JavaConfig
ResourcelessTransactionManagerの使用例

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobResourcelessTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<SalesPlanDetail> reader,
                       ItemWriter<SalesPlanDetail> writer) {
        return new StepBuilder("jobSalesPlan01.step01", jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(100,
                        transactionManager)
                .reader(reader)
                .writer(writer)
                .build();
    }

XMLConfig
ResourcelessTransactionManagerの使用例

    <batch:tasklet transaction-manager="jobResourcelessTransactionManager">
        <batch:chunk reader="reader" writer="writer" commit-interval="100" />
    </batch:tasklet>

##### 5.1.3.2. 複数データソースの場合

複数データソースに対して入出力するジョブのトランザクション制御について説明する。 入力と出力で考慮点が異なるため、これらを分けて説明する。

###### 5.1.3.2.1. 複数データソースからの取得

複数データソースからのデータを取得する場合、処理の軸となるデータと、それに付随する追加データを分けて取得する。 以降は、処理の軸となるデータを処理対象レコード、それに付随する追加データを付随データと呼ぶ。

Spring Batchの構造上、ItemReaderは1つのリソースから処理対象レコードを取得することを前提としているためである。 これは、リソースの種類を問わず同じ考え方となる。

1. 処理対象レコードの取得

   - ItemReaderにて取得する。

2. 付随データの取得

   - 付随データは、そのデータに対す変更の有無と件数に応じて、以下の取得方法を選択する必要がある。これは、択一ではなく、併用してもよい。

     - ステップ実行前に一括取得

     - 処理対象レコードに応じて都度取得

ステップ実行前に一括取得する場合

以下を行うListenerを実装し、以降のStepからデータを参照する。

- データを一括して取得する

- スコープが`Job`または`Step`のBeanに情報を格納する

  - Spring Batchの`ExecutionContext`を活用してもよいが、 可読性や保守性のために別途データ格納用のクラスを作成してもよい。 ここでは、簡単のため`ExecutionContext`を活用した例で説明する。

マスタデータなど、処理対象データに依存しないデータを読み込む場合にこの方法を採用する。 ただし、マスタデータと言えど、メモリを圧迫するような大量件数が対象である場合は、都度取得したほうがよいかを検討すること。

一括取得するListenerの実装

    @Component
    // (1)
    public class BranchMasterReadStepListener implements StepExecutionListener {

        @Inject
        BranchRepository branchRepository;

        @Override
        public void beforeStep(StepExecution stepExecution) {   // (2)

            List<Branch> branches = branchRepository.findAll(); //(3)

            Map<String, Branch> map = branches.stream()
                    .collect(Collectors.toMap(Branch::getBranchId,
                            UnaryOperator.identity()));  // (4)

            stepExecution.getExecutionContext().put("branches", map); // (5)
        }
    }

JavaConfig

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<Customer> reader,
                       ItemWriter<Customer> writer,
                       RetrieveBranchFromContextItemProcessor processor,
                       BranchMasterReadStepListener listener) {
        return new StepBuilder("outputAllCustomerList01.step01",
                jobRepository)
                .<Customer, Customer> chunk(10, transactionManager)
                .reader(reader)
                .processor(processor)
                .listener(listener) // (6)
                .writer(writer)
                .build();
    }

    @Bean
    public Job outputAllCustomerList01(JobRepository jobRepository,
                                            Step step01) {
        return new JobBuilder("outputAllCustomerList01",jobRepository)
                .start(step01)
                .build();
    }

XMLConfig

    <batch:job id="outputAllCustomerList01" job-repository="jobRepository">
        <batch:step id="outputAllCustomerList01.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="reader"
                             processor="retrieveBranchFromContextItemProcessor"
                             writer="writer" commit-interval="10"/>
                <batch:listeners>
                    <batch:listener ref="branchMasterReadStepListener"/> <!-- (6) -->
                </batch:listeners>
            </batch:tasklet>
        </batch:step>
    </batch:job>

一括取得したデータを後続ステップのItemProcessorで参照する例

    @Component
    public class RetrieveBranchFromContextItemProcessor implements
            ItemProcessor<Customer, CustomerWithBranch> {

        private Map<String, Branch> branches;

        @BeforeStep       // (7)
        @SuppressWarnings("unchecked")
        public void beforeStep(StepExecution stepExecution) {
            branches = (Map<String, Branch>) stepExecution.getExecutionContext()
                    .get("branches"); // (8)
        }

        @Override
        public CustomerWithBranch process(Customer item) throws Exception {
            CustomerWithBranch newItem = new CustomerWithBranch(item);
            newItem.setBranch(branches.get(item.getChargeBranchId()));    // (9)
            return newItem;
        }
    }

項番 | 説明
--- | ---
(1) | `StepExecutionListener`インタフェースを実装する。
(2) | ステップ実行前にデータを取得するため、`beforeStep`メソッドを実装する。
(3) | マスタデータを取得する処理を実装する。
(4) | 後続処理が利用しやすいようにList型からMap型へ変換を行う。
(5) | ステップのコンテキストに取得したマスタデータを`branches`という名前で設定する。
(6) | 対象となるジョブへ作成した`Listener`を登録する。
(7) | `ItemProcessor`のステップ実行前にマスタデータを取得するため、`@BeforeStep`アノテーションで`Listener`設定を行う。
(8) | `@BeforeStep`アノテーションが付与されたメソッド内で、ステップのコンテキストから(5)で設定されたマスタデータを取得する。
(9) | `ItemProcessor`の`process`メソッド内で、マスタデータからデータ取得を行う。

| コンテキストへ格納するオブジェクト コンテキスト(`ExecutionContext`)へ格納するオブジェクトは、`java.io.Serializable`を実装したクラスでなければならない。 これは、`ExecutionContext`が`JobRepository`へ格納されるためである。

処理対象レコードに応じて都度取得する場合

業務処理のItemProcessorとは別に、都度取得専用のItemProcessorにて取得する。 これにより、各ItemProcessorの処理を簡素化する。

1. 都度取得用のItemProcessorを定義し、業務処理と分離する。

   - この際、テーブルアクセス時はMyBatisをそのまま使う。

2. 複数のItemProcessorをCompositeItemProcessorを使用して連結する。

   - ItemProcessorは`delegates`属性に指定した順番に処理されることに留意する。

都度取得用のItemProcessorの実装例

    @Component
    public class RetrieveBranchFromRepositoryItemProcessor implements
            ItemProcessor<Customer, CustomerWithBranch> {

        @Inject
        BranchRepository branchRepository;  // (1)

        @Override
        public CustomerWithBranch process(Customer item) throws Exception {
            CustomerWithBranch newItem = new CustomerWithBranch(item);
            newItem.setBranch(branchRepository.findOne(
                    item.getChargeBranchId())); // (2)
            return newItem; // (3)
        }
    }

JavaConfig

    @Bean
    public CompositeItemProcessor<Customer, CustomerWithBranch> compositeItemProcessor(
            RetrieveBranchFromRepositoryItemProcessor retrieveBranchFromRepositoryItemProcessor,
            BusinessLogicItemProcessor businessLogicItemProcessor) {
        return new CompositeItemProcessorBuilder<Customer, CustomerWithBranch>()
                .delegates(retrieveBranchFromRepositoryItemProcessor, businessLogicItemProcessor) // (4), (5)
                .build();
    }

XMLConfig

    <bean id="compositeItemProcessor"
          class="org.springframework.batch.item.support.CompositeItemProcessor">
        <property name="delegates">
            <list>
                <ref bean="retrieveBranchFromRepositoryItemProcessor"/> <!-- (4) -->
                <ref bean="businessLogicItemProcessor"/>  <!-- (5) -->
            </list>
        </property>
    </bean>

項番 | 説明
--- | ---
(1) | MyBatisを利用した都度データ取得用のRepositoryをInjectする。
(2) | 入力データ(処理対象レコード)に対して、Repositoryから付随データを取得する。
(3) | 処理対象レコードと付随データを一緒にしたデータを返却する。 このデータが次のItemProcessorへの入力データになることに注意する。
(4) | 都度取得用のItemProcessorを設定する。
(5) | ビジネスロジックのItemProcessorを設定する。

###### 5.1.3.2.2. 複数データソースへの出力(複数ステップ)

データソースごとにステップを分割し、各ステップで単一データソースを処理することで、ジョブ全体で複数データソースを処理する。

- 1ステップ目で加工したデータをテーブルに格納し、2ステップ目でファイルに出力する、といった要領となる。

- 各ステップがシンプルになりリカバリしやすい反面、2度手間になる可能性がある。

  - この結果、以下のような弊害を許容できない場合は、1ステップで複数データソースを処理することを検討する。

    - 処理時間が伸びてしまう

    - ビジネスロジックが冗長となる

###### 5.1.3.2.3. 複数データソースへの出力(1ステップ)

一般的に、複数のデータソースに対するトランザクションを1つにまとめる場合は、2phase-commitによる分散トランザクションを利用する。 しかし、以下の様なデメリットがあることも同時に知られている。

- XAResourceなど分散トランザクションAPIにミドルウエアが対応している必要があり、それにもとづいた特殊な設定が必要になる

- バッチプログラムのようなスタンドアロンJavaで、分散トランザクションのJTA実装ライブラリを追加する必要がある

- 障害時のリカバリが難しい

Spring Batchでも分散トランザクションを活用することは可能だが、JTAによるグローバルトランザクションを使用する方法では、プロトコルの特性上、性能面のオーバーヘッドがかかる。 より簡易に複数データソースをまとめて処理する方法として、Best Efforts 1PCパターンによる実現手段がある。

Best Efforts 1PCパターンとは

  端的に言うと、複数データソースをローカルトランザクションで扱い、同じタイミングで逐次コミットを発行するという手法を指す。 下図に概念図を示す。

図 37. Best Efforts 1PCパターンの概念図
図の説明

1. | ユーザが`ChainedTransactionManager`にトランザクション開始を指示する。
2~7. | `ChainedTransactionManager`は、登録されているトランザクションマネージャの逐次トランザクションを開始する。
8~10. | ユーザは各リソースへトランザクショナルな操作を行う。
11. | ユーザが`ChainedTransactionManager`にコミットを指示する。
12~17. | `ChainedTransactionManager`は、登録されているトランザクションマネージャの逐次コミットを発行する。なお、コミット(またはロールバック)の発行はトランザクション開始と逆順になる。

この方法は分散トランザクションではないため、2番目以降のトランザクションマネージャにおけるcommit/rollback時に障害(例外)が発生した場合に、 データの整合性が保てない可能性がある。 そのため、トランザクション境界で障害が発生した場合のリカバリ方法を設計する必要があるが、リカバリ頻度を低減し、リカバリ手順を簡潔にできる効果がある。

複数のトランザクショナルリソースを同時に処理する場合

複数のデータベースを同時に処理する場合や、データベースとMQを処理する場合などに活用する。

以下のように、`ChainedTransactionManager`を使用して複数トランザクションマネージャを1つにまとめて定義することで1phase-commitとして処理する。 なお、`ChainedTransactionManager`はSpring Dataが提供するクラスである。

pom.xml

    <dependencies>
        <!-- omitted -->
        <!-- (1) -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-commons</artifactId>
        </dependency>
    <dependencies>

JavaConfig

    // (2)
    @Bean
    public ChainedTransactionManager chainedTransactionManager(
            @Qualifier("transactionManager1") PlatformTransactionManager transactionManager1,
            @Qualifier("transactionManager2") PlatformTransactionManager transactionManager2) {
        return new ChainedTransactionManager(transactionManager1, transactionManager2); // (3)
    }

    @Bean
    public Step step01(JobRepository jobRepository,
                    @Qualifier("chainedTransactionManager") PlatformTransactionManager transactionManager, // (4)
                    ItemReader<SalesPlanDetail> reader,
                    ItemWriter<SalesPlanDetail> writer) {
        return new StepBuilder("jobSalesPlan01.step01",
                jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(10, transactionManager)
                .reader(reader)
                .writer(writer)
                .build();
    }

    @Bean
    public Job jobSalesPlan01(JobRepository jobRepository,
                                            Step step01) {
        return new JobBuilder("jobSalesPlan01", jobRepository)
                .start(step01)
                .build();
    }

XMLConfig

    <!-- Chained Transaction Manager -->
    <!-- (2) -->
    <bean id="chainedTransactionManager"
          class="org.springframework.data.transaction.ChainedTransactionManager">
       <constructor-arg>
            <!-- (3) -->
            <list>
                <ref bean="transactionManager1"/>
                <ref bean="transactionManager2"/>
            </list>
        </constructor-arg>
    </bean>

    <batch:job id="jobSalesPlan01" job-repository="jobRepository">
        <batch:step id="jobSalesPlan01.step01">
            <!-- (4) -->
            <batch:tasklet transaction-manager="chainedTransactionManager">
                <!-- omitted -->
            </batch:tasklet>
        </batch:step>
    </batch:job>

項番 | 説明
--- | ---
(1) | `ChainedTransactionManager`を利用するために、依存関係を追加する。
(2) | `ChainedTransactionManager`のBean定義を行う。 尚、 spring-data-commons 2.5.0で`ChainedTransactionManager`は非推奨であり、予告なく削除される場合があるため注意すること。 spring-data-commons/ChainedTransactionManager
(3) | まとめたい複数のトランザクションマネージャをリストで定義する。
(4) | ジョブが利用するトランザクションマネージャに(2)で定義したBeanIDを指定する。

トランザクショナルリソースと非トランザクショナルリソースを同時に処理する場合

この方法は、データベースとファイルを同時に処理する場合に活用する。

データベースについては単一データソースの場合と同様。

ファイルについてはFlatFileItemWriterの`transactional`プロパティをtrueに設定することで、前述の「Best Efforts 1PCパターン」と同様の効果となる。
詳細は非トランザクショナルなデータソースに対する補足を参照。

この設定は、データベースのトランザクションをコミットする直前までファイルへの書き込みを遅延させるため、2つのデータソースで同期がとりやすくなる。 ただし、この場合でもデータベースへのコミット後、ファイル出力処理中に異常が発生した場合はデータの整合性が保てない可能性があるため、 リカバリ方法を設計する必要がある。

##### 5.1.3.3. 中間方式コミットでの注意点

非推奨ではあるがItemWriterで処理データをスキップする場合は、チャンクサイズの設定値が強制変更される。 そのことがトランザクションに非常に大きく影響することに注意する。詳細は、 スキップを参照。

### 5.2. データベースアクセス

#### 5.2.1. Overview

Macchinetta Batch 2.xでは、データベースアクセスの方法として、MyBatis3(以降、「MyBatis」と呼ぶ)を利用する。 MyBatisによるデータベースアクセスの基本的な利用方法は、Macchinetta Server 1.x 開発ガイドラインの以下を参照。

- データベースアクセス(共通編)

- データベースアクセス(MyBatis3編)

本節では、Macchinetta Batch 2.x特有の使い方を中心に説明する。

#### 5.2.2. How to use

Macchinetta Batch 2.xでのデータベースアクセス方法を説明する。

なお、チャンクモデルとタスクレットモデルにおけるデータベースアクセス方法の違いに留意する。

Macchinetta Batch 2.xでのデータベースアクセスは、以下の2つの方法がある。
これらはデータベースアクセスするコンポーネントによって使い分ける。

1. MyBatis用のItemReaderおよびItemWriterを利用する。

   - チャンクモデルでのデータベースアクセスによる入出力で使用する。

     - org.mybatis.spring.batch.MyBatisCursorItemReader

     - org.mybatis.spring.batch.MyBatisBatchItemWriter

2. `Mapper`インタフェースを利用する

   - チャンクモデルでのビジネスロジック処理で使用する。

     - ItemProcessor実装で利用する。

   - タスクレットモデルでのデータベースアクセス全般で使用する。

     - Tasklet実装で利用する。

##### 5.2.2.1. 共通設定

データベースアクセスにおいて必要な共通設定について説明を行う。

1. データソースの設定

2. MyBatisの設定

3. Mapper XMLの定義

4. MyBatis-Springの設定

###### 5.2.2.1.1. データソースの設定

Macchinetta Batch 2.xでは、2つのデータソースを前提としている。 `LaunchContextConfig.java/launch-context.xml`でデフォルト設定している2つのデータソースを示す。

表 72. データソース一覧
データソース名 | 説明
--- | ---
`adminDataSource` | Spring BatchやMacchinetta Batch 2.xが利用するデータソース JobRepositoryや非同期実行(DBポーリング)で利用している。
`jobDataSource` | ジョブが利用するデータソース

| JobRepositoryのトランザクション `JobRepositoy`と業務ジョブは利用するデータソースおよび、トランザクションが異なる。 そのため、`JobRepositoy`への更新は、ユーザが使用するデータベースのトランザクションとは独立したトランザクションで行われる。 つまり、業務処理に対する障害のみがリカバリ対象となる。

以下に、LaunchContextConfig.java/launch-context.xmlと接続情報のプロパティを示す。
これらをユーザの環境に合わせて設定すること。

JavaConfig
jp.co.ntt.fw.macchinetta.batch.functionaltest.config.LaunchContextConfig.java

    // (1)
    @Bean
    public BasicDataSource adminDataSource(@Value("${admin.h2.jdbc.driver}") String driverClassName,
                                           @Value("${admin.h2.jdbc.url}") String url,
                                           @Value("${admin.h2.jdbc.username}") String username,
                                           @Value("${admin.h2.jdbc.password}") String password) {
        final BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setMaxTotal(10);
        dataSource.setMinIdle(1);
        dataSource.setMaxWait(Duration.ofMillis(5000L));
        dataSource.setDefaultAutoCommit(false);
        return dataSource;
    }

    // (2)
    @Bean
    public BasicDataSource jobDataSource(@Value("${jdbc.driver}") String driverClassName,
                                         @Value("${jdbc.url}") String url,
                                         @Value("${jdbc.username}") String username,
                                         @Value("${jdbc.password}") String password) {
        final BasicDataSource basicDataSource = new BasicDataSource();
        basicDataSource.setDriverClassName(driverClassName);
        basicDataSource.setUrl(url);
        basicDataSource.setUsername(username);
        basicDataSource.setPassword(password);
        basicDataSource.setMaxTotal(10);
        basicDataSource.setMinIdle(1);
        basicDataSource.setMaxWait(Duration.ofMillis(5000L));
        basicDataSource.setDefaultAutoCommit(false);
        return basicDataSource;
    }

XMLConfig
resources\META-INF\spring\launch-context.xml

    <!-- (1) -->
    <bean id="adminDataSource" class="org.apache.commons.dbcp2.BasicDataSource"
          destroy-method="close"
          p:driverClassName="${admin.h2.jdbc.driver}"
          p:url="${admin.h2.jdbc.url}"
          p:username="${admin.h2.jdbc.username}"
          p:password="${admin.h2.jdbc.password}"
          p:maxTotal="10"
          p:minIdle="1"
          p:defaultAutoCommit="false">
        <property name="maxWait">
            <bean class="java.time.Duration" factory-method="ofMillis">
                <constructor-arg name="millis" type="long" value="5000" />
            </bean>
        </property>
    </bean>

    <!-- (2) -->
    <bean id="jobDataSource" class="org.apache.commons.dbcp2.BasicDataSource"
          destroy-method="close"
          p:driverClassName="${jdbc.driver}"
          p:url="${jdbc.url}"
          p:username="${jdbc.username}"
          p:password="${jdbc.password}"
          p:maxTotal="10"
          p:minIdle="1"
          p:defaultAutoCommit="false">
        <property name="maxWait">
            <bean class="java.time.Duration" factory-method="ofMillis">
                <constructor-arg name="millis" type="long" value="5000" />
            </bean>
        </property>
    </bean>

batch-application.properties

    # (3)
    # Admin DataSource settings.
    admin.h2.jdbc.driver=org.h2.Driver
    admin.h2.jdbc.url=jdbc:h2:mem:batch;DB_CLOSE_DELAY=-1
    admin.h2.jdbc.username=sa
    admin.h2.jdbc.password=

    # (4)
    # Job DataSource settings.
    jdbc.driver=org.postgresql.Driver
    jdbc.url=jdbc:postgresql://localhost:5432/postgres
    jdbc.username=postgres
    jdbc.password=postgres

表 73. 説明
項番 | 説明
--- | ---
(1) | `adminDataSource`の定義。(3)の接続情報が設定される。
(2) | `jobDataSource`の定義。(4)の接続情報が設定される。
(3) | `adminDataSource`で利用するデータベースへの接続情報 この例では、H2を利用している。
(4) | `jobDataSource`で利用するデータベースへの接続情報 この例では、PostgreSQLを利用している。

###### 5.2.2.1.2. MyBatisの設定

Macchinetta Batch 2.xで、MyBatisの設定をする上で重要な点について説明をする。

バッチ処理を実装する際の重要なポイントの1つとして「大量のデータを一定のリソースで効率よく処理する」が挙げられる。
これに関する設定を説明する。

- `fetchSize`

  - 一般的なバッチ処理では、大量のデータを処理する際の通信コストを低減するために、 JDBCドライバに適切な`fetchSize`を指定することが必須である。 `fetchSize`とは、JDBCドライバとデータベース間とで1回の通信で取得するデータ件数を設定するパラメータである。 この値は出来る限り大きい値を設定することが望ましいが、大きすぎるとメモリを圧迫するため、注意が必要である。 ユーザにてチューニングする必要がある箇所と言える。

  - MyBatisでは、全クエリ共通の設定として`defaultFetchSize`を設定することができ、さらにクエリごとの`fetchSize`設定で上書きできる。

- `executorType`
  Macchinetta Batch 2.xでは、同時に2つの異なる`ExecutorType`が存在する。 一方の`ExecutorType`で実装する場合が多いと想定するが、併用時は特に注意が必要である。 この点は、Mapperインタフェース(入力)にて詳しく説明する。
  以下は`defaultExecutorType`に設定できる`executorType`の説明である。

  - `REUSE`

    - データベースからデータを取得するときに設定する。

    - 一般的なバッチ処理では、同一トランザクション内で同じSQLを複数回実行することになる。
      `REUSE`によって`PreparedStatement`が再利用され、処理スループット向上に寄与する。

    - `MyBatisCursorItemReader`で利用する`SqlSessionFactory`には、`executorType`に`REUSE`が設定されている。

  - `BATCH`

    - データベースへデータを更新するときに設定する。

    - `BATCH`によって`PreparedStatement`が再利用され、複数のSQLが一括で実行されるため、(特に大量のデータを一度に更新する会いに)性能向上が期待できる。

    - `MyBatisBatchItemWriter`で利用する`SqlSessionTemplate`には、`executorType`に`BATCH`が設定されている。

`ExecutorType`については MACCHINETTA Server ガイドライン にて詳しく説明されている。

| MyBatisのその他のパラメータ その他のパラメータに関しては以下リンクを参照し、 アプリケーションの特性にあった設定を行うこと。 https://mybatis.org/mybatis-3/configuration.html

以下にデフォルト提供されている設定を示す。

JavaConfig
jp.co.ntt.fw.macchinetta.batch.functionaltest.config.LaunchContextConfig.java

    // (1)
    @Bean
    public SqlSessionFactory jobSqlSessionFactory(@Qualifier("jobDataSource") DataSource jobDataSource) throws Exception {
        final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(jobDataSource);

        final org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setLocalCacheScope(LocalCacheScope.STATEMENT);
        configuration.setLazyLoadingEnabled(true);
        configuration.setAggressiveLazyLoading(false);
        configuration.setDefaultFetchSize(1000);
        configuration.setDefaultExecutorType(ExecutorType.REUSE);
         sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }

    // (2)
    @Bean
    public SqlSessionTemplate batchModeSqlSessionTemplate(@Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory) {
        return new SqlSessionTemplate(jobSqlSessionFactory, ExecutorType.BATCH);
    }

XMLConfig
META-INF/spring/launch-context.xml

    <bean id="jobSqlSessionFactory"
          class="org.mybatis.spring.SqlSessionFactoryBean"
          p:dataSource-ref="jobDataSource">
        <!-- (1) -->
        <property name="configuration">
            <bean class="org.apache.ibatis.session.Configuration"
                  p:localCacheScope="STATEMENT"
                  p:lazyLoadingEnabled="true"
                  p:aggressiveLazyLoading="false"
                  p:defaultFetchSize="1000"
                  p:defaultExecutorType="REUSE"/>
        </property>
    </bean>

    <!-- (2) -->
    <bean id="batchModeSqlSessionTemplate"
          class="org.mybatis.spring.SqlSessionTemplate"
          c:sqlSessionFactory-ref="jobSqlSessionFactory"
          c:executorType="BATCH"/>

表 74. 説明
項番 | 説明
--- | ---
(1) | MyBatisの各種設定を行う。 デフォルトでは、fetchSizeを1000に設定している。
(2) | `MyBatisBatchItemWriter`のために、`executorType`が`BATCH`の`SqlSessionTemplate`を定義している。

###### 5.2.2.1.3. Mapper XMLの定義

Macchinetta Batch 2.x特有の説明事項はないので、Macchinetta Server 1.x 開発ガイドラインの データベースアクセス処理の実装を参照。

###### 5.2.2.1.4. MyBatis-Springの設定

Macchinetta Batch 2.xではMyBatis-Springが提供する開発者が用意したMybatisのMapperインタフェースをSpringのDIコンテナにBeanとして登録し、SQL操作を簡単に呼び出せる機能を利用している。
MapperインタフェースのBeanの登録方法として`@Bean/<bean>`で個別でMapperインタフェースを登録する方法と、`@MapperScan/<mybatis:scan>`で開発者が指定したパッケージ内にあるMapperインタフェースを探して自動でBeanとして登録する方法がある。

`@MapperScan/<mybatis:scan>`は個別でMapperインタフェースをSpringのDIコンテナにBeanとして登録する方法と違い、 ジョブで利用されるMapperインタフェースが配置されているパッケージを指定することにより、対象のMapperインタフェースを自動的にDIコンテナへ登録することができる。

Mapperインタフェースを個別にBean定義として逐一記述する方式と比較して、`@MapperScan/<mybatis:scan>`で特定のジョブから利用されるMapperインタフェースが格納されているパッケージを指定することにより、パッケージ配下のMapperインタフェースとBean定義との整合性が保ちやすくなることから、 Macchinetta Server 1.x 開発ガイドラインでは、Mapperインタフェースに対しては`@MapperScan/<mybatis:scan>`を使用することを原則としている。

以下にMapperスキャンを有効化するための設定例を示す。

JavaConfig
jp.co.ntt.fw.macchinetta.batch.jobs.common.JobSalesPlan01Config.java

    @MapperScan(
        value = "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan", // (1)
        sqlSessionFactoryRef = "jobSqlSessionFactory") // (2)

XMLConfig
jobSalesPlan01.xml

    <!-- (1), (2) -->
    <mybatis:scan
        base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan"
        factory-ref="jobSqlSessionFactory"/>

表 75. 説明
項番 | 説明
--- | ---
(1) | `value`属性 / `base-package`属性にMapperインタフェースを配置しているJavaのパッケージ名(例:`jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan`)を指定する。 指定したパッケージ内にあるMapperインタフェースに加え、サブパッケージも含めて再帰的にMapperインタフェースがスキャンされる。
(2) | `sqlSessionFactoryRef`属性 / `factory-ref`属性に`SqlSessionFactory` のBean名である `jobSqlSessionFactory` を設定する。 `SqlSessionFactory` は `jobSqlSessionFactory` というBean名で `LaunchContextConfig.java/launch-context.xml` で定義されており、同定義内のジョブ用に構成されたデータソースである `jobDataSource` を参照している。 またこの他に同定義内ではフレームワーク管理用のデータソースである `adminDataSource`と、それを参照している`adminSqlSessionFactory`もあるが、これはジョブからの利用には不適であるため、Bean定義の変更時などに誤って参照しないよう注意すること。

Mapperのスキャン範囲を不必要に広くしないこと

`@MapperScan/<mybatis:scan>`のスキャン範囲を広くしてしまうと、実行するジョブで利用しないMapperインタフェースも読み込まれる恐れがある。これにより、以下に示す弊害が考えられる。

- ジョブの起動までに想定以上の時間がかかる恐れ

- メモリリソースの消費が大きくなる

Macchinetta Batch 2.xが提供するブランクプロジェクトではパッケージの構造として、まず、ジョブIDで表されるパッケージを作り、その中に当該ジョブで使用するMapperインタフェースを配置している。
これは、新しくジョブを追加した場合であっても、既にジョブごとにパッケージが分割されていることから、配下に格納されたMapperインタフェースも他のジョブのMapperインタフェースと混在することがないよう、パッケージの構造としてスキャン範囲を適切に保つための意図である。
そのため、個別のジョブ定義ファイルの`@MapperScan`の`value`属性 / `<mybatis:scan>`の`base-package`属性に当該ジョブで使用するMapperインタフェースを格納したパッケージ名を指定し、当該ジョブの実行に必要なMapperインタフェースだけがBeanとして登録されるようにスキャン範囲を最小限にすることを推奨している。

##### 5.2.2.2. 入力

データベースアクセスの入力について以下のとおり説明する。

1. MyBatisCursorItemReader

   1. 機能概要

   2. チャンクモデルにおける利用方法

   3. タスクレットモデルにおける利用方法

   4. 検索条件の指定方法

2. Mapperインタフェース(入力)

   1. 機能概要

   2. チャンクモデルにおける利用方法

   3. タスクレットモデルにおける利用方法

###### 5.2.2.2.1. MyBatisCursorItemReader

ここではItemReaderとして MyBatis-Springが提供する`MyBatisCursorItemReader`によるデータベースアクセスについて説明する。

機能概要

  MyBatis-Springが提供するItemReaderとして下記の2つが存在する。

  - `org.mybatis.spring.batch.MyBatisCursorItemReader`

  - `org.mybatis.spring.batch.MyBatisPagingItemReader`

`MyBatisPagingItemReader`は、Macchinetta Server 1.x 開発ガイドラインの Entityのページネーション検索(SQL絞り込み方式)で 説明している仕組みを利用したItemReaderである。
一定件数を取得した後に再度SQLを発行するため、データの一貫性が保たれない可能性がある。 そのため、バッチ処理で利用するには危険であることから、Macchinetta Batch 2.xでは原則使用しない。
Macchinetta Batch 2.xではMyBatisと連携して、Cursorを利用し取得データを返却する`MyBatisCursorItemReader`のみを利用する。

Macchinetta Batch 2.xでは、MyBatis-Springの設定で説明したとおり、 `mybatis:scan`によって動的にMapper XMLを登録する方法を採用している。 そのため、Mapper XMLに対応するインタフェースを用意する必要がある。 詳細については、Macchinetta Server 1.x 開発ガイドラインの データベースアクセス処理の実装を参照。

| MyBatisCursorItemReaderのトランザクションについて `MyBatisCursorItemReader`は`select`ステートメントを発行する際に、ステップ処理の一部として生成された他のトランザクション(チャンクモデルにおけるチャンクごとのトランザクション)には参加しない。 これは、`MyBatisCursorItemReader`が異なるコネクションを利用しているためである。そのため、適切に排他制御を行っていない場合、デッドロックが発生する可能性があるため留意すること。

`MyBatisCursorItemReader`を利用してデータベースを参照するための実装例を処理モデルごとに以下に示す。

チャンクモデルにおける利用方法

  チャンクモデルで`MyBatisCursorItemReader`を利用してデータベースを参照する実装例を以下に示す。
  ここでは、`MyBatisCursorItemReader`の実装例と、実装した`MyBatisCursorItemReader`を利用してデータベースから取得したデータを処理する`ItemProcessor`の実装例を説明する。

JavaConfig

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan(value = {"jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common",
        "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.transaction.component",
        "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.transaction.listener"}, scopedProxy = ScopedProxyMode.TARGET_CLASS)
    @MapperScan(basePackages = "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.transaction.repository.admin", sqlSessionFactoryRef = "adminSqlSessionFactory")
    @MapperScan(basePackages = "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.mst", sqlSessionFactoryRef = "jobSqlSessionFactory") // (1)
    public class OutputAllCustomerList01Config {

        // (2)
        @Bean
        @StepScope
        public MyBatisCursorItemReader<Customer> reader(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory) {
            return new MyBatisCursorItemReaderBuilder<Customer>()
                    .sqlSessionFactory(jobSqlSessionFactory) // (4)
                    .queryId(
                            "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.mst.CustomerRepository.findAll") // (3)
                    .build();
        }
        // omitted
        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           ItemReader<Customer> reader,
                           ItemWriter<Customer> writer,
                           RetrieveBranchFromContextItemProcessor processor,
                           BranchMasterReadStepListener listener) {
            return new StepBuilder("outputAllCustomerList01.step01",
                    jobRepository)
                    .<Customer, Customer> chunk(10, transactionManager)
                    .reader(reader) // (5)
                    .processor(processor)
                    .listener(listener)
                    .writer(writer)
                    .build();
        }

        @Bean
        public Job outputAllCustomerList01(JobRepository jobRepository,
                                                Step step01) {
            return new JobBuilder("outputAllCustomerList01",jobRepository)
                    .start(step01)
                    .build();
        }
    }

XMLConfig

    <!-- (1) -->
    <mybatis:scan
        base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.mst"
        factory-ref="jobSqlSessionFactory"/>

    <!-- (2) (3) (4) -->
    <bean id="reader" class="org.mybatis.spring.batch.MyBatisCursorItemReader"
          p:queryId="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.mst.CustomerRepository.findAll"
          p:sqlSessionFactory-ref="jobSqlSessionFactory"/>

    <batch:job id="outputAllCustomerList01" job-repository="jobRepository">
        <batch:step id="outputAllCustomerList01.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <!-- (5) -->
                <batch:chunk reader="reader"
                             processor="retrieveBranchFromContextItemProcessor"
                             writer="writer" commit-interval="10"/>
                <!-- omitted -->
            </batch:tasklet>
        </batch:step>
    </batch:job>

Mapper XML

    <!-- (6) -->
    <mapper namespace="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.mst.CustomerRepository">

        <!-- omitted -->

        <!-- (7) -->
        <select id="findAll" resultType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.mst.Customer">
            <![CDATA[
            SELECT
                customer_id AS customerId,
                customer_name AS customerName,
                customer_address AS customerAddress,
                customer_tel AS customerTel,
                charge_branch_id AS chargeBranchId,
                create_date AS createDate,
                update_date AS updateDate
            FROM
                customer_mst
            ORDER by
                charge_branch_id ASC, customer_id ASC
            ]]>
        </select>

        <!-- omitted -->
    </mapper>

Mapperインタフェース

    public interface CustomerRepository {
        // (8)
        Cursor<Customer> findAll();

        // omitted.
    }

ItemProcessor実装

    @Component
    @Scope("step")
    public class RetrieveBranchFromContextItemProcessor implements ItemProcessor<Customer, CustomerWithBranch> {
        // omitted.

        @Override
        public CustomerWithBranch process(Customer item) throws Exception { // (9)
            CustomerWithBranch newItem = new CustomerWithBranch(item);
            newItem.setBranch(branches.get(item.getChargeBranchId())); // (10)
            return newItem;
        }
    }

表 76. 説明
項番 | 説明
--- | ---
(1) | Mapper XMLの登録を行う。
(2) | `MyBatisCursorItemReader`を定義する。
(3) | `MyBatisCursorItemReaderBuilderのqueryIdメソッド/<bean>要素のqueryId属性`に、(7)で定義しているSQLのIDを(6)の`namespace` + `<メソッド名>`で指定する。
(4) | `MyBatisCursorItemReaderBuilderのsqlSessionFactoryメソッド/<bean>要素のsqlSessionFactory-ref属性`に、アクセスするデータベースの`SqlSessionFactory`を指定する。
(5) | (2)で定義した`MyBatisCursorItemReader`を`StepBuilderのreaderメソッド/<batch:chunk>要素のreader属性`に指定する。
(6) | Mapper XMLを定義する。namespaceの値とインタフェースのFQCNを一致させること。
(7) | SQLを定義する。
(8) | (7)で定義したSQLのIDに対応するメソッドをインタフェースに定義する。
(9) | 引数として受け取るitemの型は、 このクラスで実装している`ItemProcessor`インタフェースの型引数で指定した入力オブジェクトの型である`Customer`とする。
(10) | 引数に渡されたitemを利用して各カラムの値を取得する。

タスクレットモデルにおける利用方法

  タスクレットモデルで`MyBatisCursorItemReader`を利用してデータベースを参照する実装例を以下に示す。
  ここでは、`MyBatisCursorItemReader`の実装例と、実装した`MyBatisCursorItemReader`を利用してデータベースから取得したデータを処理する`Tasklet`の実装例を説明する。

タスクレットモデルでチャンクモデルのコンポーネントを利用する際の留意点についてはチャンクモデルのコンポーネントを利用するTasklet実装を参照。

タスクレットモデルではチャンクモデルと異なり、`Tasklet`実装においてリソースを明示的にオープン/クローズする必要がある。 また、入力データの読み込みも明示的に行う。

JavaConfig

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan(value = {"jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common",
        "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch07.jobmanagement"}, scopedProxy = ScopedProxyMode.TARGET_CLASS)
    @MapperScan(basePackages = "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan", sqlSessionFactoryRef = "jobSqlSessionFactory") // (1)
    public class CustomizedJobExitCodeTaskletJobConfig {

        // (2)
        @Bean
        public MyBatisCursorItemReader<SalesPlanSummary> summarizeDetails(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory) {
            return new MyBatisCursorItemReaderBuilder<SalesPlanSummary>()
                    .sqlSessionFactory(jobSqlSessionFactory) // (4)
                    .queryId(
                            "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan.SalesPlanDetailRepository.summarizeDetails") // (3)
                    .build();
        }
        // omitted
        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           CheckAmountTasklet tasklet) {
            return new StepBuilder("customizedJobExitCodeTaskletJob.step01",
                    jobRepository)
                    .tasklet(tasklet, transactionManager)
                    .build();
        }

        @Bean
        public Job customizedJobExitCodeTaskletJob(JobRepository jobRepository,
                                                     Step step01,
                                                     JobExitCodeChangeListener jobExitCodeChangeListener) {
            return new JobBuilder("customizedJobExitCodeTaskletJob",
                    jobRepository)
                    .start(step01)
                    .listener(jobExitCodeChangeListener)
                    .build();
        }
        // omitted
    }

XMLConfig

    <!-- (1) -->
    <mybatis:scan
        base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan"
        factory-ref="jobSqlSessionFactory"/>

    <!-- (2) (3) (4) -->
    <bean id="summarizeDetails" class="org.mybatis.spring.batch.MyBatisCursorItemReader"
              p:queryId="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan.SalesPlanDetailRepository.summarizeDetails"
              p:sqlSessionFactory-ref="jobSqlSessionFactory"/>

    <batch:job id="customizedJobExitCodeTaskletJob" job-repository="jobRepository">
        <batch:step id="customizedJobExitCodeTaskletJob.step01">
            <batch:tasklet transaction-manager="jobTransactionManager" ref="checkAmountTasklet"/>
        </batch:step>
        <!-- omitted -->
    </batch:job>

Mapper XML

    <!-- (5) -->
    <mapper namespace="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan.SalesPlanDetailRepository">

        <!-- omitted -->

        <!-- (6) -->
        <select id="summarizeDetails" resultType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.plan.SalesPlanSummary">
            <![CDATA[
            SELECT
                branch_id AS branchId, year, month, SUM(amount) AS amount
            FROM
                sales_plan_detail
            GROUP BY
                branch_id, year, month
            ORDER BY
                branch_id ASC, year ASC, month ASC
             ]]>
        </select>

    </mapper>

Mapperインタフェース

    public interface SalesPlanDetailRepository {
        // (7)
        Cursor<SalesPlanSummary> summarizeDetails();

        // omitted.
    }

Tasklet実装

    @Component
    @Scope("step")
    public class CheckAmountTasklet implements Tasklet {
        // (8)
        @Inject
        ItemStreamReader<SalesPlanSummary> reader;

        @Override
        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
            SalesPlanSummary item = null;

            List<SalesPlanSummary> items = new ArrayList<>(CHUNK_SIZE);
            int errorCount = 0;

            try {
                // (9)
                reader.open(chunkContext.getStepContext().getStepExecution().getExecutionContext());
                while ((item = reader.read()) != null) { // (10)
                    if (item.getAmount().signum() == -1) {
                        logger.warn("amount is negative. skip item [item: {}]", item);
                        errorCount++;
                        continue;
                    }

                    // omitted.
                }

                // catch block is omitted.
            } finally {
                // (11)
                reader.close();
            }
        }
        // omitted.

        return RepeatStatus.FINISHED;
    }

表 77. 説明
項番 | 説明
--- | ---
(1) | Mapper XMLの登録を行う。
(2) | `MyBatisCursorItemReader`を定義する。
(3) | `MyBatisCursorItemReaderBuilderのqueryIdメソッド/<bean>要素のqueryId属性`に、(7)で定義しているSQLのIDを(6)の`namespace` + `<メソッド名>`で指定する。
(4) | `MyBatisCursorItemReaderBuilderのsqlSessionFactoryメソッド/<bean>要素のsqlSessionFactory-ref属性`に、アクセスするデータベースの`SqlSessionFactory`を指定する。
(5) | Mapper XMLを定義する。namespaceの値とインタフェースのFQCNを一致させること。
(6) | SQLを定義する。
(7) | (6)で定義したSQLのIDに対応するメソッドをインタフェースに定義する。
(8) | `@Inject`アノテーションを付与して、`ItemStreamReader`の実装をインジェクションする。 対象となるリソースへのオープン・クローズを実施する必要があるため、 `ItemReader`にリソースのopen/closeメソッドを追加した`ItemStreamReader`インタフェースで実装をインジェクションする。
(9) | 入力リソースをオープンする。
(10) | 入力データを1件ずつ読み込む。
(11) | 入力リソースをクローズする。 リソースは必ずクローズすること。なお、ここで例外が発生した場合、タスクレット全体のトランザクションがロールバックされ、 例外のスタックトレースを出力しジョブが異常終了する。そのため、必要に応じて例外処理を実装すること。

検索条件の指定方法

  データベースアクセスの際に検索条件を指定して検索を行いたい場合は、 Bean定義にてMap形式でジョブパラメータから値を取得し、キーを設定することで検索条件を指定することができる。 以下にジョブパラメータを指定したジョブ起動コマンドの例と実装例を示す。

ジョブパラメータを指定した場合のジョブ起動コマンド

    $ java -cp ${CLASSPATH} org.springframework.batch.core.launch.support.CommandLineJobRunner /META-INF/jobs/job001.xml job001 year=2017 month=12

MapperXMLの実装例

    <!-- (1) -->
    <select id="findByYearAndMonth"
        resultType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.performance.SalesPerformanceSummary">
        <![CDATA[
        SELECT
            branch_id AS branchId, year, month, amount
        FROM
            sales_performance_summary
        WHERE
            year = #{year} AND month = #{month}
        ORDER BY
            branch_id ASC
        ]]>
    </select>

    <!-- omitted -->

JavaConfig

    // omitted
    // (2)
    @Bean
    @StepScope
    public MyBatisCursorItemReader<SalesPerformanceSummary> reader(
            @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
            @Value("#{jobParameters['year']}") Integer year,
            @Value("#{jobParameters['month']}") Integer month,
            @Value("#{stepExecutionContext['dataSize']}") Integer dataSize,
            @Value("#{stepExecutionContext['offset']}") Integer offset) {
        Map<String, Object> parameterValues = new HashMap<>();
        parameterValues.put("year", year); // (4)
        parameterValues.put("month", month); // (4)
        // omitted
        return new MyBatisCursorItemReaderBuilder<SalesPerformanceSummary>()
                .sqlSessionFactory(jobSqlSessionFactory)
                .queryId(
                        "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch08.parallelandmultiple.repository.SalesSummaryRepository.findByYearAndMonth")
                .parameterValues(parameterValues) // (3)
                .build();
    }
    // omited

XMLConfig

    <!-- omitted -->

    <!-- (2) -->
    <bean id="reader"
          class="org.mybatis.spring.batch.MyBatisCursorItemReader" scope="step"
          p:queryId="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch08.parallelandmultiple.repository.SalesSummaryRepository.findByYearAndMonth"
          p:sqlSessionFactory-ref="jobSqlSessionFactory">
        <property name="parameterValues"> <!-- (3) -->
            <map>
                <!-- (4) -->
                <entry key="year" value="#{jobParameters['year']}" value-type="java.lang.Integer"/>
                <entry key="month" value="#{jobParameters['month']}" value-type="java.lang.Integer"/>

                <!-- omitted -->
            </map>
        </property>
    </bean>

    <!-- omitted -->

表 78. 説明
項番 | 説明
--- | ---
(1) | 検索条件を指定して取得するSQLを定義する。
(2) | データベースからデータを取得するための`ItemReader`を定義する。
(3) | `MyBatisCursorItemReaderBuilderのparameterValuesメソッド/<property>要素のname属性にparameterValues`を設定する。
(4) | 検索条件にする値をジョブパラメータから取得し、キーに設定することで検索条件を指定する。 SQLの引数が数値型で定義されているため、`Integer`に変換して渡している。

| StepExectionContextによる検索指定方法について @BeforeStepなどジョブの前処理で検索条件を指定する場合は、`StepExecutionContext`に設定することで、`JobParameters`同様に取得することができる。

###### 5.2.2.2.2. Mapperインタフェース(入力)

ItemReader以外でデータベースの参照を行うには`Mapper`インタフェースを利用する。
ここでは`Mapper`インタフェースを利用したデータベースの参照について説明する。

機能概要

  `Mapper`インタフェースを利用するにあたって、Macchinetta Batch 2.xでは以下の制約を設けている。

表 79. Mapperインタフェースの利用可能な箇所
処理 | ItemProcessor | Tasklet | リスナー
--- | --- | --- | ---
参照 | 利用可 | 利用可 | 利用可
更新 | 条件付で利用可 | 利用可 | 利用不可

チャンクモデルにおける利用方法

  チャンクモデルで`Mapper`インタフェースを利用してデータベースを参照する実装例を以下に示す。

ItemProcessorでの実装例

    @Component
    public class UpdateItemFromDBProcessor implements
            ItemProcessor<SalesPerformanceDetail, SalesPlanDetail> {

        // (1)
        @Inject
        CustomerRepository customerRepository;

        @Override
        public SalesPlanDetail process(SalesPerformanceDetail readItem) throws Exception {

            // (2)
            Customer customer = customerRepository.findOne(readItem.getCustomerId());

            // omitted.

            return writeItem;
        }
    }

JavaConfig

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan({ "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common",
            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess" })
    @MapperScan(basePackages = {
            "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository",
            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.repository" }, sqlSessionTemplateRef = "batchModeSqlSessionTemplate") // (3)
    public class DBAccessByItemProcessorConfig {

        // (4)
        @Bean
        public MyBatisCursorItemReader<SalesPerformanceDetail> reader(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory) {
            return new MyBatisCursorItemReaderBuilder<SalesPerformanceDetail>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .queryId(
                            "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.performance.SalesPerformanceDetailRepository.findAll")
                    .build();
        }
        // omitted

XMLConfig

    <!-- (3) -->
    <mybatis:scan
    base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository"
    template-ref="batchModeSqlSessionTemplate"/>

    <!-- (4) -->
    <bean id="reader" class="org.mybatis.spring.batch.MyBatisCursorItemReader"
    p:queryId="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.performance.SalesPerformanceDetailRepository.findAll"
    p:sqlSessionFactory-ref="jobSqlSessionFactory"/>

    <!-- omitted job definition -->

`Mapper`インタフェースとMapper XMLについてはMyBatisCursorItemReader で説明している内容以外に特筆すべきことがないため省略する。

表 80. 説明
項番 | 説明
(1) | `Mapper`インタフェースをInjectする。
(2) | `Mapper`インタフェースで検索処理を実行する。
(3) | Mapper XMLの登録を行う。 `@MapperScanのsqlSessionTemplateRef属性/<mybatis:scan>要素のtemplate-ref属性`に`BATCH`設定されている`batchModeSqlSessionTemplate`を指定することで、 ItemProcessorでのデータベースアクセスは`BATCH`となる。
(4) | `MyBatisCursorItemReader`を定義する。 `MyBatisCursorItemReaderBuilderのsqlSessionFactoryメソッド/<bean>要素のsqlSessionFactory-ref属性`に、アクセスするデータベースの`SqlSessionFactory`を指定する。

タスクレットモデルにおける利用方法

  タスクレットモデルで`Mapper`インタフェースを利用してデータベースを参照する実装例を以下に示す。
  大量データを処理する場合、Cursorを利用して取得データを1件ずつ処理することでメモリ容量をひっ迫せずに効率良く処理することができる。 Macchinetta Batch 2.xでは、タスクレットモデルで`Mapper`インタフェースを利用してデータベースアクセスする場合はCursorを利用することを基本とする。
  Cursor同様に大量データを処理するうえで、ResultHandlerを利用することも有効である。 ResultHandlerについては、Macchinetta Server 1.x 開発ガイドラインの ResultHandlerの実装を参照。

JavaConfig

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan({ "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common",
            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess" })
    @MapperScan(basePackages = {
            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.repository",
            "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository" }, sqlSessionFactoryRef = "jobSqlSessionFactory") // (1)
    public class JobSalesPlanCursorTaskletConfig {

        @Bean
        public Step step01(JobRepository jobRepository,
                           SalesPlanCursorTasklet tasklet,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
            return new StepBuilder("jobSalesPlanCursorTasklet.step01",
                    jobRepository)
                    .tasklet(tasklet, transactionManager)
                    .build();
        }

        @Bean
        public Job jobSalesPlanCursorTasklet(JobRepository jobRepository,
                                             Step step01) {
            return new JobBuilder("jobSalesPlanCursorTasklet", jobRepository)
                    .start(step01)
                    .build();
        }
    }

XMLConfig

    <!-- (1) -->
    <mybatis:scan
            base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.repository;jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository"
            factory-ref="jobSqlSessionFactory"/>

        <batch:job id="jobSalesPlanCursorTasklet" job-repository="jobRepository">
            <batch:step id="jobSalesPlanCursorTasklet.step01">
                <batch:tasklet transaction-manager="jobTransactionManager" ref="salesPlanCursorTasklet"/>
            </batch:step>
        </batch:job>

Mapper XML

    <!-- (2) -->
    <mapper namespace="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.repository.SalesRepository">

        <!-- omitted -->

        <!-- (3) -->
        <select id="findAll" resultType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.plan.SalesPlanDetail">
            <![CDATA[
            SELECT
                branch_id AS branchId, year, month, customer_id AS customerId, amount
            FROM
                sales_plan_detail
            ORDER BY
                branch_id ASC, year ASC, month ASC, customer_id ASC
            ]]>
        </select>

    </mapper>

Mapperインタフェース

    public interface SalesRepository {
        // (4)
        Cursor<SalesPlanDetail> findAll();
    }

Taskletでの実装例

    @Component
    public class SalesPlanCursorTasklet implements Tasklet {

        // omitted.

        // (5)
        @Inject
        SalesRepository repository;

        @Override
        public RepeatStatus execute(StepContribution contribution,
                ChunkContext chunkContext) throws Exception {

            try (Cursor<SalesPlanDetail> cursor = repository.findAll()) {  // (6)
                for (SalesPlanDetail salesPlan : cursor) { // (7)
                    // omitted.
                }
            }

            return RepeatStatus.FINISHED;
        }
    }

表 81. 説明
項番 | 説明
--- | ---
(1) | Mapper XMLの登録を行う。
(2) | Mapper XMLを定義する。namespaceの値とインタフェースのFQCNを一致させること。
(3) | SQLを定義する。
(4) | (3)で定義したSQLのIDに対応するメソッドをインタフェースに定義する。 メソッドの戻り値の型は`org.apache.ibatis.cursor.Cursor`とする。
(5) | `Mapper`インタフェースをInjectする。
(6) | Cursorから1件ずつデータを取得する。 処理の途中でエラーが発生した場合、MyBatisはCursorのクローズを保証しないため、try-with-resources文によりCursorを確実にクローズする。
(7) | 拡張for文によりCursorをフェッチする。

| Cursor利用時の注意点 Cursorで読み取り中のテーブルを他の処理で更新をかけると、 読み取り済みの古いデータを処理してしまうことになり、データの不整合が発生する可能性があるため注意すること。 これを防ぐために安易にロックをかけるとデッドロックを引き起こしかねないため、 排他制御を参照し適切にロックをかけるか、 テーブルアクセスが競合しないようにジョブ設計することを検討してほしい。

##### 5.2.2.3. 出力

データベースへの出力は、`MyBatisBatchItemWriter`もしくは`Mapper`インタフェースを利用することで実現できる。
`MyBatisBatchItemWriter`と`Mapper`インタフェースによる出力を併用した場合、`MyBatisBatchItemWriter`でエラーが発生する。 よって、`Mapper`インタフェースを用いた出力はチャンクモデルでの利用に適さないため、利用方法を記載していない。
詳細は`Mapper`インタフェース(出力)の機能概要で後述するため、そちらを参照されたい。

上記の内容について以下のとおり説明する。

1. MyBatisBatchItemWriter

   1. 機能概要

   2. チャンクモデルにおける利用方法

   3. タスクレットモデルにおける利用方法

2. Mapperインタフェース(出力)

   1. 機能概要

   2. タスクレットモデルにおける利用方法

###### 5.2.2.3.1. MyBatisBatchItemWriter

ここではItemWriterとして MyBatis-Springが提供する`MyBatisBatchItemWriter`によるデータベースアクセスについて説明する。

機能概要

  MyBatis-Springが提供するItemWriterは以下の1つのみである。

  - `org.mybatis.spring.batch.MyBatisBatchItemWriter`

`MyBatisBatchItemWriter`はMyBatisと連携してJDBCのバッチ更新機能を利用するItemWriterであり、 大量のデータを一度に更新する場合に性能向上が期待できる。
基本的な設定については、MyBatisCursorItemReaderと同じである。 `MyBatisBatchItemWriter`では、MyBatisの設定で説明した `batchModeSqlSessionTemplate`を指定する必要がある。

`MyBatisBatchItemWriter`を利用してデータベースに登録する実装例を以下に示す。

チャンクモデルにおける利用方法

  チャンクモデルで`MyBatisBatchItemWriter`を利用してデータベースに登録する実装例を以下に示す。

JavaConfig

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan("jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common")
    @MapperScan(basePackages = "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.performance",
                                sqlSessionFactoryRef = "jobSqlSessionFactory") // (1)
    public class DBAccessByMaybatisScanConfig {
        // omitted
        // (2)
        @Bean
        public MyBatisBatchItemWriter<SalesPerformanceSummary> writer(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
                SqlSessionTemplate batchModeSqlSessionTemplate) {
            return new MyBatisBatchItemWriterBuilder<SalesPerformanceSummary>()
                    .sqlSessionFactory(jobSqlSessionFactory) // (4)
                    .statementId(
                            "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.performance.SalesPerformanceSummaryRepository.create") // (3)
                    .sqlSessionTemplate(batchModeSqlSessionTemplate)
                    .build();
        }

        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           ItemReader<SalesPerformanceSummary> reader,
                           ItemWriter<SalesPerformanceSummary> writer) {
            return new StepBuilder("DBAccessByMaybatisScan.step01",
                    jobRepository)
                    .<SalesPerformanceSummary, SalesPerformanceSummary> chunk(10, transactionManager)
                    .reader(reader)
                    .writer(writer) // (5)
                    .build();
        }

        @Bean
        public Job DBAccessByMaybatisScan(JobRepository jobRepository,
                                                Step step01,
                                                JobExecutionLoggingListener listener) {
            return new JobBuilder("DBAccessByMaybatisScan",
                    jobRepository).start(step01).listener(listener).build();
        }
    }

XMLConfig

    <!-- (1) -->
    <mybatis:scan base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.performance"
                      factory-ref="jobSqlSessionFactory"/>

    <!-- omitted -->

    <!-- (2) (3) (4) -->
    <bean id="writer" class="org.mybatis.spring.batch.MyBatisBatchItemWriter"
          p:statementId="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.performance.SalesPerformanceSummaryRepository.create"
          p:sqlSessionTemplate-ref="batchModeSqlSessionTemplate"/>

    <batch:job id="DBAccessByMaybatisScan" job-repository="jobRepository">
        <batch:step id="DBAccessByMaybatisScan.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="reader"
                             writer="writer" commit-interval="10"/> <!-- (5) -->
            </batch:tasklet>
        </batch:step>
        <!-- omitted -->
    </batch:job>

Mapper XML

    <!-- (6) -->
    <mapper namespace="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.performance.SalesPerformanceSummaryRepository">

        <!-- (7) -->
        <insert id="create" parameterType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.performance.SalesPerformanceSummary">
            <![CDATA[
            INSERT INTO
                sales_performance_summary(branch_id, year, month, amount)
            VALUES (
                #{branchId}, #{year}, #{month}, #{amount}
            )
            ]]>
        </insert>

        <!-- omitted -->
    </mapper>

Mapperインタフェース

    public interface SalesPerformanceSummaryRepository {

        // (8)
        void create(SalesPerformanceSummary salesPerformanceSummary);

        // omitted.
    }

表 82. 説明
項番 | 説明
--- | ---
(1) | Mapper XMLの登録を行う。
(2) | `MyBatisBatchItemWriter`を定義する。
(3) | `MyBatisBatchItemWriterBuilderのstatementIdメソッド/<bean>要素のstatementId属性`に、(7)で定義しているSQLのIDを(6)の`namespace` + `<メソッド名>`で指定する。
(4) | `MyBatisBatchItemWriterBuilderのsqlSessionFactoryメソッド/<bean>要素のsqlSessionTemplate-ref属性`に、アクセスするデータベースの`SessionTemplate`を指定する。 指定する`SessionTemplate`は、`executorType`が`BATCH`に設定されていることが必須である。
(5) | (2)で定義した`MyBatisBatchItemWriter`を`StepBuilderのwriterメソッド/<batch:chunk>要素のwriter属性`に指定する。
(6) | Mapper XMLを定義する。namespaceの値とインタフェースのFQCNを一致させること。
(7) | SQLを定義する。
(8) | (7)で定義したSQLのIDに対応するメソッドをインタフェースに定義する。

タスクレットモデルにおける利用方法

  タスクレットモデルで`MyBatisBatchItemWriter`を利用してデータベースに登録する実装例を以下に示す。
  ここでは、`MyBatisBatchItemWriter`の実装例と実装した`MyBatisBatchItemWriter`を利用する`Tasklet`の実装例を説明する。 タスクレットモデルでチャンクモデルのコンポーネントを利用する際の留意点についてはチャンクモデルのコンポーネントを利用するTasklet実装を参照。

JavaConfig

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan(value = {"jp.co.ntt.fw.macchinetta.batch.functionaltest.app.plan",
        "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch04.listener"}, scopedProxy = ScopedProxyMode.TARGET_CLASS)
    @MapperScan(basePackages = "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan", sqlSessionFactoryRef = "jobSqlSessionFactory") // (1)
    public class TaskletJobWithListenerConfig {
        // omitted

        // (2)
        @Bean
        public MyBatisBatchItemWriter<SalesPlanDetail> writer(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
                SqlSessionTemplate batchModeSqlSessionTemplate) {
            return new MyBatisBatchItemWriterBuilder<SalesPlanDetail>()
                    .sqlSessionFactory(jobSqlSessionFactory) // (4)
                    .statementId(
                            "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan.SalesPlanDetailRepository.create") // (3)
                    .sqlSessionTemplate(batchModeSqlSessionTemplate)
                    .build();
        }

        @Bean
        public Step jobScopeStep01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           SalesPlanDetailRegisterTasklet salesPlanDetailRegisterTasklet) {
            return new StepBuilder("taskletJobWithListenerWithinJobScope.step01",
                    jobRepository)
                    .tasklet(salesPlanDetailRegisterTasklet, transactionManager)
                    .build();
        }

        @Bean
        public Job taskletJobWithListenerWithinJobScope(JobRepository jobRepository,
                                                Step jobScopeStep01,
                                                @Qualifier("allProcessListener") JobExecutionListener listener) {
            return new JobBuilder("taskletJobWithListenerWithinJobScope",
                    jobRepository)
                    .start(jobScopeStep01)
                    .listener(listener)
                    .build();
        }

        // omitted
    }

XMLConfig

    <!-- (1) -->
    <mybatis:scan base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan"
                factory-ref="jobSqlSessionFactory"/>

    <!-- (2) (3) (4) -->
    <bean id="writer" class="org.mybatis.spring.batch.MyBatisBatchItemWriter"
              p:statementId="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan.SalesPlanDetailRepository.create"
              p:sqlSessionTemplate-ref="batchModeSqlSessionTemplate"/>

    <batch:job id="taskletJobWithListenerWithinJobScope" job-repository="jobRepository">
        <batch:step id="taskletJobWithListenerWithinJobScope.step01">
            <batch:tasklet transaction-manager="jobTransactionManager" ref="salesPlanDetailRegisterTasklet"/>
        </batch:step>
        <!-- omitted. -->
    </batch:job>

Mapper XML

    <!-- (5) -->
    <mapper namespace="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.plan.SalesPlanDetailRepository">

        <!-- (6) -->
        <insert id="create" parameterType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.plan.SalesPlanDetail">
            <![CDATA[
            INSERT INTO
                sales_plan_detail(branch_id, year, month, customer_id, amount)
            VALUES (
                #{branchId}, #{year}, #{month}, #{customerId}, #{amount}
            )
            ]]>
        </insert>

        <!-- omitted -->
    </mapper>

Mapperインタフェース

    public interface SalesPlanDetailRepository {
        // (7)
        void create(SalesPlanDetail salesPlanDetail);

        // omitted.
    }

Tasklet実装

    @Component
    @Scope("step")
    public class SalesPlanDetailRegisterTasklet implements Tasklet {

        // omitted.

        // (8)
        @Inject
        ItemWriter<SalesPlanDetail> writer;

        @Override
        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
            SalesPlanDetail item = null;

            try {
                reader.open(chunkContext.getStepContext().getStepExecution().getExecutionContext());

                List<SalesPlanDetail> items = new ArrayList<>(); // (9)

                while ((item = reader.read()) != null) {

                    items.add(processor.process(item)); // (10)
                    if (items.size() == 10) {
                        writer.write(new Chunk(items)); // (11)
                        items.clear();
                    }
                }
                // omitted.
            }
            // omitted.

            return RepeatStatus.FINISHED;
        }
    }

`Mapper`インタフェースとMapper XMLについてはMyBatisBatchItemWriter で説明している内容以外に特筆すべきことがないため省略する。

表 83. 説明
項番 | 説明
--- | ---
(1) | Mapper XMLの登録を行う。
(2) | `MyBatisBatchItemWriter`を定義する。
(3) | `MyBatisBatchItemWriterBuilderのstatementIdメソッド/<bean>要素のstatementId属性`に、(7)で定義しているSQLのIDを(6)の`namespace` + `<メソッド名>`で指定する。
(4) | `MyBatisBatchItemWriterBuilderのsqlSessionFactoryメソッド/<bean>要素のsqlSessionTemplate-ref属性`に、アクセスするデータベースの`SessionTemplate`を指定する。 指定する`SessionTemplate`は、`executorType`が`BATCH`に設定されていることが必須である。
(5) | Mapper XMLを定義する。namespaceの値とインタフェースのFQCNを一致させること。
(6) | SQLを定義する。
(7) | (6)で定義したSQLのIDに対応するメソッドをインタフェースに定義する。
(8) | `@Inject`アノテーションを付与して、`ItemWriter`の実装をインジェクションする。 `ItemReader`とは異なり、データベースの更新ではリソースのopen/closeが不要であるため、`ItemStreamWriter`ではなく`ItemWriter`インタフェースにインジェクションする。
(9) | 出力データを格納するリストを定義する。 `ItemWriter`では一定件数のデータをまとめて出力する。
(10) | リストに更新データを設定する。
(11) | 更新データを設定したリストを引数に指定して、データベースへ出力する。

###### 5.2.2.3.2. Mapperインタフェース(出力)

ItemWriter以外でデータベースの更新を行うには`Mapper`インタフェースを利用する。
ここでは`Mapper`インタフェースを利用したデータベースの更新について説明する。

機能概要

  `Mapper`インタフェースを利用してデータベースアクセスするうえでのMacchinetta Batch 2.xとしての制約はMapperインタフェース(入力)を参照。
  さらにItemProcessor、Taskletで`Mapper`インタフェースを利用したデータベースの更新を行う際には以下の制約がある。

ItemProcessorでの制約

  ItemWriterに`MyBatisBatchItemWriter`を利用する場合、ItemProcessorでは`Mapper`インタフェースを利用してデータベースの更新処理を行うことができないという制約がある。
  これは、`MyBatisBatchItemWriter`がSQL実行後に自身が発行したSQLによる更新処理が行われたかをチェックしており、 その際に同一トランザクション内での他の更新処理を検知するとエラーを発生させることによるものである。
  よって、ItemProcessorで`Mapper`インタフェースを利用したデータベースの更新はできず、参照のみが可能となる。
  ItemProcessor内で更新処理を行いたいケースとして、「特定の条件に当てはまる入力データについて、 ItemWriterとは異なる更新処理をするためにItemProcessorで個別に更新処理を行う」などが考えられるが、上記の制約によりこのような利用はできない。
  代替手段として、後述のタスクレットモデルによる実装、もしくはItemProcessor、ItemWriterでの更新をステップやジョブで分離したバッチ処理の設計を検討されたい。

| `MyBatisBatchItemWriter`のエラーチェックを無効化する設定ができるが、予期せぬ動作が起きる可能性があるため無効化は禁止する。

Taskletでの制約

  Taskletでは、`Mapper`インタフェースを利用することが基本であるため、ItemProcessorのような影響はない。
  `MyBatisBatchItemWriter`をInjectして利用することも考えられるが、その場合は`Mapper`インタフェース自体を `BATCH`設定で処理すればよい。つまり、Taskletでは、`MyBatisBatchItemWriter`をInjectして使う必要は基本的にない。

タスクレットモデルにおける利用方法

  タスクレットモデルで`Mapper`インタフェースを利用してデータベースを更新(登録)する実装例を以下に示す。

Taskletでの実装例

    @Component
    public class OptimisticLockTasklet implements Tasklet {

        // (1)
        @Inject
        ExclusiveControlRepository repository;

        // omitted.

        @Override
        public RepeatStatus execute(StepContribution contribution,
                ChunkContext chunkContext) throws Exception {

            Branch branch = repository.branchFindOne(branchId);

            // (2)
            ExclusiveBranch exclusiveBranch = new ExclusiveBranch();
            exclusiveBranch.setBranchId(branch.getBranchId());
            exclusiveBranch.setBranchName(branch.getBranchName() + " - " + identifier);
            exclusiveBranch.setBranchAddress(branch.getBranchAddress() + " - " + identifier);
            exclusiveBranch.setBranchTel(branch.getBranchTel());
            exclusiveBranch.setCreateDate(branch.getUpdateDate());
            exclusiveBranch.setUpdateDate(LocalDateTime.now(clock));
            exclusiveBranch.setOldBranchName(branch.getBranchName());

            // (3)
            int result = repository.branchExclusiveUpdate(exclusiveBranch);

            // omitted.

            return RepeatStatus.FINISHED;
        }
    }

JavaConfig

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan(value = { "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common",
            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol"}, scopedProxy = ScopedProxyMode.TARGET_CLASS)
    @MapperScan(basePackages = "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol.repository", sqlSessionFactoryRef = "jobSqlSessionFactory") // (4)
    public class TaskletOptimisticLockCheckJobConfig {

        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           OptimisticLockTasklet optimisticLockTasklet) {
            return new StepBuilder("taskletOptimisticLockCheckJob.step01",
                    jobRepository)
                    .tasklet(optimisticLockTasklet, transactionManager) // (5)
                    .build();
        }

        @Bean
        public Job taskletOptimisticLockCheckJob(JobRepository jobRepository,
                                                Step step01,
                                                JobExecutionLoggingListener listener) {
            return new JobBuilder("taskletOptimisticLockCheckJob",jobRepository)
                    .start(step01)
                    .listener(listener)
                    .build();
        }
    }

XMLConfig

    <!-- (4) -->
    <mybatis:scan
            base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol.repository"
            factory-ref="jobSqlSessionFactory"/>

    <batch:job id="taskletOptimisticLockCheckJob" job-repository="jobRepository">
        <batch:step id="taskletOptimisticLockCheckJob.step01">
            <batch:tasklet transaction-manager="jobTransactionManager"
                           ref="optimisticLockTasklet"> <!-- (5) -->
            </batch:tasklet>
        </batch:step>
    </batch:job>

`Mapper`インタフェースとMapper XMLは省略する。

表 84. 説明
項番 | 説明
--- | ---
(1) | `Mapper`インタフェースをInjectする。
(2) | DTOオブジェクトを生成して、更新データを設定する。
(3) | 更新データを設定したDTOオブジェクトを引数に指定して、`Mapper`インタフェースで更新処理を実行する。
(4) | Mapper XMLの登録を行う。 `@MapperScanのsqlSessionFactoryRef属性/<mybatis:scan>要素のfactory-ref属性`に`REUSE`設定されている`jobSqlSessionFactory`を指定する。
(5) | `Mapper`インタフェースをInjectしTaskletを設定する。

##### 5.2.2.4. リスナーでのデータベースアクセス

リスナーでのデータベースアクセスは他のコンポーネントと連携することが多い。 使用するリスナー及び実装方法によっては、`Mapper`インタフェースで取得したデータを、 他のコンポーネントへ引き渡す仕組みを追加で用意する必要がある。

リスナーで`Mapper`インタフェースを利用してデータベースアクセスを実装するにあたり、以下の制約がある。

リスナーでの制約

  リスナーでもItemProcessorでの制約と同じ制約が成立する。 加えて、リスナーでは、更新を必要とするユースケースが考えにくい。よって、リスナーでは、更新系処理を行うことを推奨しない。

| リスナーで想定される更新処理の代替 ジョブの状態管理 Spring BatchのJobRepositoryによって行われている データベースへのログ出力 ログのAppenderで実施すべき。ジョブのトランザクションとも別管理する必要がある。

ここでは一例として、StepExecutionListenerで ステップ実行前にデータを取得して、ItemProcessorで取得したデータを利用する例を示す。

リスナーでの実装例

    public class CacheSetListener implements StepExecutionListener {

        // (1)
        @Inject
        CustomerRepository customerRepository;

        // (2)
        @Inject
        CustomerCache cache;

        @Override
        public void beforeStep(StepExecution stepExecution) {
            // (3)
            for(Customer customer : customerRepository.findAllAtOnce()) {
                cache.addCustomer(customer.getCustomerId(), customer);
            }
        }
    }

ItemProcessorでの利用例

    @Component
    public class UpdateItemFromCacheProcessor implements
            ItemProcessor<SalesPerformanceDetail, SalesPlanDetail> {

        // (4)
        @Inject
        CustomerCache cache;

        @Override
        public SalesPlanDetail process(SalesPerformanceDetail readItem) throws Exception {
            Customer customer = cache.getCustomer(readItem.getCustomerId());  // (5)

            SalesPlanDetail writeItem = new SalesPlanDetail();

            // omitted.
            writerItem.setCustomerName(customer.getCustomerName); // (6)

            return writeItem;
        }
    }

キャッシュクラス

    // (7)
    @Component
    public class CustomerCache {

        Map<String, Customer> customerMap = new HashMap<>();

        public Customer getCustomer(String customerId) {
            return customerMap.get(customerId);
        }

        public void addCustomer(String id, Customer customer) {
            customerMap.put(id, customer);
        }
    }

JavaConfig

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan({ "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common",
            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess" })
    @MapperScan(basePackages = {
            "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository",
            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.repository" }, sqlSessionTemplateRef = "batchModeSqlSessionTemplate") // (8)
    public class DBAccessByItemListenerConfig {

        // omitted

        // (9)
        @Bean
        public CacheSetListener cacheSetListener() {
            return new CacheSetListener();
        }

        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           ItemReader<SalesPerformanceDetail> reader,
                           @Qualifier("updateItemFromDBProcessor") ItemProcessor<SalesPerformanceDetail, SalesPlanDetail> processor,
                           ItemWriter<SalesPlanDetail> writer,
                           CacheSetListener cacheSetListener) {
            return new StepBuilder("DBAccessByItemListener.step01",
                    jobRepository)
                    .<SalesPerformanceDetail, SalesPlanDetail> chunk(10, transactionManager)
                    .listener(cacheSetListener) // (11)
                    .reader(reader).processor(processor) // (10)
                    .writer(writer)
                    .build();
        }

        @Bean
        public Job DBAccessByItemListener(JobRepository jobRepository,
                                                Step step01,
                                                JobExecutionLoggingListener listener) {
            return new JobBuilder("DBAccessByItemListener",
                    jobRepository).start(step01).listener(listener).build();
        }
    }

XMLConfig

    <!-- omitted -->

    <!-- (8) -->
    <mybatis:scan
            base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository"
            template-ref="batchModeSqlSessionTemplate"/>
    <!-- (9) -->
    <bean id="cacheSetListener"
          class="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.CacheSetListener"/>

    <!-- omitted -->

    <batch:job id="DBAccessByItemListener" job-repository="jobRepository">
        <batch:step id="DBAccessByItemListener.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="reader"
                             processor="updateItemFromCacheProcessor"
                             writer="writer" commit-interval="10"/> <!-- (10) -->
                <!-- (11) -->
                <batch:listeners>
                    <batch:listener ref="cacheSetListener"/>
                </batch:listeners>
            </batch:tasklet>
        </batch:step>
    </batch:job>

表 85. 説明
項番 | 説明
(1) | `Mapper`インタフェースをInjectする。
(2) | `Mapper`インタフェースから取得したデータをキャッシュするためのBeanをInjectする。
(3) | リスナーにて、`Mapper`インタフェースからデータを取得してキャッシュする。 ここでは、`StepExecutionListener#beforeStep`にてステップ実行前にキャッシュを作成し、 以降の処理ではキャッシュを参照することで、入出力を低減し処理効率を高めている。
(4) | (2)で設定したキャッシュと同じBeanをInjectする。
(5) | キャッシュから該当するデータを取得する。
(6) | 更新データにキャッシュからのデータを反映する。
(7) | キャッシュクラスをコンポーネントとして実装する。 ここではBeanスコープは`singleton`にしている。ジョブに応じて設定すること。
(8) | Mapper XMLの登録を行う。 `@MapperScanのsqlSessionTemplateRef属性/<mybatis:scan>要素のtemplate-ref属性`に`BATCH`が設定されている`batchModeSqlSessionTemplate`を指定する。
(9) | `Mapper`インタフェースを利用するリスナーを定義する。
(10) | キャッシュを利用するItemProcessorを指定する。
(11) | (9)で定義したリスナーを登録する。

| リスナーでのSqlSessionFactoryの利用 上記の例では、`batchModeSqlSessionTemplate`を設定しているが、`jobSqlSessionFactory`を設定してもよい。 チャンクのスコープ外で動作するリスナーについては、トランザクション外で処理されるため、 `jobSqlSessionFactory`を設定しても問題ない。

#### 5.2.3. How to extend

##### 5.2.3.1. CompositeItemWriterにおける複数テーブルの更新

チャンクモデルで、1つの入力データに対して複数のテーブルへ更新を行いたい場合は、Spring Batchが提供する`CompositeItemWriter`を利用し、 各テーブルに対応した`MyBatisBatchItemWriter`を連結することで実現できる。

ここでは、売上計画と売上実績の2つのテーブルを更新する場合の実装例を示す。

`ItemProcessor`の実装例

    @Component
    public class SalesItemProcessor implements ItemProcessor<SalesPlanDetail, SalesDTO> {
        @Override
        public SalesDTO process(SalesPlanDetail item) throws Exception { // (1)

            SalesDTO salesDTO = new SalesDTO();

            // (2)
            SalesPerformanceDetail spd = new SalesPerformanceDetail();
            spd.setBranchId(item.getBranchId());
            spd.setYear(item.getYear());
            spd.setMonth(item.getMonth());
            spd.setCustomerId(item.getCustomerId());
            spd.setAmount(new BigDecimal(0L));
            salesDTO.setSalesPerformanceDetail(spd);

            // (3)
            item.setAmount(item.getAmount().add(new BigDecimal(1L)));
            salesDTO.setSalesPlanDetail(item);

            return salesDTO;
        }
    }

DTOの実装例

    public class SalesDTO implements Serializable {

        // (4)
        private SalesPlanDetail salesPlanDetail;

        // (5)
        private SalesPerformanceDetail salesPerformanceDetail;

        // omitted
    }

MapperXMLの実装例

    <mapper namespace="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.repository.SalesRepository">

        <select id="findAll" resultType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.plan.SalesPlanDetail">
            <![CDATA[
            SELECT
                branch_id AS branchId, year, month, customer_id AS customerId, amount
            FROM
                sales_plan_detail
            ORDER BY
                branch_id ASC, year ASC, month ASC, customer_id ASC
            ]]>
        </select>

        <!-- (6) -->
        <update id="update" parameterType="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.SalesDTO">
            <![CDATA[
            UPDATE
                sales_plan_detail
            SET
                amount = #{salesPlanDetail.amount}
            WHERE
                branch_id = #{salesPlanDetail.branchId}
            AND
                year = #{salesPlanDetail.year}
            AND
                month = #{salesPlanDetail.month}
            AND
                customer_id = #{salesPlanDetail.customerId}
            ]]>
        </update>

        <!-- (7) -->
        <insert id="create" parameterType="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.SalesDTO">
            <![CDATA[
            INSERT INTO
                sales_performance_detail(
                    branch_id,
                    year,
                    month,
                    customer_id,
                    amount
                )
            VALUES (
                #{salesPerformanceDetail.branchId},
                #{salesPerformanceDetail.year},
                #{salesPerformanceDetail.month},
                #{salesPerformanceDetail.customerId},
                #{salesPerformanceDetail.amount}
            )
            ]]>
        </insert>

    </mapper>

JavaConfig

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan(value = { "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common",
            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess" }, scopedProxy = ScopedProxyMode.TARGET_CLASS)
    @MapperScan(basePackages = {
            "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository",
            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.repository" }, sqlSessionTemplateRef = "batchModeSqlSessionTemplate")
    public class UseCompositeItemWriterConfig {

        @Bean
        public MyBatisCursorItemReader<SalesPlanDetail> reader(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory) {
            return new MyBatisCursorItemReaderBuilder<SalesPlanDetail>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .queryId(
                            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.repository.SalesRepository.findAll")
                    .build();
        }

        // (8)
        @Bean
        public MyBatisBatchItemWriter<SalesDTO> planWriter(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
                @Qualifier("batchModeSqlSessionTemplate") SqlSessionTemplate batchModeSqlSessionTemplate) {
            return new MyBatisBatchItemWriterBuilder<SalesDTO>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .statementId(
                            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.repository.SalesRepository.update")
                    .sqlSessionTemplate(batchModeSqlSessionTemplate)
                    .build();
        }

        // (9)
        @Bean
        public MyBatisBatchItemWriter<SalesDTO> performanceWriter(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
                @Qualifier("batchModeSqlSessionTemplate") SqlSessionTemplate batchModeSqlSessionTemplate) {
            return new MyBatisBatchItemWriterBuilder<SalesDTO>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .statementId(
                            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.repository.SalesRepository.create")
                    .sqlSessionTemplate(batchModeSqlSessionTemplate)
                    .build();
        }

        // (10)
        @Bean
        public CompositeItemWriter<SalesDTO> writer(
                @Qualifier("performanceWriter") MyBatisBatchItemWriter<SalesDTO> performanceWriter,
                @Qualifier("planWriter") MyBatisBatchItemWriter<SalesDTO> planWriter) {
            return new CompositeItemWriter<SalesDTO>(performanceWriter, planWriter); // (11)
        }

        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           @Qualifier("reader") ItemReader<SalesPlanDetail> reader,
                           @Qualifier("salesItemProcessor") ItemProcessor<SalesPlanDetail, SalesDTO> processor,
                           @Qualifier("writer") ItemWriter<SalesDTO> writer) {
            return new StepBuilder("useCompositeItemWriter.step01",
                    jobRepository)
                    .<SalesPlanDetail, SalesDTO> chunk(3, transactionManager)
                    .reader(reader).processor(processor)
                    .writer(writer) // (12)
                    .build();
        }

        @Bean
        public Job useCompositeItemWriter(JobRepository jobRepository,
                                                Step step01,
                                                JobExecutionLoggingListener listener) {
            return new JobBuilder("useCompositeItemWriter",
                    jobRepository).start(step01).listener(listener).build();
        }

    }

XMLConfig

    <!-- reader using MyBatisCursorItemReader -->
    <bean id="reader" class="org.mybatis.spring.batch.MyBatisCursorItemReader"
          p:queryId="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.repository.SalesRepository.findAll"
          p:sqlSessionFactory-ref="jobSqlSessionFactory"/>

    <!-- writer MyBatisBatchItemWriter -->
    <!-- (8) -->
    <bean id="planWriter" class="org.mybatis.spring.batch.MyBatisBatchItemWriter"
          p:statementId="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.repository.SalesRepository.update"
          p:sqlSessionTemplate-ref="batchModeSqlSessionTemplate"/>

    <!-- (9) -->
    <bean id="performanceWriter" class="org.mybatis.spring.batch.MyBatisBatchItemWriter"
          p:statementId="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.dbaccess.repository.SalesRepository.create"
          p:sqlSessionTemplate-ref="batchModeSqlSessionTemplate"/>

    <!-- (10) -->
    <bean id="writer" class="org.springframework.batch.item.support.CompositeItemWriter">
        <property name="delegates">
          <!-- (11)-->
            <list>
                <ref bean="performanceWriter"/>
                <ref bean="planWriter"/>
            </list>
        </property>
    </bean>

    <!-- (12) -->
    <batch:job id="useCompositeItemWriter" job-repository="jobRepository">
        <batch:step id="useCompositeItemWriter.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="reader"
                             processor="salesItemProcessor"
                             writer="writer" commit-interval="3"/>
            </batch:tasklet>
        </batch:step>
    </batch:job>

表 86. 説明
項番 | 説明
(1) | 入力データに対して2つのテーブルを更新するための各エンティティを保持するDTOを出力とする`ItemProcessor`を実装する。 `ItemWriter`には2つのテーブルを更新するために異なるオブジェクトを渡すことはできないため、更新に必要なオブジェクトを集約するDTOを定義している。
(2) | 売上実績(SalesPerformanceDetail)を新規作成するためのエンティティを作成し、DTOに格納する。
(3) | 入力データでもある売上計画(SalesPlanDetail)を更新するため、入力データを更新してDTOに格納する。
(4) | 売上計画(SalesPlanDetail)を保持するようにDTOに定義する。
(5) | 売上実績(SalesPerformanceDetail)を保持するようにDTOに定義する。
(6) | DTOから取得した売上計画(SalesPlanDetail)で、売上計画テーブル(sales_plan_detail)を更新するSQLを定義する。
(7) | DTOから取得した売上実績(SalesPlanDetail)で、売上実績テーブル(sales_performance_detail)を新規作成するSQLを定義する。
(8) | 売上計画テーブル(sales_plan_detail)を更新する`MyBatisBatchItemWriter`を定義する。
(9) | 売上実績テーブル(sales_performance_detail)を新規作成する`MyBatisBatchItemWriter`を定義する。
(10) | (8),(9)を順番に実行するために`CompositeItemWriter`を定義する。
(11) | `CompositeItemWriterのコンストラクタの引数/<list>要素内`に(8),(9)を設定する。指定した順番にItemWriterが実行される。
(12) | `StepBuilderのwriterメソッド/<batch:chunk>要素のwriter属性`に(10)で定義したBeanを指定する。`processor`属性には(1)のItemProcessorを指定する。

| 複数データソースへの出力(1ステップ)で説明した `org.springframework.data.transaction.ChainedTransactionManager`と同時に使用することで複数データソースに対しての更新もできる。 また、CompositeItemWriterは、ItemWriter実装であれば連結できるので、 MyBatisBatchItemWriterとFlatFileItemWriterを設定することで、データベース出力とファイル出力を同時に行うこともできる。

### 5.3. ファイルアクセス

#### 5.3.1. Overview

本節では、ファイルの入出力を行う方法について説明する。

本章における、ファイルのReader/WriterのBean定義については、チャンクモデルとタスクレットモデルとで同じ定義になる。

##### 5.3.1.1. 扱えるファイルの種類

扱えるファイルの種類

Macchinetta Batch 2.xで扱えるファイルは以下のとおりである。
これは、TERASOLUNA Batch 5.xにて扱えるものと同じである。

- フラットファイル

- XML

ここではフラットファイルの入出力を行うための方法について説明したのち、 XMLについてHow to extendで説明する。

まず、Macchinetta Batch 2.xで扱えるフラットファイルの種類を示す。
フラットファイルにおける行をここでは`レコード`と呼び、 ファイルの種類はレコードの形式にもとづく、とする。

表 87. レコード形式
形式 | 概要
--- | ---
可変長レコード | CSVやTSVに代表される区切り文字により各項目を区切ったレコード形式。各項目の長さが可変である。
固定長レコード | 項目の長さ(バイト数)により各項目を区切ったレコード形式。各項目の長さが固定である。
単一文字列レコード | 1レコードを1文字列として扱う形式。

扱えるファイルの構造

フラットファイルの基本構造は以下の2点から構成される。

- レコード区分

- レコードフォーマット

表 88. フラットファイルのフォーマットを構成する要素
要素 | 概要
--- | ---
レコード区分 | レコードの種類、役割を指す。ヘッダ、データ、トレーラなどがある。 詳しくは後述する。
レコードフォーマット | ヘッダ、データ、トレーラレコードがそれぞれ何行あるのか、ヘッダ部～トレーラ部が複数回繰り返されるかなど、レコードの構造を指す。 シングルフォーマットとマルチフォーマットがある。詳しくは後述する。

Macchinetta Batch 2.xでは、各種レコード区分をもつシングルフォーマットおよびマルチフォーマットのフラットファイルを扱うことができる。

各種レコード区分およびレコードフォーマットについて説明する。

各種レコード区分の概要を以下に示す。

表 89. レコード区分ごとの特徴
レコード区分 | 概要
--- | ---
ヘッダレコード | ファイル(データ部)の先頭に付与されるレコードである。 フィールド名、ファイル共通の事項、データ部の集計情報などをもつ。
データレコード | ファイルの主な処理対象となるデータをもつレコードである。
トレーラ/フッタレコード | ファイル(データ部)の末尾に付与されるレコードである。 ファイル共通の事項、データ部の集計情報などをもつ。 シングルフォーマットの場合、フッタレコードと呼ばれることもある。
フッタ/エンドレコード | マルチフォーマットの場合にファイルの末尾に付与されるレコードである。 ファイル共通の事項、ファイル全体の集計情報などをもつ。

| レコード区分を示すフィールドについて ヘッダレコードやトレーラレコードをもつフラットファイルでは、レコード区分を示すフィールドをもたせる場合がある。 Macchinetta Batch 2.xでは特にマルチフォーマットファイルの処理において、レコード区分ごとに異なる処理を実施する場合などにレコード区分のフィールドを活用する。 レコード区分によって実行する処理を選択する場合の実装は、マルチフォーマットを参考にすること。

| ファイルフォーマット関連の名称について 個々のシステムにおけるファイルフォーマットの定義によっては、 フッタレコードをエンドレコードと呼ぶなど本ガイドラインとは異なる名称が使われている場合がある。 適宜読み替えを行うこと。

シングルフォーマットおよびマルチフォーマットの概要を以下に示す。

表 90. シングルフォーマットおよびマルチフォーマットの概要
フォーマット | 概要
--- | ---
シングルフォーマット | ヘッダn行 + データn行 + トレーラn行 の形式である。
マルチフォーマット | (ヘッダn行 + データn行 + トレーラn行)* n + フッタn行 の形式である。 シングルフォーマットを複数回繰り返した後にフッタレコードが付与されている形式である。

マルチフォーマットのレコード構成を図に表すと下記のようになる。

図 38. マルチフォーマットのレコード構成図

シングルフォーマット、マルチフォーマットフラットファイルの例を以下に示す。
なお、ファイルの内容説明に用いるコメントアウトを示す文字として`//`を使用する。

シングルフォーマット、レコード区分なしフラットファイル(CSV形式)の例

    branchId,year,month,customerId,amount  // (1)
    000001,2016,1,0000000001,100000000  // (2)
    000001,2016,1,0000000002,200000000  // (2)
    000001,2016,1,0000000003,300000000  // (2)
    000001,3,600000000  // (3)

表 91. ファイルの内容の項目一覧
項番 | 説明
--- | ---
(1) | ヘッダレコードである。 データ部のフィールド名を示している。
(2) | データレコードである。
(3) | トレーラレコードである。 データ部の集計情報を保持している。

マルチフォーマット、レコード区分ありのフラットファイル(CSV形式)の例

    // (1)
    H,branchId,year,month,customerId,amount  // (2)
    D,000001,2016,1,0000000001,100000000
    D,000001,2016,1,0000000002,200000000
    D,000001,2016,1,0000000003,300000000
    T,000001,3,600000000
    H,branchId,year,month,customerId,amount  // (2)
    D,00002,2016,1,0000000004,400000000
    D,00002,2016,1,0000000005,500000000
    D,00002,2016,1,0000000006,600000000
    T,00002,3,1500000000
    H,branchId,year,month,customerId,amount  // (2)
    D,00003,2016,1,0000000007,700000000
    D,00003,2016,1,0000000008,800000000
    D,00003,2016,1,0000000009,900000000
    T,00003,3,2400000000
    F,3,9,4500000000  // (3)

表 92. ファイルの内容の項目一覧
項番 | 説明
--- | ---
(1) | レコードの先頭にレコード区分を示すフィールドをもっている。 それぞれ下記のレコード区分を示す。 `H`：ヘッダレコード `D`：データレコード `T`：トレーラレコード `F`：フッタレコード
(2) | branchIdが変わるごとにヘッダ、データ、トレーラを3回繰り返している。
(3) | フッタレコードである。 ファイル全体の集計情報を保持している。

| データ部のフォーマットに関する前提 How to useでは、データ部のレコードが同一のフォーマットである事を前提として説明する。 これは、データ部のレコードがすべて同じ変換対象クラスへマッピングされることを意味する。

| マルチフォーマットファイルの説明について How to useでは、シングルフォーマットファイルについて説明する。 マルチフォーマットや上記の構造にフッタ部を含む構造をもつフラットファイルについては、How to extendを参照。

##### 5.3.1.2. フラットファイルの入出力を行うコンポーネント

フラットファイルを扱うためのクラスを示す。

入力

フラットファイルの入力を行うために使用するクラスの関連は以下のとおりである。

図 39. フラットファイルの入力を行うために使用するクラスの関連

各コンポーネントの呼び出し関係は以下のとおりである。

図 40. 各コンポーネントの呼び出し関係

各コンポーネントの詳細を以下に示す。

org.springframework.batch.item.file.FlatFileItemReader

  フラットファイルを読み込みに使用する`ItemReader`の実装クラス。以下のコンポーネントを利用する。
  簡単な処理の流れは以下のとおり。
  1.`BufferedReaderFactory`を使用して`BufferedReader`を取得する。
  2.取得した`BufferedReader`を使用してフラットファイルから1レコードを読み込む。
  3.`LineMapper`を使用して1レコードを対象Beanへマッピングする。

  org.springframework.batch.item.file.BufferedReaderFactory

    ファイルを読み込むための`BufferedReader`を生成する。

  org.springframework.batch.item.file.LineMapper

    1レコードを対象Beanへマッピングする。以下のコンポーネントを利用する。
    簡単な処理の流れは以下のとおり。
    1.`LineTokenizer`を使用して1レコードを各項目に分割する。
    2.`FieldSetMapper`によって分割した項目をBeanのプロパティにマッピングする。

    org.springframework.batch.item.file.transform.LineTokenizer

      ファイルから取得した1レコードを各項目に分割する。
      分割された各項目は`FieldSet`クラスに格納される。

    org.springframework.batch.item.file.mapping.FieldSetMapper

      分割した1レコード内の各項目を対象Beanのプロパティへマッピングする。

出力

フラットファイルの出力を行うために使用するクラスの関連は以下のとおりである。

図 41. フラットファイルの出力を行うために使用するクラスの関連

各コンポーネントの呼び出し関係は以下のとおりである。

図 42. 各コンポーネントの呼び出し関係

org.springframework.batch.item.file.FlatFileItemWriter

  フラットファイルへの書き出しに使用する`ItemWriter`の実装クラス。以下のコンポーネントを利用する。 `LineAggregator`対象Beanを1レコードへマッピングする。

  org.springframework.batch.item.file.transform.LineAggregator

    対象Beanを1レコードへマッピングするために使う。 Beanのプロパティとレコード内の各項目とのマッピングは`FieldExtractor`で行う。

    org.springframework.batch.item.file.transform.FieldExtractor

      対象Beanのプロパティを1レコード内の各項目へマッピングする。

#### 5.3.2. How to use

フラットファイルのレコード形式別に使い方を説明する。

- 可変長レコード

- 固定長レコード

- 単一文字列レコード

その後、以下の項目について説明する。

- ヘッダとフッタ

- 複数ファイル

- コントロールブレイク

##### 5.3.2.1. 可変長レコード

可変長レコードファイルを扱う場合の定義方法を説明する。

###### 5.3.2.1.1. 入力

下記の入力ファイルを読み込むための設定例を示す。

入力ファイル例

    000001,2016,1,0000000001,1000000000
    000002,2017,2,0000000002,2000000000
    000003,2018,3,0000000003,3000000000

変換対象クラス

    public class SalesPlanDetail {

        private String branchId;
        private int year;
        private int month;
        private String customerId;
        private BigDecimal amount;

        // omitted getter/setter
    }

上記のファイルを読む込むための設定は以下のとおり。

JavaConfig
Bean定義例

    @Bean
    @StepScope
    public FlatFileItemReader<SalesPlanDetail> reader(
            @Value("#{jobParameters['inputFile']}") File inputFile) {
        final DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer(); // (5)
        tokenizer.setNames("branchId", "year", "month", "customerId", "amount"); // (6)
        tokenizer.setDelimiter(","); // (7)
        tokenizer.setQuoteCharacter('"'); // (8)
        final BeanWrapperFieldSetMapper<SalesPlanDetail> fieldSetMapper = new BeanWrapperFieldSetMapper<>(); // (9)
        fieldSetMapper.setTargetType(SalesPlanDetail.class);
        final DefaultLineMapper<SalesPlanDetail> lineMapper = new DefaultLineMapper<>(); // (4)
        lineMapper.setLineTokenizer(tokenizer);
        lineMapper.setFieldSetMapper(fieldSetMapper);
        return new FlatFileItemReaderBuilder<SalesPlanDetail>()
                .name(ClassUtils.getShortName(FlatFileItemReader.class))
                .resource(new FileSystemResource(inputFile)) // (1)
                .lineMapper(lineMapper)
                .encoding("MS932") // (2)
                .strict(true) // (3)
                .build();
    }

`FlatFileItemReaderBuilder`クラスでの`name`メソッドの使用

`FlatFileItemReaderBuilder`は、`FlatFileItemReader`を生成するBuilderクラスである。

本章で登場する以下のBuilderクラスでは、デフォルトで`name`メソッドの使用が必須となっている。

- `FlatFileItemReaderBuilder` / `FlatFileItemWriterBuilder`

- `MultiResourceItemReaderBuilder` / `MultiResourceItemWriterBuilder`

- `StaxEventItemReaderBuilder` / `StaxEventItemWriterBuilder`

`name`メソッドでは、`ExecutionContext`に実行状態を保存する際のキー名を引数に設定する。キー名は、特段の理由がないかぎり、`ItemStreamSupport`のサブクラス名を指定する。`ItemStreamSupport`のサブクラスとは、Builderクラスで生成するクラスのことである。

`name`メソッドで`ItemStreamSupport`のサブクラス名を指定する理由

`ExecutionContext`やログに出力される名前を、その出力元のクラス名と一致させておくことで、トレーサビリティを担保する。これは障害解析時等に、原因箇所特定の手がかりとするためである。

FlatFileItemReaderBuilderでの設定例

`ItemStreamSupport`のサブクラス名は`org.springframework.batch.item.file.FlatFileItemReader`の短縮名、すなわち`"FlatFileItemReader"`となる。下記のように設定する。

    return new FlatFileItemReaderBuilder<SalesPlanDetail>()
      .name(ClassUtils.getShortName(FlatFileItemReader.class))

なお、Builderクラスを使用せず直接`FlatFileItemReader`をnewしたときも、デフォルトでコンストラクタ内で`ItemStreamSupport`のサブクラス名（`"FlatFileItemReader"`）が設定される（以下）。

org.springframework.batch.item.file.FlatFileItemReaderのコンストラクタ

    public FlatFileItemReader() {
      setName(ClassUtils.getShortName(FlatFileItemReader.class));
    }

`name`メソッドについては以下の公式リファレンスを参照のこと。

- クラス FlatFileItemReaderBuilder<T>

- クラス FlatFileItemWriterBuilder<T>

- クラス MultiResourceItemReaderBuilder<T>

- クラス MultiResourceItemWriterBuilder<T>

- クラス StaxEventItemReaderBuilder<T>

- クラス StaxEventItemWriterBuilder<T>

XMLConfig
Bean定義例

    <!-- (1) (2) (3) -->
    <bean id="reader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}"
          p:encoding="MS932"
          p:strict="true">
      <property name="lineMapper">  <!-- (4) -->
        <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
          <property name="lineTokenizer">  <!-- (5) -->
            <!-- (6) (7) (8) -->
            <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
                  p:names="branchId,year,month,customerId,amount"
                  p:delimiter=","
                  p:quoteCharacter='"'/>
          </property>
          <property name="fieldSetMapper">  <!-- (9) -->
            <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
                  p:targetType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.plan.SalesPlanDetail"/>
          </property>
        </bean>
      </property>
    </bean>

表 93. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 入力ファイルを設定する。 |  | なし
(2) | encoding | 入力ファイルのエンコーディングを設定する。 未指定の場合のデフォルト値は「UTF-8」。 |  | UTF-8
(3) | strict | trueを設定すると、入力ファイルが存在しない(開けない)場合に例外が発生する。 |  | true
(4) | lineMapper | `org.springframework.batch.item.file.mapping.DefaultLineMapper`を設定する。 `DefaultLineMapper`は、設定された`LineTokenizer`と`FieldSetMapper`を用いてレコードを変換対象クラスへ変換する基本的な動作を提供する`LineMapper`である。 |  | なし
(5) | lineTokenizer | `org.springframework.batch.item.file.transform.DelimitedLineTokenizer`を設定する。 `DelimitedLineTokenizer`は、区切り文字を指定してレコードを分割する`LineTokenizer`の実装クラス。 CSV形式の一般的書式とされるRFC-4180の仕様に定義されている、エスケープされた改行、区切り文字、囲み文字の読み込みに対応している。 |  | なし
(6) | names | 1レコードの各項目に名前を付与する。 `FieldSetMapper`で使われる`FieldSet`で設定した名前を用いて各項目を取り出すことができるようになる。 レコードの先頭から各名前をカンマ区切りで設定する。 `BeanWrapperFieldSetMapper`を利用する場合は、必須設定である。 |  | なし
(7) | delimiter | 区切り文字を設定する。 TSVファイルの読み込みを行う場合には、TSV形式ファイルの入力方法を参照。 |  | カンマ
(8) | quoteCharacter | 囲み文字を設定する |  | なし
(9) | fieldSetMapper | 文字列や数字など特別な変換処理が不要な場合は、`org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper`を利用し、 `targetType`属性に変換対象クラスを指定する。 これにより、(5)で設定した各項目の名前と一致するフィールドに値を自動的に設定したインスタンスを生成する。 変換処理が必要な場合は、`org.springframework.batch.item.file.mapping.FieldSetMapper`の実装クラスを設定する。 |  | なし

TSV形式ファイルの入力方法

TSVファイルの読み込みを行う場合には、区切り文字にタブを設定することで実現可能である。

JavaConfig
TSVファイル読み込み時:区切り文字設定例(定数による設定)

    tokenizer.setDelimiter(DelimitedLineTokenizer.DELIMITER_TAB);

XMLConfig
TSVファイル読み込み時:区切り文字設定例(定数による設定)

    <property name="delimiter">
        <util:constant
                static-field="org.springframework.batch.item.file.transform.DelimitedLineTokenizer.DELIMITER_TAB"/>
    </property>

| FieldSetMapperの独自実装について FieldSetMapperを独自に実装する場合については、How to extendを参照。

| BeanWrapperFieldSetMapperの留意事項 `BeanWrapperFieldSetMapper`は、文字列を`trim`するため、先頭・末尾の空白と制御文字が削除されることに留意すること。

###### 5.3.2.1.2. 出力

下記の出力ファイルを書き出すための設定例を示す。

出力ファイル例

    001,CustomerName001,CustomerAddress001,11111111111,001
    002,CustomerName002,CustomerAddress002,11111111111,002
    003,CustomerName003,CustomerAddress003,11111111111,003

変換対象クラス

    public class Customer {

        private String customerId;
        private String customerName;
        private String customerAddress;
        private String customerTel;
        private String chargeBranchId;
        private LocalDateTime createDate;
        private LocalDateTime updateDate;

        // omitted getter/setter
    }

上記のファイルを書き出すための設定は以下のとおり。

JavaConfig
Bean定義例

    // Writer
    @Bean
    @StepScope
    public FlatFileItemWriter<Customer> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile) {
        final BeanWrapperFieldExtractor<Customer> fieldExtractor = new BeanWrapperFieldExtractor<>(); // (10)
        fieldExtractor.setNames(
                new String[] { "customerId", "customerName", "customerAddress",
                        "customerTel", "chargeBranchId" }); // (11)
        final DelimitedLineAggregator<Customer> lineAggregator = new DelimitedLineAggregator<>(); // (8)
        lineAggregator.setDelimiter(","); // (9)
        lineAggregator.setFieldExtractor(fieldExtractor);
        return new FlatFileItemWriterBuilder<Customer>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .resource(new FileSystemResource(outputFile)) // (1)
                .encoding("MS932") // (2)
                .lineSeparator("\n") // (3)
                .append(true) // (4)
                .shouldDeleteIfExists(false) // (5)
                .shouldDeleteIfEmpty(false) // (6)
                .transactional(true) // (7)
                .lineAggregator(lineAggregator)
                .build();
    }

XMLConfig
Bean定義例

    <!-- Writer -->
    <!-- (1) (2) (3) (4) (5) (6) (7) -->
    <bean id="writer"
          class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"
          p:encoding="MS932"
          p:lineSeparator="&#x0A;"
          p:appendAllowed="true"
          p:shouldDeleteIfExists="false"
          p:shouldDeleteIfEmpty="false"
          p:transactional="true">
      <property name="lineAggregator">  <!-- (8) -->
        <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator"
              p:delimiter=",">  <!-- (9) -->
          <property name="fieldExtractor">  <!-- (10) -->
            <!-- (11) -->
            <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                  p:names="customerId,customerName,customerAddress,customerTel,chargeBranchId"/>
          </property>
        </bean>
      </property>
    </bean>

表 94. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 出力ファイルを設定する。 |  | なし
(2) | encoding | 出力ファイルのエンコーディングを設定する。 未指定の場合のデフォルト値は「UTF-8」。 |  | UTF-8
(3) | lineSeparator | レコード区切り(改行コード)を設定する。 |  | システムプロパティの`line.separator`
(4) | appendAllowed | trueの場合、既存のファイルに追記をする。 trueの場合、shouldDeleteIfExistsの設定値は無効化されるため、注意が必要である。 |  | false
(5) | shouldDeleteIfExists | appendAllowedがtrueの場合は、この設定は無効化されるため、値を指定しないことを推奨する。 ファイル出力前に出力ファイルが存在していた場合の動作を設定する。 trueの場合、ファイル出力前に既にファイルが存在すれば削除する。 falseの場合、ファイル出力前に既にファイルが存在すれば例外をスローする。 |  | true
(6) | shouldDeleteIfEmpty | 出力件数が0件であった場合にファイルを削除するかを設定する。 trueの場合、ファイル出力後に出力件数が0件であれば出力対象ファイルを削除する。 他の設定との組み合わせによって意図しない動作をする場合がある。詳細はFlatFileItemWriterのshouldDeleteIfExistsとshouldDeleteIfEmptyの動作の違いを参照。 |  | false
(7) | transactional | トランザクション制御を行うかを設定する。詳細は、トランザクション制御を参照。 |  | true
(8) | lineAggregator | `org.springframework.batch.item.file.transform.DelimitedLineAggregator`を設定する。 フィールドを囲み文字で囲む場合は、`org.terasoluna.batch.item.file.transform.EnclosableDelimitedLineAggregator`を設定する。 `EnclosableDelimitedLineAggregator`の使用方法は後述する。 |  | なし
(9) | delimiter | 区切り文字を設定する。 |  | カンマ
(10) | fieldExtractor | 文字列や数字など特別な変換処理が不要な場合は、`org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor`が利用できる。 変換処理が必要な場合は、`org.springframework.batch.item.file.transform.FieldExtractor`の実装クラスを設定する。 `FieldExtractor`の実装例は固定長レコードの出力にて、全角文字のフォーマットを例に説明しているためそちらを参照。 |  | なし
(11) | names | 1レコードの各項目に名前を付与する。 レコードの先頭から各名前をカンマ区切りで設定する。 |  | なし

EnclosableDelimitedLineAggregatorについて

フィールドを囲み文字で囲む場合は、TERASOLUNA Batch 5.xが提供する`org.terasoluna.batch.item.file.transform.EnclosableDelimitedLineAggregator`を使用する。
この拡張クラスでは、Spring Batchにより提供されている`org.springframework.batch.item.file.transform.DelimitedLineAggregator`に、フィールドを囲み文字で囲む処理に対応することができ、RFC-4180 で求められている要件を満たすための拡張が行われている。 本機能の実装の経緯については、 Spring Batch/BATCH-2463 を参照。
`EnclosableDelimitedLineAggregator`の仕様は以下のとおり。

- 囲み文字、区切り文字を任意に指定可能

  - デフォルトはCSV形式で一般的に使用される以下の値である

    - 囲み文字：`"`(ダブルクォート)

    - 区切り文字：`,`(カンマ)

- CSV形式のフォーマットについて、CSV形式の一般的書式とされるRFC-4180では下記のように定義されている。

  - フィールドに改行、囲み文字、区切り文字が含まれていない場合、各フィールドはダブルクォート(囲み文字)で囲んでも囲わなくてもよい

  - 改行(CRLF)、ダブルクォート(囲み文字)、カンマ(区切り文字)を含むフィールドは、ダブルクォートで囲むべきである

  - フィールドがダブルクォート(囲み文字)で囲まれている場合、フィールドの値に含まれるダブルクォートは、その直前に1つダブルクォートを付加して、エスケープしなければならない

`EnclosableDelimitedLineAggregator`の使用方法を以下に示す。

出力ファイル例

    "001","CustomerName""001""","CustomerAddress,001","11111111111","001"
    "002","CustomerName""002""","CustomerAddress,002","11111111111","002"
    "003","CustomerName""003""","CustomerAddress,003","11111111111","003"

変換対象クラス

    // 上記の例と同様

JavaConfig
Bean定義例(lineAggregatorの設定のみ)

    final EnclosableDelimitedLineAggregator<SalesPlanDetail> lineAggregator = new EnclosableDelimitedLineAggregator<>(); // (1)
    lineAggregator.setDelimiter(','); // (2)
    lineAggregator.setEnclosure('"'); // (3)
    lineAggregator.setAllEnclosing(true); // (4)
    lineAggregator.setFieldExtractor(fieldExtractor);

XMLConfig
Bean定義例(lineAggregatorの設定のみ)

    <property name="lineAggregator">  <!-- (1) -->
      <!-- (2) (3) (4) -->
      <bean class="org.terasoluna.batch.item.file.transform.EnclosableDelimitedLineAggregator"
            p:delimiter=","
            p:enclosure='"'
            p:allEnclosing="true">
          <property name="fieldExtractor">
            <!-- omitted settings -->
          </property>
      </bean>
    </property>

表 95. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | lineAggregator | `org.terasoluna.batch.item.file.transform.EnclosableDelimitedLineAggregator`を設定する。 `EnclosableDelimitedLineAggregator`の使用方法はEnclosableDelimitedLineAggregatorについてを参照。 |  | なし
(2) | delimiter | 区切り文字を設定する。 TSVファイルの出力を行う場合には、TSV形式ファイルの出力方法を参照。 |  | カンマ
(3) | enclosure | 囲み文字を設定する。 囲み文字がフィールドに含まれる場合は、エスケープ処理として囲み文字を2つ連結されたものへ置換される。 |  | ダブルクォート
(4) | allEnclosing | trueの場合、すべてのフィールドが囲み文字で囲まれる。 falseの場合フィールド内に行頭復帰(CR)、改行(LF)、区切り文字、囲み文字が含まれるフィールドのみ囲み文字で囲まれる。 |  | false

TSV形式ファイルの出力方法

TSVファイルの出力を行う場合には、区切り文字にタブを設定することで実現可能である。

JavaConfig
TSVファイル出力時の区切り文字設定例(定数による設定)

    tokenizer.setDelimiter(DelimitedLineTokenizer.DELIMITER_TAB);

XMLConfig
TSVファイル出力時の区切り文字設定例(定数による設定)

    <property name="delimiter">
        <util:constant
                static-field="org.springframework.batch.item.file.transform.DelimitedLineTokenizer.DELIMITER_TAB"/>
    </property>

###### 5.3.2.1.3. FlatFileItemWriterのshouldDeleteIfExistsとshouldDeleteIfEmptyの動作の違い

`FlatFileItemWriter`のプロパティである、`shouldDeleteIfExists`と`shouldDeleteIfEmpty`は実行タイミングや判定対象が異なる。

表 96. shouldDeleteIfExistsとshouldDeleteIfEmptyの違い
| 機能 | 判定タイミング | 判定対象 | 設定値(true/false)の挙動
--- | --- | --- | --- | ---
shouldDeleteIfExists | 同一名称の出力ファイルが既に存在する場合の挙動を制御する | ファイル出力前 | 同一名称の出力ファイルの有無 | true:既存の出力ファイルを削除し、新たに出力ファイルを作成する false:例外をスローする
shouldDeleteIfEmpty | ジョブの出力件数が0件の場合の挙動を制御する | ファイル出力後 | ジョブの出力件数が0件かどうか (*1) | true:出力ファイルを削除する false:何もしない

*1. 特定条件下では出力件数が0件ではない場合でもファイルが削除される場合がある、詳細は後述の「`shouldDeleteIfEmpty`がtrueの場合の注意すべき動作」を参照。

`shouldDeleteIfExists`はファイル出力前の出力ファイルを対象とし、`shouldDeleteIfEmpty`はファイル出力後の出力件数を対象をしている。

`shouldDeleteIfEmpty`がtrueの場合の注意すべき動作について説明する。
以下の条件を満たした場合、出力件数が0件でなくとも出力ファイルが削除されるため注意が必要である。

- `shouldDeleteIfExists`にfalseを設定

- `shouldDeleteIfEmpty`にtrueを設定

- Jobによるファイル出力前に既に出力ファイルが存在する

条件を満たした場合、以下の様に処理が進む。

1. `shouldDeleteIfExists`はジョブ開始時に既存ファイルがあるかチェックされる。

2. この時点ではまだ`ItemWriter`によるファイル出力前だが、同名のファイルが既に存在したときはファイル出力を行われずに例外がスローされる。

3. しかし例外スロー前に後処理としてファイルクローズが行われ、ジョブ終了時の`shouldDeleteIfEmpty`が評価される。

4. ここで`shouldDeleteIfEmpty`がtrueの場合、前処理で例外がスローされているためジョブの出力件数が0件となりファイルが削除される。
   これは既存ファイルのレコードがたとえ10000行あっても出力件数が0件として評価されファイルが削除される。

このように、`shouldDeleteIfExists`と`souldDeleteIfEmpty`の設定値の組み合わせによっては、出力ファイルが意図せず削除される恐れがあり注意が必要である。
意図せぬ出力ファイルの削除を避けるため、以下を検討するとよい。

- 不要な出力ファイルは、別の方法で削除する

  - ジョブスケジューラ等で、不要な出力ファイルを定期的にクリーンアップする

- `shouldDeleteIfExists`、`shouldDeleteIfEmpty`による出力ファイル削除を禁止する

  - `shouldDeleteIfExists`、`shouldDeleteIfEmpty`はともにfalseとする

以上のことから、`shouldDeleteIfExists`と`shouldDeleteIfEmpty`は実行タイミングや判定対象が異なることと、特定条件下で出力ファイルが意図せず削除される恐れがあるため注意が必要である。

##### 5.3.2.2. 固定長レコード

固定長レコードファイルを扱う場合の定義方法を説明する。

###### 5.3.2.2.1. 入力

下記の入力ファイルを読み込むための設定例を示す。

Macchinetta Batch 2.xでは、レコードの区切りを改行で判断する形式とバイト数で判断する形式 に対応している。

入力ファイル例1(レコードの区切りは改行)

    売上012016 1   00000011000000000
    売上022017 2   00000022000000000
    売上032018 3   00000033000000000

入力ファイル例2(レコードの区切りはバイト数、32バイトで1レコード)

    売上012016 1   00000011000000000売上022017 2   00000022000000000売上032018 3   00000033000000000

表 97. 入力ファイル仕様
項番 | フィールド名 | データ型 | バイト数
--- | --- | --- | ---
(1) | branchId | String | 6
(2) | year | int | 4
(3) | month | int | 2
(4) | customerId | String | 10
(5) | amount | BigDecimal | 10

変換対象クラス

    public class SalesPlanDetail {

        private String branchId;
        private int year;
        private int month;
        private String customerId;
        private BigDecimal amount;

        // omitted getter/setter
    }

上記のファイルを読む込むための設定は以下のとおり。

JavaConfig
Bean定義例

    @Bean
    @StepScope
    public FlatFileItemReader<SalesPlanDetail> reader(
            @Value("#{jobParameters['inputFile']}") Resource inputFile) {
        BufferedReaderFactory  bufferedReaderFactory = new DefaultBufferedReaderFactory(); // (4)
        Range[] ranges = new Range[] {new Range(1, 6), new Range(7, 10), new Range(11, 12), new Range(13, 22), new Range(23, 32)}; // (8)
        final FixedByteLengthLineTokenizer tokenizer = new FixedByteLengthLineTokenizer(Charset.forName("MS932"), ranges); // (6)(9)
        tokenizer.setNames("branchId", "year", "month", "customerId", "amount"); // (7)
        final BeanWrapperFieldSetMapper<SalesPlanDetail> fieldSetMapper = new BeanWrapperFieldSetMapper<>(); // (10)
        fieldSetMapper.setTargetType(SalesPlanDetail.class);
        final DefaultLineMapper<SalesPlanDetail> lineMapper = new DefaultLineMapper<>(); // (5)
        lineMapper.setLineTokenizer(tokenizer); // (6)
        lineMapper.setFieldSetMapper(fieldSetMapper);
        FileSystemResourceLoader loader = new FileSystemResourceLoader();
        return new FlatFileItemReaderBuilder<SalesPlanDetail>()
                .name(ClassUtils.getShortName(FlatFileItemReader.class))
                .resource(inputFile) // (1)
                .encoding("MS932") // (2)
                .strict(true) // (3)
                .bufferedReaderFactory(bufferedReaderFactory) // (4)
                .lineMapper(lineMapper) // (5)
                .build();
    }

XMLConfig
Bean定義例

    <!-- (1) (2) (3) -->
    <bean id="reader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}"
          p:encoding="MS932"
          p:strict="true">
        <property name="bufferedReaderFactory">  <!-- (4) -->
            <bean class="org.springframework.batch.item.file.DefaultBufferedReaderFactory"/>
        </property>
        <property name="lineMapper">  <!-- (5) -->
            <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
                <property name="lineTokenizer">  <!-- (6) -->
                    <!-- (7) -->
                    <!-- (8) -->
                    <!-- (9) -->
                    <bean class="org.terasoluna.batch.item.file.transform.FixedByteLengthLineTokenizer"
                          p:names="branchId,year,month,customerId,amount"
                          c:ranges="1-6, 7-10, 11-12, 13-22, 23-32"
                          c:charset="MS932" />
                </property>
                <property name="fieldSetMapper">  <!-- (10) -->
                  <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
                        p:targetType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.plan.SalesPlanDetail"/>
                </property>
            </bean>
        </property>
    </bean>

表 98. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 入力ファイルを設定する。 |  | なし
(2) | encoding | 入力ファイルのエンコーディングを設定する。 未指定の場合のデフォルト値は「UTF-8」。 |  | UTF-8
(3) | strict | trueを設定すると、入力ファイルが存在しない(開けない)場合に例外が発生する。 |  | true
(4) | bufferedReaderFactory | レコードの区切りを改行で判断する場合は、デフォルト値である`org.springframework.batch.item.file.DefaultBufferedReaderFactory`を使用する。 `DefaultBufferedReaderFactory`が生成する`BufferedReader`は改行までを1レコードとして取得する。 レコードの区切りをバイト数で判断する場合は、TERASOLUNA Batch 5.xが提供する`org.terasoluna.batch.item.file.FixedByteLengthBufferedReaderFactory`を設定する。 `FixedByteLengthBufferedReaderFactory`が生成する`BufferedReader`は指定したバイト数までを1レコードとして取得する。 `FixedByteLengthBufferedReaderFactory`の詳しい仕様および使用方法は後述する。 |  | `DefaultBufferedReaderFactory`
(5) | lineMapper | `org.springframework.batch.item.file.mapping.DefaultLineMapper`を設定する。 |  | なし
(6) | lineTokenizer | TERASOLUNA Batch 5.xが提供する`org.terasoluna.batch.item.file.transform.FixedByteLengthLineTokenizer`を設定する。 |  | なし
(7) | names | 1レコードの各項目に名前を付与する。 `FieldSetMapper`で使われる`FieldSet`で設定した名前を用いて各項目を取り出すことができるようになる。 レコードの先頭から各名前をカンマ区切りで設定する。 `BeanWrapperFieldSetMapper`を利用する場合は、必須設定である。 |  | なし
(8) | ranges (コンストラクタ引数) | 区切り位置を設定する。レコードの先頭から区切り位置をカンマ区切りで設定する。 各区切り位置の単位はバイトであり、`開始位置,終了位置(JavaConfig)/開始位置-終了位置(XMLConfig)`形式で指定する。 区切り位置を設定した順番でレコードから指定された範囲を取得し、`FieldSet`に格納される。 (6)のnamesを指定した場合は区切り位置を設定した順番でnamesと対応付けて`FieldSet`に格納される。 |  | なし
(9) | charset (コンストラクタ引数) | (2)で指定したエンコーディングと同じ値を設定する。 |  | なし
(10) | fieldSetMapper | 文字列や数字など特別な変換処理が不要な場合は、`org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper`を利用し、 `targetType`属性に変換対象クラスを指定する。 これにより、(6)で設定した各項目の名前と一致するフィールドに値を自動的に設定したインスタンスを生成する。 変換処理が必要な場合は、`org.springframework.batch.item.file.mapping.FieldSetMapper`の実装クラスを設定する。 |  | なし

| FieldSetMapperの独自実装について FieldSetMapperを独自に実装する場合については、How to extendを参照。

FixedByteLengthBufferedReaderFactoryの使用方法

レコードの区切りをバイト数で判断するファイルを読み込む場合は、TERASOLUNA Batch 5.xが提供する`org.terasoluna.batch.item.file.FixedByteLengthBufferedReaderFactory`を使用する。

`FixedByteLengthBufferedReaderFactory`を使用することで指定したバイト数までを1レコードとして取得することができる。
`FixedByteLengthBufferedReaderFactory`の仕様は以下のとおり。

- コンストラクタ引数としてレコードのバイト数を指定する

- 指定されたバイト数を1レコードとしてファイルを読み込む`FixedByteLengthBufferedReader`を生成する

`FixedByteLengthBufferedReader`の使用は以下のとおり。

- インスタンス生成時に指定されたバイト長を1レコードとしてファイルを読み込む

- 改行コードが存在する場合、破棄せず1レコードのバイト長に含めて読み込みを行う

- 読み込み時に使用するファイルエンコーディングは`FlatFileItemWriter`に設定したものが`BufferedReader`生成時に設定される

`FixedByteLengthBufferedReaderFactory`の定義方法を以下に示す。

JavaConfig

    FixedByteLengthBufferedReaderFactory bufferedReaderFactory = new FixedByteLengthBufferedReaderFactory(32); // (1)

XMLConfig

    <property name="bufferedReaderFactory">
        <bean class="org.terasoluna.batch.item.file.FixedByteLengthBufferedReaderFactory"
            c:byteLength="32"/>  <!-- (1) -->
    </property>

表 99. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | byteLength (コンストラクタ引数) | 1レコードあたりのバイト数を設定する。 |  | なし

| 固定長ファイルを扱う場合に使用するコンポーネント 固定長ファイルを扱う場合は、Macchinetta Batch 2.xが提供するコンポーネントを使うことを前提にしている。 FixedByteLengthBufferedReaderFactory 改行なし固定長ファイルから、指定したエンコーディングのバイト数で1レコードを読み込む`BufferedReader`生成クラス FixedByteLengthLineTokenizer マルチバイト文字列に対応したバイト数区切りの`FixedLengthTokenizer`拡張クラス

| マルチバイト文字列を含むレコードを処理する場合 マルチバイト文字列を含むレコードを処理する場合は、`FixedByteLengthLineTokenizer`を必ず利用する。 Spring Batchが提供する`FixedLengthTokenizer`は、レコードをバイト数ではなく文字数で区切ってしまうため、期待どおりの項目切り出しが行われない恐れがある。 この点についてはSpring Batch/BATCH-2540で報告しているため、今後不要になる可能性がある。

| FieldSetMapperの実装については、How to extendを参照。

###### 5.3.2.2.2. 出力

下記の出力ファイルを書き出すための設定例を示す。

固定長ファイルを書き出すためには、Beanから取得した値をフィールドのバイト数にあわせてフォーマットを行う必要がある。
フォーマットの実行方法は全角文字が含まれるか否かによって下記のように異なる。

- 全角文字が含まれない場合(半角文字のみであり文字のバイト数が一定)

  - `FormatterLineAggregator`にてフォーマットを行う。

  - フォーマットは、`String.format`メソッドで使用する書式で設定する。

- 全角文字が含まれる場合(文字コードによって文字のバイト数が一定ではない)

  - `FieldExtractor`の実装クラスにてフォーマットを行う。

まず、出力ファイルに全角文字が含まれない場合の設定例を示し、その後全角文字が含まれる場合の設定例を示す。

出力ファイルに全角文字が含まれない場合の設定について下記に示す。

出力ファイル例

       0012016 10000000001  10000000
       0022017 20000000002  20000000
       0032018 30000000003  30000000

表 100. 出力ファイル仕様
項番 | フィールド名 | データ型 | バイト数
--- | --- | --- | ---
(1) | branchId | String | 6
(2) | year | int | 4
(3) | month | int | 2
(4) | customerId | String | 10
(5) | amount | BigDecimal | 10

フィールドのバイト数に満たない部分は半角スペース埋めとしている。

変換対象クラス

    public class SalesPlanDetail {

        private String branchId;
        private int year;
        private int month;
        private String customerId;
        private BigDecimal amount;

        // omitted getter/setter
    }

上記のファイルを書き出すための設定は以下のとおり。

JavaConfig
Bean定義

    @Bean
    @StepScope
    public FlatFileItemWriter<SalesPlanDetail> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile) {
        final BeanWrapperFieldExtractor<SalesPlanDetail> fieldExtractor = new BeanWrapperFieldExtractor<>(); // (10)
        fieldExtractor.setNames(
                new String[] { "branchId", "year", "month", "customerId", "amount" });
        final FormatterLineAggregator<SalesPlanDetail> lineAggregator = new FormatterLineAggregator<>(); // (8)
        lineAggregator.setFormat("%6s%4s%2s%10s%10s"); // (9)
        lineAggregator.setFieldExtractor(fieldExtractor);
        return new FlatFileItemWriterBuilder<SalesPlanDetail>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .resource(new FileSystemResource(outputFile)) // (1)
                .encoding("MS932") // (2)
                .lineSeparator("\n") // (3)
                .append(true) // (4)
                .shouldDeleteIfExists(false) // (5)
                .shouldDeleteIfEmpty(false) // (6)
                .transactional(true) // (7)
                .lineAggregator(lineAggregator)
                .build();
    }

XMLConfig
Bean定義

    <!-- Writer -->
    <!-- (1) (2) (3) (4) (5) (6) (7) -->
    <bean id="writer"
          class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"
          p:encoding="MS932"
          p:lineSeparator="&#x0A;"
          p:appendAllowed="true"
          p:shouldDeleteIfExists="false"
          p:shouldDeleteIfEmpty="false"
          p:transactional="true">
        <property name="lineAggregator">  <!-- (8) -->
            <bean class="org.springframework.batch.item.file.transform.FormatterLineAggregator"
                  p:format="%6s%4s%2s%10s%10s">  <!-- (9) -->
                <property name="fieldExtractor">  <!-- (10) -->
                  <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                        p:names="branchId,year,month,customerId,amount"/>  <!-- (11) -->
                </property>
            </bean>
        </property>
    </bean>

表 101. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 出力ファイルを設定する。 |  | なし
(2) | encoding | 出力ファイルのエンコーディングを設定する。 未指定の場合のデフォルト値は「UTF-8」。 |  | UTF-8
(3) | lineSeparator | レコード区切り(改行コード)を設定する。 改行なしにする場合は、空文字を設定する。 |  | システムプロパティの`line.separator`
(4) | appendAllowed | trueの場合、既存のファイルに追記をする。 trueの場合、shouldDeleteIfExistsの設定値は無効化されるため、注意が必要である。 |  | false
(5) | shouldDeleteIfExists | appendAllowedがtrueの場合は、この設定は無効化されるため、値を指定しないことを推奨する。 ファイル出力前に出力ファイルが存在していた場合の動作を設定する。 trueの場合、ファイル出力前に既にファイルが存在すれば削除する。 falseの場合、ファイル出力前に既にファイルが存在すれば例外をスローする。 |  | true
(6) | shouldDeleteIfEmpty | 出力件数が0件であった場合にファイルを削除するかを設定する。 trueの場合、ファイル出力後に出力件数が0件であれば出力対象ファイルを削除する。 他の設定との組み合わせによって意図しない動作をする場合がある。詳細はFlatFileItemWriterのshouldDeleteIfExistsとshouldDeleteIfEmptyの動作の違いを参照。 |  | false
(7) | transactional | トランザクション制御を行うかを設定する。詳細は、トランザクション制御を参照。 |  | true
(8) | lineAggregator | `org.springframework.batch.item.file.transform.FormatterLineAggregator`を設定する。 |  | なし
(9) | format | `String.format`メソッドで使用する書式で出力フォーマットを設定する。 |  | なし
(10) | fieldExtractor | 文字列や数字など特別な変換処理、全角文字のフォーマットが不要な場合は、`org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor`が利用できる。 値の変換処理や全角文字をフォーマットする等の対応が必要な場合は、`org.springframework.batch.item.file.transform.FieldExtractor`の実装クラスを設定する。 全角文字をフォーマットする場合における`FieldExtractor`の実装例は後述する。 |  | `PassThroughFieldExtractor`
(11) | names | 1レコードの各項目に名前を付与する。 レコードの先頭から各フィールドの名前をカンマ区切りで設定する。 |  | なし

| PassThroughFieldExtractorとは `FormatterLineAggregator`がもつ`fieldExtractor`プロパティのデフォルト値は`org.springframework.batch.item.file.transform.PassThroughFieldExtractor`である。 `PassThroughFieldExtractor`は、もとのアイテムに対して処理を行わずに返すクラスであり、`FieldExtractor`にて何も処理を行わない場合に使用する。 アイテムが配列またはコレクションの場合はそのまま返されるが、それ以外の場合は、単一要素の配列にラップされる。

全角文字が含まれるフィールドに対してフォーマットを行う際の設定例

全角文字に対するフォーマットを行う場合、文字コードにより1文字あたりのバイト数が異なるため、`FormatterLineAggregator`ではなく、`FieldExtractor`の実装クラスを使用する。

`FieldExtractor`の実装クラスは以下の要領で実装する。

- `FieldExtractor`クラスを実装し、`extract`メソッドをオーバーライドする

- `extract`メソッドは以下の要領で実装する

  - item(処理対象のBean)から値を取得し、適宜変換処理等を行う

  - Object型の配列に格納し返す

`FieldExtractor`の実装クラスで行う全角文字を含むフィールドのフォーマットは以下の要領で実装する。

- 文字コードに対するバイト数を取得する

- 取得したバイト数をもとにパディング・トリム処理で整形する

以下に全角文字を含むフィールドをフォーマットする場合の設定例を示す。

出力ファイル例

       0012016 10000000001  10000000
      番号2017 2 売上高002  20000000
     番号32018 3   売上003  30000000

出力ファイルの使用は上記の例と同様。

JavaConfig
Bean定義(lineAggregatorの設定のみ)

    final FormatterLineAggregator<SalesPlanDetail> lineAggregator = new FormatterLineAggregator<>(); // (1)
    lineAggregator.setFormat("%s%4s%2s%s%10s"); // (2)
    final SalesPlanFixedLengthFieldExtractor fieldExtractor = new SalesPlanFixedLengthFieldExtractor(); // (3)
    lineAggregator.setFieldExtractor(fieldExtractor);

XMLConfig
Bean定義(lineAggregatorの設定のみ)

    <property name="lineAggregator">  <!-- (1) -->
        <bean class="org.springframework.batch.item.file.transform.FormatterLineAggregator"
              p:format="%s%4s%2s%s%10s">  <!-- (2) -->
            <property name="fieldExtractor">  <!-- (3) -->
                <bean class="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.fileaccess.plan.SalesPlanFixedLengthFieldExtractor"/>
            </property>
        </bean>
    </property>

表 102. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | lineAggregator | `org.springframework.batch.item.file.transform.FormatterLineAggregator`を設定する。 |  | なし
(2) | format | `String.format`メソッドで使用する書式で出力フォーマットを設定する。 全角文字が含まれないフィールドに対してのみ桁数の指定をしている。 |  | なし
(3) | fieldExtractor | `FieldExtractor`の実装クラスを設定する。 実装例は後述する。 |  | `PassThroughFieldExtractor`

変換対象クラス

    public class SalesPlanDetail {

        private String branchId;
        private int year;
        private int month;
        private String customerId;
        private BigDecimal amount;

        // omitted getter/setter
    }

全角文字をフォーマットするFieldExtractorの実装例

    public class SalesPlanFixedLengthFieldExtractor implements FieldExtractor<SalesPlanDetail> {
        // (1)
        @Override
        public Object[] extract(SalesPlanDetail item) {
            Object[] values = new Object[5];  // (2)

            // (3)
            values[0] = fillUpSpace(item.getBranchId(), 6);  // (4)
            values[1] = item.getYear();
            values[2] = item.getMonth();
            values[3] = fillUpSpace(item.getCustomerId(), 10);  // (4)
            values[4] = item.getAmount();

            return values; // (8)
        }

        // It is a simple impl for example
        private String fillUpSpace(String val, int num) {
            String charsetName = "MS932";
            int len;
            try {
                len = val.getBytes(charsetName).length;  // (5)
            } catch (UnsupportedEncodingException e) {
                // omitted exception handling
            }

            // (6)
            if (len > num) {
                throw new IncorrectFieldLengthException("The length of field is invalid. " + "[value:" + val + "][length:"
                        + len + "][expect length:" + num + "]");
            }

            if (num == len) {
                return val;
            }

            StringBuilder filledVal = new StringBuilder();
            for (int i = 0; i < (num - len); i++) {  // (7)
                filledVal.append(" ");
            }
            filledVal.append(val);

            return filledVal.toString();
        }
    }

表 103. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `FieldExtractor`クラスを実装し、`extract`メソッドをオーバーライドする。 `FieldExtractor`の型引数には変換対象クラスを設定する。
(2) | 変換処理等を行ったデータを格納するためのObject型配列を定義する。
(3) | 引数で受けたitem(処理対象のBean)から値を取得し、適宜変換処理を行い、Object型の配列に格納する。
(4) | 全角文字が含まれるフィールドに対してフォーマット処理を行う。 フォーマット処理の詳細は(5)、(6)を参照。
(5) | 文字コードに対するバイト数を取得する。
(6) | 取得したバイト数が最大長を超えている場合は、例外をスローする。
(7) | 取得したバイト数をもとにパディング・トリム処理で整形する。 実装例では指定されたバイト数まで文字列の前に空白を付与している。
(8) | 処理結果を保持しているObject型の配列を返す。

##### 5.3.2.3. 単一文字列レコード

単一文字列レコードファイルを扱う場合の定義方法を説明する

###### 5.3.2.3.1. 入力

下記の入力ファイルを読み込むための設定例を示す。

入力ファイル例

    Summary1:4,000,000,000
    Summary2:5,000,000,000
    Summary3:6,000,000,000

上記のファイルを読む込むための設定は以下のとおり。

JavaConfig
Bean定義

    @Bean
    @StepScope
    public FlatFileItemReader<String> reader(
            @Value("#{jobParameters['inputFile']}") File inputFile) {
        final FlatFileItemReader<String> reader = new FlatFileItemReader<>();
        final PassThroughLineMapper lineMapper = new PassThroughLineMapper(); // (4)
        reader.setResource(new FileSystemResource(inputFile)); // (1)
        reader.setEncoding("MS932"); // (2)
        reader.setStrict(true); // (3)
        reader.setLineMapper(lineMapper);
        return reader;
    }

XMLConfig
Bean定義

    <!-- (1) (2) (3) -->
    <bean id="reader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}"
          p:encoding="MS932"
          p:strict="true">
        <property name="lineMapper">  <!-- (4) -->
            <bean class="org.springframework.batch.item.file.mapping.PassThroughLineMapper"/>
        </property>
    </bean>

表 104. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 入力ファイルを設定する。 |  | なし
(2) | encoding | 入力ファイルのエンコーディングを設定する。 未指定の場合のデフォルト値は「UTF-8」。 |  | UTF-8
(3) | strict | trueを設定すると、入力ファイルが存在しない(開けない)場合に例外が発生する。 |  | true
(4) | lineMapper | `org.springframework.batch.item.file.mapping.PassThroughLineMapper`を設定する。 `PassThroughLineMapper`は渡されたレコードをそのまま文字列として返す`LineMapper`の実装クラスである。 |  | なし

###### 5.3.2.3.2. 出力

下記の出力ファイルを書き出すための設定例を示す。

出力ファイル例

    Summary1:4,000,000,000
    Summary2:5,000,000,000
    Summary3:6,000,000,000

JavaConfig
Bean定義

    // Writer
    @Bean
    @StepScope
    public FlatFileItemWriter<SalesPlanDetail> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile) {
        final PassThroughLineAggregator<SalesPlanDetail> lineAggregator = new PassThroughLineAggregator<>(); // (8)
        return new FlatFileItemWriterBuilder<SalesPlanDetail>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .resource(new FileSystemResource(outputFile)) // (1)
                .encoding("MS932") // (2)
                .lineSeparator("\n") // (3)
                .append(true) // (4)
                .shouldDeleteIfExists(false) // (5)
                .shouldDeleteIfEmpty(false) // (6)
                .transactional(false) // (7)
                .lineAggregator(lineAggregator)
                .build();
    }

XMLConfig
Bean定義

    <!-- Writer -->
    <!-- (1) (2) (3) (4) (5) (6) (7) -->
    <bean id="writer"
          class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"
          p:encoding="MS932"
          p:lineSeparator="&#x0A;"
          p:appendAllowed="true"
          p:shouldDeleteIfExists="false"
          p:shouldDeleteIfEmpty="false"
          p:transactional="true">
        <property name="lineAggregator">  <!-- (8) -->
            <bean class="org.springframework.batch.item.file.transform.PassThroughLineAggregator"/>
        </property>
    </bean>

表 105. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 出力ファイルを設定する。 |  | なし
(2) | encoding | 出力ファイルのエンコーディングを設定する。 未指定の場合のデフォルト値は「UTF-8」。 |  | UTF-8
(3) | lineSeparator | レコード区切り(改行コード)を設定する。 |  | システムプロパティの`line.separator`
(4) | appendAllowed | trueの場合、既存のファイルに追記をする。 trueの場合、shouldDeleteIfExistsの設定値は無効化されるため、注意が必要である。 |  | false
(5) | shouldDeleteIfExists | appendAllowedがtrueの場合は、この設定は無効化されるため、値を指定しないことを推奨する。 ファイル出力前に出力ファイルが存在していた場合の動作を設定する。 trueの場合、ファイル出力前に既にファイルが存在すれば削除する。 falseの場合、ファイル出力前に既にファイルが存在すれば例外をスローする。 |  | true
(6) | shouldDeleteIfEmpty | 出力件数が0件であった場合にファイルを削除するかを設定する。 trueの場合、ファイル出力後に出力件数が0件であれば出力対象ファイルを削除する。 他の設定との組み合わせによって意図しない動作をする場合がある。詳細はFlatFileItemWriterのshouldDeleteIfExistsとshouldDeleteIfEmptyの動作の違いを参照。 |  | false
(7) | transactional | トランザクション制御を行うかを設定する。詳細は、トランザクション制御を参照。 |  | true
(8) | lineAggregator | `org.springframework.batch.item.file.transform.PassThroughLineAggregator`を設定する。 `PassThroughLineAggregator`はitem(処理対象のBean)をそのまま文字列へ変換(`item.toString()`を実行)する`LineAggregator`の実装クラスである。 |  | なし

##### 5.3.2.4. ヘッダとフッタ

ヘッダ・フッタがある場合の入出力方法を説明する。

ここでは行数指定にてヘッダ・フッタを読み飛ばす方法を説明する。
ヘッダ・フッタのレコード数が可変であり行数指定ができない場合は、マルチフォーマットの入力を参考に`PatternMatchingCompositeLineMapper`を使用すること。

###### 5.3.2.4.1. 入力

ヘッダの読み飛ばし

ヘッダレコードを読み飛ばす方法には以下に示す2パターンがある。

- `FlatFileItemReader`の`linesToSkip`にファイルの先頭から読み飛ばす行数を設定

- OSコマンドによる前処理でヘッダレコードを取り除く

入力ファイル例

    sales_plan_detail_11
    branchId,year,month,customerId,amount
    000001,2016,1,0000000001,1000000000
    000002,2017,2,0000000002,2000000000
    000003,2018,3,0000000003,3000000000

先頭から2行がヘッダレコードである。

上記のファイルを読む込むための設定は以下のとおり。

JavaConfig
linesToSkipによる読み飛ばし

    @Bean
    @StepScope
    public FlatFileItemReader<SalesPlanDetail> reader(
            @Value("#{jobParameters['inputFile']}") File inputFile) {
        final FlatFileItemReader<SalesPlanDetail> reader = new FlatFileItemReader<>();
        // omitted settings
        reader.setResource(new FileSystemResource(inputFile));
        reader.setLineMapper(lineMapper);
        reader.setLinesToSkip(2); // (1)
        return reader;
    }

XMLConfig
linesToSkipによる読み飛ばし

    <bean id="reader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}"
          p:linesToSkip="2">  <!-- (1) -->
        <property name="lineMapper">
            <!-- omitted settings -->
        </property>
    </bean>

表 106. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | linesToSkip | 読み飛ばすヘッダ行数を設定する。 |  | 0

OSコマンドによる読み飛ばし処理

    # Remove number of lines in header from the top of input file
    tail -n +`expr 2 + 1` input.txt > output.txt

tailコマンドを利用し、入力ファイルinput.txtの3行目以降を取得し、output.txtに出力している。 tailコマンドのオプション`-n +K`に指定する値はヘッダレコードの数+1となるため注意すること。

| ヘッダレコードとフッタレコードを読み飛ばすOSコマンド headコマンドとtailコマンドをうまく活用することでヘッダレコードとフッタレコードを行数指定をして読み飛ばすことが可能である。 ヘッダレコードの読み飛ばし方 tailコマンドをオプション`-n +K`を付与して実行することで、処理対象の`K`行目以降を取得する。 フッタレコードの読み飛ばし方 headコマンドをオプション`-n -K`を付与して実行することで、処理対象の末尾から`K`行目より前を取得する。 ヘッダレコードとフッタレコードをそれぞれ読み飛ばすシェルスクリプト例を下記に示す。 ヘッダ/フッタから指定行数を取り除くシェルスクリプトの例
    #!/bin/bash

    if [ $# -ne 4 ]; then
      echo "The number of arguments must be 4, given is $#." 1>&2
      exit 1
    fi

    # Input file.
    input=$1

    # Output file.
    output=$2

    # Number of lines in header.
    header=$3

    # Number of lines in footer.
    footer=$4

    # Remove number of lines in header from the top of input file
    # and number of lines in footer from the end,
    # and save to output file.
    tail -n +`expr ${header} + 1` ${input} | head -n -${footer} > ${output}
表 107. 引数
項番 | 説明
--- | ---
(1) | 入力ファイル
(2) | 出力ファイル
(3) | 読み飛ばすヘッダの行数
(4) | 読み飛ばすフッタの行数

ヘッダ情報の取り出し

ヘッダレコードを認識し、ヘッダレコードの情報を取り出す方法を示す。

ヘッダ情報の取り出しは以下の要領で実装する。

設定

  - `org.springframework.batch.item.file.LineCallbackHandler`の実装クラスにヘッダに対する処理を実装する

    - `LineCallbackHandler#handleLine()`内で取得したヘッダ情報を`stepExecutionContext`に格納する

  - `FlatFileItemReader`の`skippedLinesCallback`に`LineCallbackHandler`の実装クラスを設定する

  - `FlatFileItemReader`の`linesToSkip`にヘッダの行数を指定する

ファイル読み込みおよびヘッダ情報の取り出し

  - `linesToSkip`の設定によってスキップされるヘッダレコード1行ごとに`LineCallbackHandler#handleLine()`が呼び出される

    - ヘッダ情報が`stepExecutionContext`に格納される

取得したヘッダ情報を利用する

  - ヘッダ情報を`stepExecutionContext`から取得してデータ部の処理で利用する

ヘッダレコードの情報を取り出す際の実装例を示す。

JavaConfig
Bean定義

    @Bean
    public HoldHeaderLineCallbackHandler lineCallbackHandler() {
        return new HoldHeaderLineCallbackHandler();
    }

    @Bean
    @StepScope
    public FlatFileItemReader<SalesPlanDetail> reader(
            @Value("#{jobParameters['inputFile']}") File inputFile,
            HoldHeaderLineCallbackHandler lineCallbackHandler) {
        final FlatFileItemReader<SalesPlanDetail> reader = new FlatFileItemReader<>();
        // omitted settings
        reader.setLinesToSkip(2); // (1)
        reader.setSkippedLinesCallback(lineCallbackHandler); // (2)
        reader.setResource(new FileSystemResource(inputFile));
        reader.setLineMapper(lineMapper);
        return reader;
    }

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<SalesPlanDetail> reader,
                       ItemWriter<SalesPlanDetail> writer,
                       LoggingHeaderRecordItemProcessor processor,
                       LoggingItemReaderListener loggingItemReaderListener,
                       HoldHeaderLineCallbackHandler lineCallbackHandler) {
        return new StepBuilder("jobReadCsvSkipAndReferHeader.step01",
                jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(10, transactionManager)
                .reader(reader)
                .processor(processor)
                .listener(loggingItemReaderListener)
                .listener(lineCallbackHandler) // (3)
                .writer(writer)
                .build();
    }

    @Bean
    public Job jobReadCsvSkipAndReferHeader(JobRepository jobRepository,
                                            Step step01,
                                            JobExecutionLoggingListener listener) {
        return new JobBuilder("jobReadCsvSkipAndReferHeader", jobRepository)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig
Bean定義

    <bean id="lineCallbackHandler"
          class="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.fileaccess.module.HoldHeaderLineCallbackHandler"/>

    <!-- (1) (2) -->
    <bean id="reader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:linesToSkip="2"
          p:skippedLinesCallback-ref="lineCallbackHandler"
          p:resource="file:#{jobParameters['inputFile']}">
        <property name="lineMapper">
            <!-- omitted settings -->
        </property>
    </bean>

    <batch:job id="jobReadCsvSkipAndReferHeader" job-repository="jobRepository">
        <batch:step id="jobReadCsvSkipAndReferHeader.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="reader"
                             processor="loggingHeaderRecordItemProcessor"
                             writer="writer" commit-interval="10"/>
                <batch:listeners>
                    <batch:listener ref="lineCallbackHandler"/>  <!-- (3) -->
                </batch:listeners>
            </batch:tasklet>
        </batch:step>
    </batch:job>

表 108. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | linesToSkip | 読み飛ばすヘッダ行数を設定する。 |  | 0
(2) | skippedLinesCallback | `LineCallbackHandler`の実装クラスを設定する。 実装例は後述する。 |  | なし
(3) | listener | `StepExecutionListener`の実装クラスを設定する。 `FlatFileItemReader`の`skippedLinesCallback`に指定する`LineCallbackHandler`は自動で`Listener`として登録されないため設定が必須となる。 詳しい理由は後述する。 |  | なし

リスナー設定について

下記の2つの場合は自動で`Listener`として登録されないため、ジョブ定義時に`listenerメソッド/<batch:listeners>`にも定義を追加する必要がある。
(リスナの定義を追加しないと、`StepExecutionListener#beforeStep()`が実行されない)

- `FlatFileItemReader`の`skippedLinesCallback`に指定する`LineCallbackHandler`の`StepExecutionListener`

- `Tasklet`の実装クラスに実装する`StepExecutionListener`

JavaConfig

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<SalesPlanDetail> reader,
                       ItemWriter<SalesPlanDetail> writer,
                       LoggingHeaderRecordItemProcessor processor,
                       LoggingItemReaderListener loggingItemReaderListener,
                       HoldHeaderLineCallbackHandler lineCallbackHandler) {
        return new StepBuilder("jobReadCsvSkipAndReferHeader.step01",
                jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(10, transactionManager)
                .reader(reader)
                .processor(processor)
                .listener(loggingItemReaderListener)
                // manantory
                .listener(lineCallbackHandler)
                .writer(writer)
                .build();
    }

    @Bean
    public Job jobReadCsvSkipAndReferHeader(JobRepository jobRepository,
                                            Step step01,
                                            JobExecutionLoggingListener listener) {
        return new JobBuilder("jobReadCsvSkipAndReferHeader", jobRepository)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig

    <batch:job id="jobReadCsvSkipAndReferHeader" job-repository="jobRepository">
        <batch:step id="jobReadCsvSkipAndReferHeader.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="reader"
                             processor="loggingHeaderRecordItemProcessor"
                             writer="writer" commit-interval="10"/>
                <batch:listeners>
                    <batch:listener ref="loggingItemReaderListener"/>
                    <!-- mandatory -->
                    <batch:listener ref="lineCallbackHandler"/>
                </batch:listeners>
            </batch:tasklet>
        </batch:step>
    </batch:job>

`LineCallbackHandler`は以下の要領で実装する。

- `StepExecutionListener#beforeStep()`の実装

  - 下記のいずれかの方法で`StepExecutionListener#beforeStep()`を実装する

    - `StepExecutionListener`クラスを実装し、`beforeStep`メソッドをオーバーライドする

    - beforeStepメソッドを実装し、`@BeforeStep`アノテーションを付与する

  - beforeStepメソッドにて`StepExecution`を取得してクラスフィールドに保持する

- `LineCallbackHandler#handleLine()`の実装

  - `LineCallbackHandler`クラスを実装し、`handleLine`メソッドをオーバーライドする

    - `handleLine`メソッドはスキップする1行ごとに1回呼ばれる点に注意すること。

  - `StepExecution`から`stepExecutionContext`を取得し、`stepExecutionContext`にヘッダ情報を格納する。

LineCallbackHandlerの実装例

    @Component
    public class HoldHeaderLineCallbackHandler implements LineCallbackHandler {  // (1)
        private StepExecution stepExecution;  // (2)

        @BeforeStep  // (3)
        public void beforeStep(StepExecution stepExecution) {
            this.stepExecution = stepExecution;  // (4)
        }

        @Override  // (5)
        public void handleLine(String line) {
            this.stepExecution.getExecutionContext().putString("header", line);  // (6)
        }
    }

表 109. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `LineCallbackHandler`クラスを実装し、`handleLine`メソッドをオーバーライドする。
(2) | `StepExecution`を保持するためのフィールドを定義する。
(3) | `beforeStep`メソッドを実装し、`@BeforeStep`アノテーションを付与する。 シグネチャは`void beforeStep(StepExecution stepExecution)`とする。 `StepExecutionListener`クラスを実装し、`beforeStep`メソッドをオーバーライドする方法でもよい。
(4) | `StepExecution`を取得してクラスフィールドに保持する。
(5) | `LineCallbackHandler`クラスを実装し、`handleLine`メソッドをオーバーライドする。
(6) | `StepExecution`から`stepExecutionContext`を取得し、`header`というキーを指定して`stepExecutionContext`にヘッダ情報を格納する。 ここでは簡単のため、スキップする2行のうち、最後の1行だけを格納している。

ヘッダ情報を`stepExecutionContext`から取得してデータ部の処理で利用する例を示す。
`ItemProcessor`にてヘッダ情報を利用する場合を例にあげて説明する。
他のコンポーネントでヘッダ情報を利用する際も同じ要領で実現することができる。

ヘッダ情報を利用する処理は以下の要領で実装する。

- `LineCallbackHandler`の実装例と同様に`StepExecutionListener#beforeStep()`を実装する

- `beforeStep`メソッドにて`StepExecution`を取得してクラスフィールドに保持する

- `StepExecution`から`stepExecutionContext`およびヘッダ情報を取得して利用する

ヘッダ情報の利用例

    @Component
    public class LoggingHeaderRecordItemProcessor implements
            ItemProcessor<SalesPlanDetail, SalesPlanDetail> {
        private StepExecution stepExecution;  // (1)

        @BeforeStep  // (2)
        public void beforeStep(StepExecution stepExecution) {
            this.stepExecution = stepExecution;  // (3)
        }

        @Override
        public SalesPlanDetail process(SalesPlanDetail item) throws Exception {
            String headerData = this.stepExecution.getExecutionContext()
                    .getString("header");  // (4)
            // omitted business logic
            return item;
        }
    }

表 110. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `StepExecution`を保持するためのフィールドを定義する。
(2) | `beforeStep`メソッドを実装し、`@BeforeStep`アノテーションを付与する。 シグネチャは`void beforeStep(StepExecution stepExecution)`とする。 `StepExecutionListener`クラスを実装し、`beforeStep`メソッドをオーバーライドする方法でもよい。
(3) | `StepExecution`を取得してクラスフィールドに保持する。
(4) | `StepExecution`から`stepExecutionContext`を取得し、`header`というキーを指定して`stepExecutionContext`からヘッダ情報を取得する。

| Job/StepのExecutionContextの使用について ヘッダ(フッタ)情報の取出しでは、読み込んだヘッダ情報を`StepExecution`の`ExecutionContext`に格納しておき、使用する際に`ExecutionContext`から取り出す方式をとる。 下記の例では1つのステップ内でヘッダ情報の取得および利用を行うため`StepExecution`の`ExecutionContext`へヘッダ情報を格納している。 ヘッダ情報の取得および利用にてステップが分かれる場合は`JobExecution`の`ExecutionContext`を利用すること。 Job/Stepの`ExecutionContext`に関する詳細は、Spring Batchのアーキテクチャを参照。

フッタの読み飛ばし

TERASOLUNA Batch 5.xおよびMacchinetta Batch 2.xでは、フッタレコードの読み飛ばし機能は提供していないため、OSコマンドで対応する。

入力ファイル例

    000001,2016,1,0000000001,1000000000
    000002,2017,2,0000000002,2000000000
    000003,2018,3,0000000003,3000000000
    number of items,3
    total of amounts,6000000000

末尾から2行がフッタレコードである。

上記のファイルを読む込むための設定は以下のとおり。

OSコマンドによる読み飛ばし処理

    $ # Remove number of lines in footer from the end of input file
    $ head -n -2 input.txt > output.txt

headコマンドを利用し、入力ファイルinput.txtの末尾から2行目より前を取得し、output.txtに出力している。

フッタ情報の取り出し

TERASOLUNA Batch 5.xおよびMacchinetta Batch 2.xでは、フッタレコードの読み飛ばし機能、フッタ情報の取得機能は提供していない。

そのため、処理を下記ようにOSコマンドによる前処理と2つのステップに分割することで対応する。

- OSコマンドによってフッタレコードを分割する

- 1つめのステップにてフッタレコードを読み込み、フッタ情報を`ExecutionContext`に格納する

- 2つめのステップにて`ExecutionContext`からフッタ情報を取得し、利用する

フッタ情報を取り出しは以下の要領で実装する。

OSコマンドによるフッタレコードの分割

  - OSコマンドを利用して入力ファイルをフッタ部とフッタ部以外に分割する

1つめのステップでフッタレコードを読み込み、フッタ情報を取得する

  - フッタレコードを読み込み`jobExecutionContext`に格納する

    - フッタ情報の格納と利用にてステップが異なるため、`jobExecutionContext`に格納する。

    - `jobExecutionContext`を利用する方法は、JobとStepのスコープに関する違い以外は、ヘッダ情報の取り出しにて説明した`stepExecutionContext`と同様である。

2つめのステップにて取得したフッタ情報を利用する

  - フッタ情報を`jobExecutionContext`から取得してデータ部の処理で利用する

以下に示すファイルのフッタ情報を取り出して利用する場合を例にあげて説明する。

入力ファイル例

    000001,2016,1,0000000001,1000000000
    000002,2017,2,0000000002,2000000000
    000003,2018,3,0000000003,3000000000
    number of items,3
    total of amounts,6000000000

末尾から2行がフッタレコードである。

OSコマンドによるフッタレコードの分割

上記のファイルをOSコマンドを利用してフッタ部とフッタ部以外に分割する設定は以下のとおり。

OSコマンドによる読み飛ばし処理

    $ # Extract non-footer record from input file and save to output file.
    $ head -n -2 input.txt > input_data.txt

    $ # Extract footer record from input file and save to output file.
    $ tail -n 2 input.txt > input_footer.txt

headコマンドを利用し、入力ファイルinput.txtのフッタ部以外をinput_data.txtへ、フッタ部をinput_footer.txtに出力している。

出力ファイル例は以下のとおり。

出力ファイル例(input_data.txt)

    000001,2016,1,0000000001,1000000000
    000002,2017,2,0000000002,2000000000
    000003,2018,3,0000000003,3000000000

出力ファイル例(input_footer.txt)

    number of items,3
    total of amounts,6000000000

フッタ情報の取得、利用

OSコマンドにて分割したフッタレコードからフッタ情報を取得、利用する方法を説明する。

フッタレコードを読み込むステップを前処理として主処理とステップを分割している。
ステップの分割に関する詳細は、フロー制御を参照。

下記の例ではフッタ情報を取得し、`jobExecutionContext`へフッタ情報を格納するまでの例を示す。
`jobExecutionContext`からフッタ情報を取得し利用する方法はヘッダ情報の取り出しと同じ要領で実現可能である。

データレコードの情報を保持するクラス

    public class SalesPlanDetail {

        private String branchId;
        private int year;
        private int month;
        private String customerId;
        private BigDecimal amount;

        // omitted getter/setter
    }

フッタレコードの情報を保持するクラス

    public class SalesPlanDetailFooter implements Serializable {

        // omitted serialVersionUID

        private String name;
        private String value;

        // omitted getter/setter
    }

下記の要領でBean定義を行う。

- フッタレコードを読み込む`ItemReader`を定義する

- データレコードを読み込む`ItemReader`を定義する

- フッタレコードを取得するビジネスロジックを定義する

  - 下記の例では`Tasklet`の実装クラスで実現している

- ジョブを定義する

  - フッタ情報を取得する前処理ステップとデータレコードを読み込み主処理を行うステップを定義する

JavaConfig
Bean定義

    // (1)
    @Bean
    @StepScope
    public FlatFileItemReader<SalesPlanDetailFooter> footerReader(
            @Value("#{jobParameters['footerInputFile']}") File inputFile) {
        // omitted other settings
        final DefaultLineMapper<SalesPlanDetailFooter> lineMapper = new DefaultLineMapper<>();
        // omitted other settings
        reader.setResource(new FileSystemResource(inputFile));
        reader.setLineMapper(lineMapper);
        return reader;
    }

    // (2)
    @Bean
    @StepScope
    public FlatFileItemReader<SalesPlanDetail> dataReader(
            @Value("#{jobParameters['dataInputFile']}") File inputFile) {
        // omitted other settings
        final DefaultLineMapper<SalesPlanDetail> lineMapper = new DefaultLineMapper<>();
        // omitted other settings
        reader.setResource(new FileSystemResource(inputFile));
        reader.setLineMapper(lineMapper);
        return reader;
    }

    @Bean
    @StepScope
    public FlatFileItemWriter<SalesPlanDetail> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile,
            WriteFooterFlatFileFooterCallback writeFooterFlatFileFooterCallback) {
        // omitted other settings
    }

    // Tasklet for reading footer records
    @Bean
    @JobScope
    public ReadFooterTasklet readFooterTasklet() {
        return new ReadFooterTasklet();
    }

    // (3)
    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ReadFooterTasklet readFooterTasklet) {
        return new StepBuilder("jobReadAndWriteCsvWithFooter.step01", jobRepository)
                .tasklet(readFooterTasklet, transactionManager)
                .build();
    }

    // (4)
    @Bean
    public Step step02(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<SalesPlanDetail> dataReader,
                       ItemWriter<SalesPlanDetail> writer) {
        return new StepBuilder("jobReadAndWriteCsvWithFooter.step02", jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(10, transactionManager)
                .reader(dataReader)
                .writer(writer)
                .build();
    }

    @Bean
    public Job jobReadAndWriteCsvWithFooter(JobRepository jobRepository,
                                            Step step01,
                                            Step step02,
                                            JobExecutionLoggingListener jobExecutionLoggingListener,
                                            ReadFooterTasklet readFooterTasklet,
                                            WriteFooterFlatFileFooterCallback writeFooterFlatFileFooterCallback) {
        return new JobBuilder("jobReadAndWriteCsvWithFooter", jobRepository)
                .start(step01)
                .next(step02)
                .listener(jobExecutionLoggingListener)
                .listener(readFooterTasklet) // (5)
                .listener(writeFooterFlatFileFooterCallback)
                .build();
    }

XMLConfig
Bean定義

    <!-- ItemReader for reading footer records -->
    <!-- (1) -->
    <bean id="footerReader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['footerInputFile']}">
        <property name="lineMapper">
            <!-- omitted other settings -->
        </property>
    </bean>

    <!-- ItemReader for reading data records -->
    <!-- (2) -->
    <bean id="dataReader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['dataInputFile']}">
        <property name="lineMapper">
            <!-- omitted other settings -->
        </property>
    </bean>

    <bean id="writer"
          class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step">
      <!-- omitted settings -->
    </bean>

    <!-- Tasklet for reading footer records -->
    <bean id="readFooterTasklet"
          class="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.fileaccess.module.ReadFooterTasklet"/>

    <batch:job id="jobReadAndWriteCsvWithFooter" job-repository="jobRepository">
        <!-- (3) -->
        <batch:step id="jobReadAndWriteCsvWithFooter.step01"
                next="jobReadAndWriteCsvWithFooter.step02">
            <batch:tasklet ref="readFooterTasklet"
                           transaction-manager="jobTransactionManager"/>
        </batch:step>
        <!-- (4) -->
        <batch:step id="jobReadAndWriteCsvWithFooter.step02">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="dataReader"
                             writer="writer" commit-interval="10"/>
            </batch:tasklet>
        </batch:step>
        <batch:listeners>
            <batch:listener ref="readFooterTasklet"/> <!-- (5) -->
        </batch:listeners>
    </batch:job>

表 111. 設定内容の項目一覧
項番 | 項目 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | footerReader | フッタレコードを保持するファイルを読み込むための`ItemReader`を定義する。 フッタ情報を取得するステップで実行される`readFooterTasklet`にてインジェクトして使用する。 |  |
(2) | dataReader | データレコードを保持するファイルを読み込むための`ItemReader`を定義する。 |  |
(3) | 前処理ステップ | フッタ情報を取得するステップを定義する。 処理は`readFooterTasklet`に実装している。実装例は後述する。 |  |
(4) | 主処理ステップ | データ情報を取得するとともにフッタ情報を利用するステップを定義する。 `reader`には`dataReader`を使用する。 例ではフッタ情報を`jobExecutionContext`から取得し利用する処理(`ItemProcessor`等)は実装していない。 フッタ情報を取得し利用する方法はヘッダ情報の取り出しと同じ要領で実現可能である。 |  |
(5) | listeners | `readFooterTasklet`を設定する。 この設定を行わないと`readFooterTasklet`内に実装する`JobExecutionListener#beforeJob()`が実行されない。 詳しい理由は、ヘッダ情報の取り出しを参照。 |  | なし

フッタレコードを保持するファイルを読み込み、`jobExecutionContext`に格納する処理を行う処理の例を示す。

`Tasklet`の実装クラスとして実現する際の要領は以下のとおり。

- Bean定義した`footerReader`を`@Inject`アノテーションと`@Named`アノテーションを使用し名前指定でインジェクトする。

- 読み込んだフッタ情報を`jobExecutionContext`に格納する

  - 実現方法はヘッダ情報の取り出しと同様である

フッタ情報の取得

    public class ReadFooterTasklet implements Tasklet {
        // (1)
        @Inject
        @Named("footerReader")
        ItemStreamReader<SalesPlanDetailFooter> itemReader;

        private JobExecution jobExecution;

        @BeforeJob
        public void beforeJob(JobExecution jobExecution) {
            this.jobExecution = jobExecution;
        }

        @Override
        public RepeatStatus execute(StepContribution contribution,
                ChunkContext chunkContext) throws Exception {
            ArrayList<SalesPlanDetailFooter> footers = new ArrayList<>();

            // (2)
            itemReader.open(chunkContext.getStepContext().getStepExecution()
                    .getExecutionContext());

            SalesPlanDetailFooter footer;
            while ((footer = itemReader.read()) != null) {
                footers.add(footer);
            }

            // (3)
            jobExecution.getExecutionContext().put("footers", footers);

            return RepeatStatus.FINISHED;
        }
    }

表 112. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | Bean定義した`footerReader`を`@Inject`アノテーションと`@Named`アノテーションを使用し名前指定でインジェクトする。
(2) | `footerReader`を使用してフッタレコードを保持したファイルを読み込みフッタ情報を取得する。 `Tasklet`の実装クラス内でBean定義した`ItemReader`を使用する方法はタスクレット指向ジョブの作成を参照。
(3) | `JobExecution`から`jobExecutionContext`を取得し、`footers`というキーを指定して`jobExecutionContext`へフッタ情報を格納する。

###### 5.3.2.4.2. 出力

ヘッダ情報の出力

フラットファイルでヘッダ情報を出力する際は以下の要領で実装する。

- `org.springframework.batch.item.file.FlatFileHeaderCallback`の実装を行う

- 実装した`FlatFileHeaderCallback`を`FlatFileItemWriter`の`headerCallback`に設定する

  - `headerCallback`を設定すると`FlatFileItemWriter`の出力処理で、最初に`FlatFileHeaderCallback#writeHeader()`が実行される

`FlatFileHeaderCallback`は以下の要領で実装する。

- `FlatFileHeaderCallback`クラスを実装し、`writeHeader`メソッドをオーバーライドする

- 引数で受ける`Writer`を用いてヘッダ情報を出力する。

下記に`FlatFileHeaderCallback`クラスの実装例を示す。

FlatFileHeaderCallbackの実装例

    @Component
    // (1)
    public class WriteHeaderFlatFileFooterCallback implements FlatFileHeaderCallback {
        @Override
        public void writeHeader(Writer writer) throws IOException {
            // (2)
            writer.write("omitted");
        }
    }

表 113. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `FlatFileHeaderCallback`クラスを実装し、`writeHeader`メソッドをオーバーライドする。
(2) | 引数で受ける`Writer`を用いてヘッダ情報を出力する。 `FlatFileHeaderCallback#writeHeader()`の実行直後に`FlatFileItemWriter`が出力する処理を実行する。 そのため、ヘッダ情報末尾の改行は出力不要である。 出力される改行は、Bean定義時に`FlatFileItemWriter`に指定したものである。

JavaConfig
Bean定義

    @Bean
    @StepScope
    public FlatFileItemWriter<Customer> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile,
            WriteHeaderFlatFileFooterCallback writeHeaderFlatFileFooterCallback) {
        // omitted settings
        return new FlatFileItemWriterBuilder<Customer>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .headerCallback(writeHeaderFlatFileFooterCallback) // (1)
                .lineSeparator("\n") // (2)
                .resource(new FileSystemResource(outputFile))
                .transactional(false)
                .lineAggregator(lineAggregator)
                .build();
    }

XMLConfig
Bean定義

    <!-- (1) (2) -->
    <bean id="writer"
          class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
          p:headerCallback-ref="writeHeaderFlatFileFooterCallback"
          p:lineSeparator="&#x0A;"
          p:resource="file:#{jobParameters['outputFile']}">
        <property name="lineAggregator">
            <!-- omitted settings -->
        </property>
    </bean>

表 114. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | headerCallback | `FlatFileHeaderCallback`の実装クラスを設定する。 |  |
(2) | lineSeparator | レコード区切り(改行コード)を設定する。 |  | システムプロパティの`line.separator`

| FlatFileHeaderCallback実装時にヘッダ情報末尾の改行は出力不要 `FlatFileItemWriter`内で`FlatFileHeaderCallback#writeHeader()`の実行直後にBean定義時に指定した改行を出力する処理が実行されるため、ヘッダ情報末尾の改行は出力不要である。

フッタ情報の出力

フラットファイルでフッタ情報を出力する際は以下の要領で実装する。

- `org.springframework.batch.item.file.FlatFileFooterCallback`の実装を行う

- 実装した`FlatFileFooterCallback`を`FlatFileItemWriter`の`footerCallback`に設定する

  - `footerCallback`を設定すると`FlatFileItemWriter`の出力処理で、最後に`FlatFileFooterCallback#writeFooter()`が実行される

フラットファイルでフッタ情報を出力する方法について説明する。

`FlatFileFooterCallback`は以下の要領で実装する。

- 引数で受ける`Writer`を用いてフッタ情報を出力する。

- `FlatFileFooterCallback`クラスを実装し、`writeFooter`メソッドをオーバーライドする

下記にJobの`ExecutionContext`からフッタ情報を取得し、ファイルへ出力する`FlatFileFooterCallback`クラスの実装例を示す。

フッタレコードの情報を保持するクラス

    public class SalesPlanDetailFooter implements Serializable {

        // omitted serialVersionUID

        private String name;
        private String value;

        // omitted getter/setter
    }

FlatFileFooterCallbackの実装例

    @Component
    public class WriteFooterFlatFileFooterCallback implements FlatFileFooterCallback {  // (1)
        private JobExecution jobExecution;

        @BeforeJob
        public void beforeJob(JobExecution jobExecution) {
            this.jobExecution = jobExecution;
        }

        @Override
        public void writeFooter(Writer writer) throws IOException {
            @SuppressWarnings("unchecked")
            ArrayList<SalesPlanDetailFooter> footers = (ArrayList<SalesPlanDetailFooter>) this.jobExecution.getExecutionContext().get("footers");  // (2)

            BufferedWriter bufferedWriter = new BufferedWriter(writer);  // (3)
            // (4)
            for (SalesPlanDetailFooter footer : footers) {
                bufferedWriter.write(footer.getName() +" is " + footer.getValue());
                bufferedWriter.newLine();
                bufferedWriter.flush();
            }
        }
    }

表 115. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `FlatFileFooterCallback`クラスを実装し、`writeFooter`メソッドをオーバーライドする。
(2) | Jobの`ExecutionContext`から`footers`というkeyを指定してフッタ情報を取得する。 例ではArrayListで複数のフッタ情報を取得している。
(3) | 例では改行の出力に`BufferedWriter.newLine()`を使用するため、引数で受ける`Writer`を引数として`BufferedWriter`を生成する。
(4) | 引数で受ける`Writer`を用いてフッタ情報を出力する。

JavaConfig
Bean定義

    @Bean
    @StepScope
    public FlatFileItemWriter<SalesPlanDetail> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile,
            WriteFooterFlatFileFooterCallback writeFooterFlatFileFooterCallback) {
        // omitted settings
        return new FlatFileItemWriterBuilder<SalesPlanDetail>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .resource(new FileSystemResource(outputFile))
                .footerCallback(writeFooterFlatFileFooterCallback) // (1)
                .transactional(false)
                .lineAggregator(lineAggregator)
                .build();
    }

XMLConfig
Bean定義

    <bean id="writer"
          class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"
          p:footerCallback-ref="writeFooterFlatFileFooterCallback">  <!-- (1) -->
        <property name="lineAggregator">
            <!-- omitted settings -->
        </property>
    </bean>

表 116. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | footerCallback | `FlatFileFooterCallback`の実装クラスを設定する。 |  |

##### 5.3.2.5. 複数ファイル

複数ファイルを扱う場合の定義方法を説明する。

###### 5.3.2.5.1. 入力

同一レコード形式の複数ファイルを読み込む場合は、`org.springframework.batch.item.file.MultiResourceItemReader`を利用する。
`MultiResourceItemReader`は指定された`ItemReader`を使用し正規表現で指定された複数のファイルを読み込むことができる。

`MultiResourceItemReader`は以下の要領で定義する。

- `MultiResourceItemReader`のBeanを定義する

  - `resources`プロパティに読み込み対象のファイルを指定する

    - 正規表現で複数ファイルを指定する

  - `delegate`プロパティにファイル読み込みに利用する`ItemReader`を指定する

下記に示す複数のファイルを読み込む`MultiResourceItemReader`の定義例は以下のとおりである。

読み込み対象ファイル(ファイル名)

    sales_plan_detail_01.csv
    sales_plan_detail_02.csv
    sales_plan_detail_03.csv

JavaConfig
Bean定義

    @Bean
    @StepScope
    public MultiResourceItemReader<SalesPlanDetail> multiResourceReader(
            @Value("file:input/sales_plan_detail_*.csv") Resource[] resources,
            FlatFileItemReader<SalesPlanDetail> reader) {
        return new MultiResourceItemReaderBuilder<SalesPlanDetail>()
        .name(ClassUtils.getShortName(MultiResourceItemReader.class))
        .resources(resources) // (1)
        .delegate(reader) // (2)
        .build();
    }

    // (3)
    @Bean
    public FlatFileItemReader<SalesPlanDetail> reader() {
        final FlatFileItemReader<SalesPlanDetail> reader = new FlatFileItemReader<>();
        // omitted settings
        reader.setLineMapper(lineMapper);
        return reader;
    }

XMLConfig
Bean定義

    <!-- (1) (2) -->
    <bean id="multiResourceReader"
          class="org.springframework.batch.item.file.MultiResourceItemReader"
          scope="step"
          p:resources="file:input/sales_plan_detail_*.csv"
          p:delegate-ref="reader"/>

    <!-- (3) -->
    <bean id="reader"
          class="org.springframework.batch.item.file.FlatFileItemReader">
        <property name="lineMapper">
          <!-- omitted settings -->
        </property>
    </bean>

表 117. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 正規表現で複数の入力ファイルを設定する。 |  | なし
(2) | delegate | 実際にファイルを読み込み処理する`ItemReader`を設定する。 |  | なし
(3) | 実際にファイルを読み込み処理する`ItemReader` | `resource`プロパティは、`MultiResourceItemReader`から自動的に設定されるため、Bean定義に設定は不要である。 |  |

| MultiResourceItemReaderが使用するItemReaderにresourceの指定は不要である `MultiResourceItemReader`から委譲される`ItemReader`の`resource`は、`MultiResourceItemReader`から自動的に設定されるため、Bean定義に設定は不要である。

###### 5.3.2.5.2. 出力

複数ファイルを扱う場合の定義方法を説明する。

一定の件数ごとに異なるファイルへ出力する場合は、`org.springframework.batch.item.file.MultiResourceItemWriter`を利用する。

`MultiResourceItemWriter`は指定された`ItemWriter`を使用して指定した件数ごとに複数ファイルへ出力することができる。
出力対象のファイル名は重複しないように一意にする必要があるが、そのための仕組みとして`ResourceSuffixCreator`が提供されている。
`ResourceSuffixCreator`はファイル名が一意となるようなサフィックスを生成するクラスである。

たとえば、出力対象ファイルを`outputDir/customer_list_01.csv`(`01`の部分は連番)というファイル名にしたい場合は下記のように設定する。

- `MultiResourceItemWriter`に`outputDir/customer_list_`と設定する

- サフィックス`01.csv`(`01`の部分は連番)を生成する処理を`ResourceSuffixCreator`に実装する

  - 連番は`MultiResourceItemWriter`から自動で増分されて渡される値を使用することができる

- 実施に使用される`ItemWriter`には`outputDir/customer_list_01.csv`が設定される

`MultiResourceItemWriter`は以下の要領で定義する。`ResourceSuffixCreator`の実装方法は後述する。

- `ResourceSuffixCreator`の実装クラスを定義する

- `MultiResourceItemWriter`のBeanを定義する

  - `resources`プロパティに出力対象のファイルを指定する

    - `ResourceSuffixCreator`の実装クラスで付与するサフィックスまでを設定

  - `resourceSuffixCreator`プロパティにサフィックスを生成する`ResourceSuffixCreator`の実装クラスを指定する

  - `delegate`プロパティにファイル読み込みに利用する`ItemWriter`を指定する

  - `itemCountLimitPerResource`プロパティに1ファイルあたりの出力件数を指定する

JavaConfig
Bean定義

    @Bean
    @StepScope
    public MultiResourceItemWriter<Customer> multiResourceItemWriter(
            @Value("#{jobParameters['outputDir']}") File outputDir,
            CustomerListResourceSuffixCreator customerListResourceSuffixCreator,
            FlatFileItemWriter<Customer> writer) {
        return new MultiResourceItemWriterBuilder<Customer>()
                .name(ClassUtils.getShortName(MultiResourceItemWriter.class))
                .resource(new FileSystemResource(outputDir)) // (1)
                .resourceSuffixCreator(customerListResourceSuffixCreator) // (2)
                .delegate(writer) // (3)
                .itemCountLimitPerResource(4) // (4)
                .build();
    }

    // (5)
    @Bean
    public FlatFileItemWriter<Customer> writer() {
        // omitted settings
        final DelimitedLineAggregator<Customer> lineAggregator = new DelimitedLineAggregator<>();
        lineAggregator.setFieldExtractor(fieldExtractor);
        return new FlatFileItemWriterBuilder<Customer>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .transactional(false)
                .lineAggregator(lineAggregator)
                .build();
    }

    // (6)
    @Bean
    public CustomerListResourceSuffixCreator customerListResourceSuffixCreator() {
        return new CustomerListResourceSuffixCreator();
    }

XMLConfig
Bean定義

    <!-- (1) (2) (3) (4) -->
    <bean id="multiResourceItemWriter"
          class="org.springframework.batch.item.file.MultiResourceItemWriter"
          scope="step"
          p:resource="file:#{jobParameters['outputDir']}"
          p:resourceSuffixCreator-ref="customerListResourceSuffixCreator"
          p:delegate-ref="writer"
          p:itemCountLimitPerResource="4"/>

    <!-- (5) -->
    <bean id="writer"
          class="org.springframework.batch.item.file.FlatFileItemWriter">
        <property name="lineAggregator">
            <!-- omitted settings -->
        </property>
    </bean>

    <bean id="customerListResourceSuffixCreator"
          class="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.fileaccess.module.CustomerListResourceSuffixCreator"/>  <!-- (6) -->

表 118. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 出力対象ファイルのサフィックスを付与する前の状態を設定する。 `ItemWriter`には、`MultiResourceItemWriter`が自動でサフィックスを付与したものが設定される。 |  | なし
(2) | resourceSuffixCreator | `ResourceSuffixCreator`の実装クラスを設定する。 デフォルト値は`"." + index`というサフィックスを生成する`org.springframework.batch.item.file.SimpleResourceSuffixCreator`である。 |  | `SimpleResourceSuffixCreator`
(3) | delegate | 実際にファイルを書き込み処理する`ItemWriter`を設定する。 |  | なし
(4) | itemCountLimitPerResource | 1ファイルあたりの出力件数を設定する。 |  | `Integer.MAX_VALUE`
(5) | 実際にファイルを書き込み処理する`ItemWriter` | `resource`プロパティは、`MultiResourceItemWriter`から自動的に設定されるため、Bean定義に設定は不要である。 |  |
(6) | `ResourceSuffixCreator`の実装クラス | サフィックスを生成する`ResourceSuffixCreator`の実装クラスを定義する。 実装方法は後述する。 |  |

| MultiResourceItemWriterが使用するItemWriterにresourceの指定は不要である `MultiResourceItemWriter`から委譲される`ItemWriter`の`resource`は、`MultiResourceItemWriter`から自動的に設定されるため、Bean定義に設定は不要である。

`ResourceSuffixCreator`は以下の要領で実装する。

- `ResourceSuffixCreator`クラスを実装し、`getSuffix`メソッドをオーバーライドする

- 引数で受ける`index`を用いてサフィックスを生成して返り値として返す

  - `index`は初期値`1`で始まり出力対象ファイルごとにインクリメントされる`int`型の値である

ResourceSuffixCreatorの実装例

    // (1)
    public class CustomerListResourceSuffixCreator implements ResourceSuffixCreator {
        @Override
        public String getSuffix(int index) {
            return String.format("%02d", index) + ".csv";  // (2)
        }
    }

表 119. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `ResourceSuffixCreator`クラスを実装し、`getSuffix`メソッドをオーバーライドする。
(2) | 引数で受ける`index`を用いてサフィックスを生成して返り値として返す。 `index`は初期値`1`で始まり出力対象ファイルごとにインクリメントされる`int`型の値である。

##### 5.3.2.6. コントロールブレイク

コントロールブレイクの実現方法について説明する。

コントロールブレイクとは

  コントロールブレイク処理(またはキーブレイク処理)とは、ソート済みのレコードを順次読み込み、 レコード内にある特定の項目(キー項目)が同じレコードを1つのグループとして処理する手法のことを指す。
  主にデータを集計するときに用いられ、 キー項目が同じ値の間は集計を続け、キー項目が異なる値になる際に集計値を出力する、 というアルゴリズムになる。

コントロールブレイク処理をするためには、グループの変わり目を判定するために、レコードを先読みする必要がある。 `org.springframework.batch.item.support.SingleItemPeekableItemReader`を使うことで先読みを実現できる。
また、コントロールブレイクはタスクレットモデルでのみ処理可能とする。 これは、チャンクが前提とする「1行で定義するデータ構造をN行処理する」や「一定件数ごとのトランザクション境界」といった点が、 コントロールブレイクの「グループの変わり目で処理をする」という点と合わないためである。

コントロールブレイク処理の実行タイミングと比較条件を以下に示す。

- 対象レコード処理前にコントロールブレイク実施

  - 前回読み取ったレコードを保持し、前回レコードと現在読み込んだレコードとの比較

- 対象レコード処理後にコントロールブレイク実施

  - `SingleItemPeekableItemReader`により次のレコードを先読みし、次レコードと現在読み込んだレコードとの比較

下記に入力データから処理結果を出力するコントロールブレイクの実装例を示す。

入力データ

    01,2016,10,1000
    01,2016,11,1500
    01,2016,12,1300
    02,2016,12,900
    02,2016,12,1200

処理結果

    Header Branch Id : 01,,,
    01,2016,10,1000
    01,2016,11,1500
    01,2016,12,1300
    Summary Branch Id : 01,,,3800
    Header Branch Id : 02,,,
    02,2016,12,900
    02,2016,12,1200
    Summary Branch Id : 02,,,2100

コントロールブレイクの実装例

    @Component
    public class ControlBreakTasklet implements Tasklet {

        @Inject
        SingleItemPeekableItemReader<SalesPerformanceDetail> reader; // (1)

        @Inject
        ItemStreamWriter<SalesPerformanceDetail> writer;

        @Override
        public RepeatStatus execute(StepContribution contribution,
                ChunkContext chunkContext) throws Exception {

            // omitted.

            SalesPerformanceDetail previousData = null;   // (2)
            BigDecimal summary = new BigDecimal(0);  //(3)

            List<SalesPerformanceDetail> items = new ArrayList<>();   // (4)

            try {
                reader.open(executionContext);
                writer.open(executionContext);

                while (reader.peek() != null) {   // (5)
                    SalesPerformanceDetail data = reader.read(); // (6)

                    // (7)
                    if (isBreakByBranchId(previousData, data)) {
                        SalesPerformanceDetail beforeBreakData =
                                new SalesPerformanceDetail();
                        beforeBreakData.setBranchId("Header Branch Id : "
                                  + currentData.getBranchId());
                        items.add(beforeBreakData);
                    }

                    // omitted.
                    items.add(data);  // (8)

                    SalesPerformanceDetail nextData = reader.peek();  // (9)
                    summary = summary.add(data.getAmount());

                    // (10)
                    SalesPerformanceDetail afterBreakData = null;
                    if (isBreakByBranchId(nextData, data)) {
                        afterBreakData = new SalesPerformanceDetail();
                        afterBreakData.setBranchId("Summary Branch Id : "
                                + currentData.getBranchId());
                        afterBreakData.setAmount(summary);
                        items.add(afterBreakData);
                        summary = new BigDecimal(0);
                        writer.write(new Chunk(items));  // (11)
                        items.clear();
                    }
                    previousData = data;  // (12)
                }
            } finally {
                try {
                    reader.close();
                } catch (ItemStreamException e) {
                }
                try {
                    writer.close();
                } catch (ItemStreamException e) {
                }
            }
            return RepeatStatus.FINISHED;
        }
        // (13)
        private boolean isBreakByBranchId(SalesPerformanceDetail o1,
                SalesPerformanceDetail o2) {
            return (o1 == null || !o1.getBranchId().equals(o2.getBranchId()));
        }
    }

表 120. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `SingleItemPeekableItemReader`をInjectする。
(2) | 前回読み取ったレコードを保持する変数を定義する。
(3) | グループごとの集計値を格納する変数を定義する。
(4) | コントロールブレイクの処理結果を含めたグループ単位のレコードを格納する変数を定義する。
(5) | 入力データが無くなるまで処理を繰り返す。
(6) | 処理対象のレコードを読み込む。
(7) | 対象レコード処理前にコントロールブレイクを実施する。 ここではグループの先頭であれば見出しを設定して、(4)で定義した変数に格納する。
(8) | 対象レコードへの処理結果を(4)で定義した変数に格納する。
(9) | 次のレコードを先読みする。
(10) | 対象レコード処理後にコントロールブレイクを実施する。 ここではグループの末尾であれば集計データをトレーラに設定して、(4)で定義した変数に格納する。
(11) | グループ単位で処理結果を出力する。
(12) | 処理レコードを(2)で定義した変数に格納する。
(13) | キー項目が切り替わったか判定する。

JavaConfig
Bean定義

    // (1)
    @Bean
    public SingleItemPeekableItemReader<SalesPerformanceDetail> reader(
            FlatFileItemReader<SalesPerformanceDetail> delegateReader) {
        return new SingleItemPeekableItemReaderBuilder<SalesPerformanceDetail>()
                .delegate(delegateReader) // (2)
                .build();
    }

    // (3)
    @Bean
    @StepScope
    public FlatFileItemReader<SalesPerformanceDetail> delegateReader(
            @Value("#{jobParameters['inputFile']}") File inputFile) {
        FlatFileItemReader<SalesPerformanceDetail> delegateReader = new FlatFileItemReader<>();
        DefaultLineMapper<SalesPerformanceDetail> lineMapper = new DefaultLineMapper<>();
        DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
        lineTokenizer.setNames("branchId", "year", "month", "customerId", "amount");
        BeanWrapperFieldSetMapper<SalesPerformanceDetail> fieldSetMapper = new BeanWrapperFieldSetMapper<>();
        fieldSetMapper.setTargetType(SalesPerformanceDetail.class);
        lineMapper.setLineTokenizer(lineTokenizer);
        lineMapper.setFieldSetMapper(fieldSetMapper);
        delegateReader.setLineMapper(lineMapper);
        delegateReader.setResource(new FileSystemResource(inputFile));
        return delegateReader;
    }

XMLConfig
Bean定義

    <!-- (1) -->
    <bean id="reader"
          class="org.springframework.batch.item.support.SingleItemPeekableItemReader"
          p:delegate-ref="delegateReader" />  <!-- (2) -->

    <!-- (3) -->
    <bean id="delegateReader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}">
        <property name="lineMapper">
            <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
                <property name="lineTokenizer">
                    <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
                          p:names="branchId,year,month,customerId,amount"/>
                </property>
                <property name="fieldSetMapper">
                    <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
                          p:targetType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.performance.SalesPerformanceDetail"/>
                </property>
            </bean>
        </property>
    </bean>

表 121. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `SingleItemPeekableItemReader`をBean定義する。TaskletへのInject対象。
(2) | `delegate-ref`属性に実際にファイルを読み込むItemReaderのBeanを指定する。
(3) | 実際にファイルを読み込むItemReaderのBeanを定義する。

#### 5.3.3. How to extend

ここでは、以下のケースについて説明する。

- FieldSetMapperの実装

- XMLファイルの入出力

- マルチフォーマットの入出力

##### 5.3.3.1. FieldSetMapperの実装

`FieldSetMapper`を自作で実装する方法について説明する。

`FieldSetMapper`の実装クラスは下記の要領で実装する。

- `FieldSetMapper`クラスを実装し、`mapFieldSet`メソッドをオーバーライドする

- 引数で受けた`FieldSet`から値を取得し、適宜変換処理を行い、変換対象のBeanに格納し返り値として返す

  - `FieldSet`クラスはJDBCにある`ResultSet`クラスのようにインデックスまたは名前と関連付けてデータを保持するクラスである

  - `FieldSet`クラスは`LineTokenizer`によって分割されたレコードの各フィールドの値を保持する

  - インデックスまたは名前を指定して値を格納および取得することができる

下記のような西暦フォーマットのLocalDate型やカンマを含むBigDecimal型の変換を行うファイルを読み込む場合の実装例を示す。

入力ファイル例

    "000001","2016年1月1日","000000001","1,000,000,000"
    "000002","2017年2月2日","000000002","2,000,000,000"
    "000003","2018年3月3日","000000003","3,000,000,000"

表 122. 入力ファイル仕様
項番 | フィールド名 | データ型 | 備考
--- | --- | --- | ---
(1) | branchId | String |
(2) | 日付 | LocalDate | 西暦フォーマット
(3) | customerId | String |
(4) | amount | BigDecimal | カンマを含む

変換対象クラス

    public class UseDateSalesPlanDetail {

        private String branchId;
        private LocalDate date;
        private String customerId;
        private BigDecimal amount;

        // omitted getter/setter
    }

FieldSetMapperの実装例

    @Component
    public class UseDateSalesPlanDetailFieldSetMapper implements FieldSetMapper<UseDateSalesPlanDetail> {  // (1)
        /**
         * {@inheritDoc}
         *
         * @param fieldSet {@inheritDoc}
         * @return Sales performance detail.
         * @throws BindException {@inheritDoc}
         */
        @Override
        public UseDateSalesPlanDetail mapFieldSet(FieldSet fieldSet) throws BindException {
            UseDateSalesPlanDetail item = new UseDateSalesPlanDetail();  // (2)

            item.setBranchId(fieldSet.readString("branchId"));  // (3)

            // (4)
            DateTimeFormatter japaneseFormat = DateTimeFormatter.ofPattern("yyyy年M月d日");
            try {
                item.setDate(LocalDate.parse(fieldSet.readString("date"), japaneseFormat));
            } catch (DateTimeParseException e) {
                // omitted exception handling
            }

            // (5)
            item.setCustomerId(fieldSet.readString("customerId"));

            // (6)
            DecimalFormat decimalFormat = new DecimalFormat();
            decimalFormat.setParseBigDecimal(true);
            try {
                item.setAmount((BigDecimal) decimalFormat.parse(fieldSet.readString("amount")));
            } catch (ParseException e) {
                // omitted exception handling
            }

            return item;  // (7)
        }
    }

表 123. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `FieldSetMapper`クラスを実装し、`mapFieldSet`メソッドをオーバーライドする。 `FieldSetMapper`の型引数には変換対象クラスを設定する。
(2) | 変換処理等を行ったデータを格納するために変換対象クラスの変数を定義する。
(3) | 引数で受けた`FieldSet`から`branchId`を取得し、変換対象クラスの変数へ格納する。 `branchId`は変換処理が不要であるため、変換処理等は行っていない。
(4) | 引数で受けた`FieldSet`から`date`を取得し、変換対象クラスの変数へ格納する。 西暦フォーマットの日付をLocalDate型へ変換するため、`DateTimeFormatter`でフォーマットを指定している。
(5) | 引数で受けた`FieldSet`から`customerId`を取得し、変換対象クラスの変数へ格納する。 `customerId`は変換処理が不要であるため、変換処理等は行っていない。
(6) | 引数で受けた`FieldSet`から`amount`を取得し、変換対象クラスの変数へ格納する。 カンマを含む値をBigDecimal型へ変換するため、`DecimalFormat`を使用している。
(7) | 処理結果を保持している変換対象クラスを返す。

| FieldSetクラスからの値取得 `FieldSet`クラスは、下記のような格納された値を取得するための様々なデータ型に対応したメソッドをもつ。 また、`FieldSet`生成時にフィールドの名前と関連付けられてデータを格納した場合は、名前指定でのデータ取得、名前を指定しない場合ではインデックスを指定してのデータ取得が可能である。 `readString()` `readInt()` `readBigDecimal()` など

##### 5.3.3.2. XMLファイル

XMLファイルを扱う場合の定義方法を説明する。

##### 5.3.3.3. オブジェクト変換ライブラリ

BeanとXML間の変換処理(O/X (Object/XML) マッピング)にはSpring Frameworkが提供するライブラリを使用する。
XMLファイルとオブジェクト間の変換処理を行うライブラリとして、XStreamやJAXBなどを利用した`Marshaller`および`Unmarshaller`を実装クラスが提供されている。
状況に応じて適しているものを使用すること。

JAXBとXStreamを例に特徴と採用する際のポイントを説明する。

JAXB

  - 変換対象のBeanはBean定義にて指定する

  - スキーマファイルを用いたバリデーションを行うことができる

  - 対外的にスキーマを定義しており、入力ファイルの仕様が厳密に決まっている場合に有用である

XStream

  - Bean定義にて柔軟にXMLの要素とBeanのフィールドをマッピングすることができる

  - 柔軟にBeanマッピングする必要がある場合に有用である

なお、以降の説明ではJAXBを利用する例を示す。

###### 5.3.3.3.1. 入出力におけるエンコーディングの仕様

XMLの入出力にはSpring Batchが提供する`org.springframework.batch.item.xml.StaxEventItemReader` および`org.springframework.batch.item.xml.StaxEventItemWriter`を使用する。

これらのコンポーネントのエンコーディングのデフォルト値は以下の表のとおり異なっているため、利用時には注意する必要がある。 デフォルト値の違いによって意図しないエンコーディングで入出力が行われることを防ぐため、 デフォルト値をそのまま使用する意図である場合でも明示的にエンコーディングを設定することを推奨する。

表 124. 各コンポーネントのエンコーディングについての仕様
項番 | コンポーネント名 | エンコーディングの指定方法 | デフォルト値
--- | --- | --- | ---
(1) | `StaxEventItemReader` | Bean定義においてencodingプロパティを設定する。 | UTF-8
(2) | `StaxEventItemWriter` | Bean定義においてencodingプロパティを設定する。 | UTF-8

###### 5.3.3.3.2. 入力

XMLファイルの入力にはSpring Batchが提供する`org.springframework.batch.item.xml.StaxEventItemReader`を使用する。
`StaxEventItemReader`は指定した`Unmarshaller`を使用してXMLファイルをBeanにマッピングすることでXMLファイルを読み込むことができる。

`StaxEventItemReader`は以下の要領で定義する。

- XMLのルート要素となる変換対象クラスに`@XmlRootElement`を付与する

- `StaxEventItemReader`に以下のプロパティを設定する

  - `resource`プロパティに読み込み対象ファイルを設定する

  - `fragmentRootElementName`プロパティにルート要素の名前を設定する

  - `unmarshaller`プロパティに`org.springframework.oxm.jaxb.Jaxb2Marshaller`を設定する

- `Jaxb2Marshaller`には以下のプロパティを設定する

  - `classesToBeBound`プロパティに変換対象のクラスをリスト形式で設定する

  - スキーマファイルを用いたバリデーションを行う場合は、以下に示す2つのプロパティを設定する

    - `schema`プロパティにバリデーションにて使用するスキーマファイルを設定する

    - `validationEventHandler`プロパティにバリデーションにて発生したイベントを処理する`ValidationEventHandler`の実装クラスを設定する

下記の入力ファイルを読み込むための設定例を示す。

依存ライブラリの追加

本ガイドラインの前提においてJAXBを利用する場合は、以下の依存関係が必須である。
アプリケーションの依存ライブラリやバッチから提供されるライブラリにjaxb-implがない場合は、jaxb-implも追加する。

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-oxm</artifactId>
    </dependency>

    <dependency>
        <groupId>jakarta.xml.bind</groupId>
        <artifactId>jakarta.xml.bind-api</artifactId>
    </dependency>

    <dependency>
        <groupId>com.sun.xml.bind</groupId>
        <artifactId>jaxb-impl</artifactId>
        <scope>runtime</scope> <!-- (1) -->
    </dependency>

表 125. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | jaxb-implはアプリケーションの実行時にのみ利用されるため、スコープはruntimeとする。

入力ファイル例

    <?xml version="1.0" encoding="UTF-8"?>
    <records>
        <customer>
            <name>Data Taro</name>
            <phoneNumbers>
                <phone-number>01234567890</phone-number>
            </phoneNumbers>
        </customer>
        <customer>
            <name>Data Jiro</name>
            <phoneNumbers>
                <phone-number>01234567891</phone-number>
                <phone-number>01234567892</phone-number>
            </phoneNumbers>
        </customer>
        <customer>
            <name>Data Hanako</name>
            <phoneNumbers>
                <phone-number>01234567893</phone-number>
                <phone-number>01234567894</phone-number>
            </phoneNumbers>
        </customer>
    </records>

変換対象クラス

    @XmlRootElement  // (1)
    public class Customer {

        private String name;
        private List<PhoneNumber> phoneNumbers = new ArrayList<>();

        @XmlElement  // (2)
        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @XmlElement(name = "phone-number")  // (3)
        @XmlElementWrapper(name = "phoneNumbers")  // (4)
        public List<PhoneNumber> getPhoneNumbers() {
            return phoneNumbers;
        }

        public void setPhoneNumbers(List<PhoneNumber> phoneNumbers) {
            this.phoneNumbers = phoneNumbers;
        }

        // omitted.
    }

    @XmlType(name = "phone-number") 　// (5)
    public class PhoneNumber {

        private String phoneNumber;

        @XmlValue 　// (6)
        public String getPhoneNumber() {
            return phoneNumber;
        }

        public void setPhoneNumber(String phoneNumber) {
            this.phoneNumber = phoneNumber;
        }

        // omitted.
    }

表 126. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `@XmlRootElement`アノテーションで`<customer>`要素をルート要素としてXML要素にマップする。 XML要素とクラス名が同じの場合、`name`属性での要素の指定は不要である。
(2) | `@XmlElement`アノテーションで`<name>`要素をXML要素にマップする。 XML要素とJavaBeanプロパティが同じの場合、`name`属性での要素の指定は不要である。
(3) | `@XmlElement`アノテーションで`<phone-number>`要素をXML要素にマップする。 `name`属性には、(4)の属性名でラップされる要素の名前を設定する。
(4) | `@XmlElementWrapper`アノテーションで(3)の要素をラップするラッパー要素を生成する。 `name`属性には、ラッパー要素の名前を設定する。
(5) | `@XmlType`アノテーションで(3)の`<phone-number>`要素にクラスをマップする。 `name`属性には、マップされる要素の名前を設定する。
(6) | `@XmlValue`アノテーションで(5)でマップされたクラスの`<phone-number>`要素にマップする。

上記のファイルを読む込むための設定は以下のとおり。

batch-application.properties

    (1)
    #TERASOLUNA FileAccess settings.
    p:schema="file:/usr/local/terasoluna/functionaltest/files/test/input/ch05/fileaccess/customer.xsd"

表 127. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | XMLファイルの入力に使用するスキーマファイルのパスを指定する。

JavaConfig
Bean定義

    @Bean
    @StepScope
    public StaxEventItemReader<Customer> reader(
            @Value("#{jobParameters['inputFile']}") File inputFile, // (1)
            @Value("${fileaccess.schema-file-path}") String schemaFile, // (6)
            CustomerValidationEventHandler customerValidationEventHandler) throws Exception {
        Jaxb2Marshaller unmarshaller = new Jaxb2Marshaller(); // (5)
        unmarshaller.setSchema(new FileSystemResource(schemaFile)); // (6)
        unmarshaller.setValidationEventHandler(customerValidationEventHandler); // (7)
        unmarshaller.setClassesToBeBound(Customer.class); // (8)
        unmarshaller.afterPropertiesSet();
        return new StaxEventItemReaderBuilder<Customer>()
                .name(ClassUtils.getShortName(StaxEventItemReader.class))
                .unmarshaller(unmarshaller)
                .resource(new FileSystemResource(inputFile)) // (1)
                .encoding("UTF-8") // (2)
                .addFragmentRootElements("customer") // (3)
                .strict(true) // (4)
                .build();
    }

XMLConfig
Bean定義

    <!-- (1) (2) (3) (4) -->
    <bean id="reader" class="org.springframework.batch.item.xml.StaxEventItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}"
          p:encoding="UTF-8"
          p:fragmentRootElementName="customer"
          p:strict="true">
        <property name="unmarshaller">  <!-- (5) -->
            <!-- (6) (7) -->
            <bean class="org.springframework.oxm.jaxb.Jaxb2Marshaller"
                  p:schema="file:${fileaccess.schema-file-path}"
                  p:validationEventHandler-ref="customerValidationEventHandler">
                <property name="classesToBeBound">  <!-- (8) -->
                    <list>
                        <value>jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.fileaccess.model.jaxb.Customer</value>
                    </list>
                </property>
            </bean>
        </property>
    </bean>

表 128. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 入力ファイルを設定する。 |  | なし
(2) | encoding | 入力ファイルのエンコーディングを設定する。 入出力におけるエンコーディングの仕様で述べたように、ItemReader/Writer間のデフォルト値の違いによって意図しないエンコーディングで 入力が行われることを防ぐため、デフォルト値をそのまま使用する意図である場合でも明示的にエンコーディングを設定することを推奨する。 |  | JavaVMのデフォルトエンコーディング
(3) | fragmentRootElementName | ルート要素の名前を設定する。 対象となるオブジェクトが複数ある場合には、`fragmentRootElementNames`を利用する。 |  | なし
(4) | strict | trueを設定すると、入力ファイルが存在しない(開けない)場合に例外が発生する。 |  | true
(5) | unmarshaller | アンマーシャラを設定する。 JAXBを利用する場合は、`org.springframework.oxm.jaxb.Jaxb2Marshaller`のBeanを設定する。 |  | なし
(6) | schema | バリデーションにて使用するスキーマファイルを設定する。 この例では、スキーマファイルの場所を`batch-application.properties`に定義し、`@Value`を使用してプロパティとしてインジェクションする方式とする。 これ以外にもファイルの場所の指定方法はいくつかのバリエーションが考えられるが、詳細説明は割愛する（ファイルパスではなくクラスパスで指定する、パス情報をプロパティファイルに定義する、等）。 開発案件のルールやアプリケーション実行環境を考慮して最適な方式を検討すること。 スキーマファイルの記述例は後述する。 |  |
(7) | validationEventHandler | バリデーションにて発生したイベントを処理する`ValidationEventHandler`の実装クラスを設定する。 `ValidationEventHandler`の実装例は後述する。 |  |
(8) | classesToBeBound | 変換対象のクラスをリスト形式で設定する。 |  | なし

スキーマファイル記述例

    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
        <!-- (1) -->
        <xs:element name="customer">
            <!-- (2) -->
            <xs:complexType>
                <!-- (3) -->
                <xs:sequence>
                    <!-- (4) -->
                    <xs:element name="name" type="stringMaxSize"/>  <!-- (5) -->
                    <xs:element name="phoneNumbers" type="phoneNumberList"/>  <!-- (6) -->
                </xs:sequence>
            </xs:complexType>
        </xs:element>

        <!-- (7) -->
        <xs:simpleType name="stringMaxSize">
            <xs:restriction base="xs:string">
                <xs:maxLength value="10"/>
            </xs:restriction>
        </xs:simpleType>

        <!-- (8) -->
        <xs:complexType name="phoneNumberList">
            <xs:sequence>
                <xs:element name="phone-number" minOccurs="1" maxOccurs="2"/>
            </xs:sequence>
        </xs:complexType>

    </xs:schema>

表 129. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `<xs:element>`要素の`name`属性には、入力XMLファイルのレコードにおけるルート要素を設定する。 ここでは`customer`を設定する。
(2) | `<xs:complexType>`要素は、子要素や属性を設定する。
(3) | `<xs:sequence>`要素は、子要素の出現順序を設定する。 ここでは、`name`、`phoneNumbers`の順で出現することを設定する。
(4) | `<xs:element>`要素は、子要素の属性名や属性値のデータ型などを設定する。
(5) | 子要素`name`に(7)の制約を設定する。
(6) | 子要素`phoneNumbers`に(8)の制約を設定する。
(7) | `<xs:simpleType>`要素は、整数値の範囲や文字列の長さといった制約を設定することができる。 ここでは、文字列の長さの最大値は10という制約を設定する。
(8) | `phoneNumbers`要素の子要素である`phone-number`要素に制約を設定する。 ここでは、最低出現回数が1回、最高出現回数が2回という制約を設定する。

ValidationEventHandlerの実装例

    @Component
    // (1)
    public class CustomerValidationEventHandler implements ValidationEventHandler {
        /**
         * Logger.
         */
        private static final Logger logger = LoggerFactory.getLogger(CustomerValidationEventHandler.class);

        @Override
        public boolean handleEvent(ValidationEvent event) {
            // (2)
            logger.error("[EVENT [SEVERITY:{}] [MESSAGE:{}] [LINKED EXCEPTION:{}] [LOCATOR: " +
                    "[LINE NUMBER:{}] [COLUMN NUMBER:{}] [OFFSET:{}] [OBJECT:{}] [NODE:{}] [URL:{}] ] ]",
                    event.getSeverity(),
                    event.getMessage(),
                    event.getLinkedException(),
                    event.getLocator().getLineNumber(),
                    event.getLocator().getColumnNumber(),
                    event.getLocator().getOffset(),
                    event.getLocator().getObject(),
                    event.getLocator().getNode(),
                    event.getLocator().getURL());
            return false; // (3)
        }
    }

表 130. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `ValidationEventHandler`クラスを実装し、`handleEvent`メソッドをオーバーライドする。
(2) | 引数で受けたevent(`ValidationEvent`)からイベントの情報を取得し、適宜処理を行う。 例ではイベントの情報をログ出力している。
(3) | 検証処理を終了させるためfalseを返す。 検証処理を続行する場合はtrueを返す。 適切な`UnmarshalException`、`ValidationException`、または`MarshalException`を生成して現在の操作を終了させる場合はfalseを返す。

###### 5.3.3.3.3. 出力

XMLファイルの出力にはSpring Batchが提供する`org.springframework.batch.item.xml.StaxEventItemWriter`を使用する。
`StaxEventItemWriter`は指定した`Marshaller`を使用してBeanをXMLにマッピングすることでXMLファイルを出力することができる。

`StaxEventItemWriter`は以下の要領で定義する。

- 変換対象クラスに以下の設定を行う

  - XMLのルート要素となる変換対象クラスに`@XmlRootElement`を付与する

  - `@XmlType`アノテーションを使用してフィールドを出力する順番を設定する

  - XMLへの変換対象外とするフィールドがある場合、対象フィールドのgetterに`@XmlTransient`アノテーションを付与する

- `StaxEventItemWriter`に以下のプロパティを設定する

  - `resource`プロパティに出力対象ファイルを設定する

  - `marshaller`プロパティに`org.springframework.oxm.jaxb.Jaxb2Marshaller`を設定する

- `Jaxb2Marshaller`には以下のプロパティを設定する

  - `classesToBeBound`プロパティに変換対象のクラスをリスト形式で設定する

後述で示される出力ファイル例を書き出すための設定例を示す。

依存ライブラリの追加

依存ライブラリを追加する際の設定方法は入力で説明されている依存ライブラリの追加を参照する。

XMLファイル出力時のフォーマット処理(改行およびインデント)について

`Jaxb2Marshaller` のデフォルト設定では、XMLファイルはフォーマット処理(改行およびインデント)が行われないまま出力される。
後述の出力ファイル例で示されているように、XMLのフォーマット処理を行うためには、以下のように `Jaxb2Marshaller` に対して `marshallerProperties` を設定する必要がある。

JavaConfig

    Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
    marshaller.setClassesToBeBound(CustomerToJaxb.class);
    marshaller.setMarshallerProperties(Map.of("javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT", true));
    marshaller.afterPropertiesSet();

XMLConfig

    <property name="marshaller">
        <bean class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
            <property name="classesToBeBound">
                <!-- omitted settings -->
            </property>
            <property name="marshallerProperties">
                <map>
                    <entry>
                        <key>
                            <util:constant
                                static-field="javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT"/>
                        </key>
                        <value type="java.lang.Boolean">true</value>
                    </entry>
                </map>
            </property>
        </bean>
    </property>

変換対象クラス

    @XmlRootElement(name = "Customer")  // (1)
    @XmlType(propOrder={"customerId", "customerName", "customerAddress",
            "customerTel", "chargeBranchId"})  // (2)
    public class CustomerToJaxb {

        private String customerId;
        private String customerName;
        private String customerAddress;
        private String customerTel;
        private String chargeBranchId;
        private LocalDateTime createDate;
        private LocalDateTime updateDate;

        // omitted getter/setter

        @XmlTransient  // (3)
        public LocalDateTime getCreateDate() { return createDate; }

        @XmlTransient  // (3)
        public LocalDateTime getUpdateDate() { return updateDate; }
    }

表 131. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `@XmlRootElement`アノテーションで`<Customer>`要素をルート要素としてXML要素にマップする。 `name`属性には、`Customer`を設定する。
(2) | `@XmlType`アノテーションを使用してフィールドを出力する順番を設定する。
(3) | XMLへの変換対象外とするフィールドのgetterに`@XmlTransient`アノテーションを付与する。

後述の出力ファイル例を書き出すための設定は以下のとおり。

JavaConfig
Bean定義

    @Bean
    @StepScope
    public StaxEventItemWriter<Customer> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile) throws Exception {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); // (8)
        marshaller.setClassesToBeBound(CustomerToJaxb.class); // (9)
        marshaller.setMarshallerProperties(Map.of("javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT", true)); // (10)
        marshaller.afterPropertiesSet();
        return new StaxEventItemWriterBuilder<Customer>()
                .name(ClassUtils.getShortName(StaxEventItemWriter.class))
                .resource(new FileSystemResource(outputFile)) // (1)
                .encoding("MS932") // (2)
                .rootTagName("records") // (3)
                .overwriteOutput(true) // (4)
                .shouldDeleteIfEmpty(false) // (5)
                .transactional(true) // (6)
                .standalone(false) // (7)
                .marshaller(marshaller)
                .build();
    }

XMLConfig
Bean定義

    <!-- (1) (2) (3) (4) (5) (6) (7) -->
    <bean id="writer"
          class="org.springframework.batch.item.xml.StaxEventItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"
          p:encoding="MS932"
          p:rootTagName="records"
          p:overwriteOutput="true"
          p:shouldDeleteIfEmpty="false"
          p:transactional="true"
          p:standalone="false">
        <property name="marshaller">  <!-- (8) -->
            <bean class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
                <property name="classesToBeBound">  <!-- (9) -->
                    <list>
                        <value>jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.fileaccess.model.mst.CustomerToJaxb</value>
                    </list>
                </property>
                <property name="marshallerProperties">
                    <map>
                        <entry>
                            <key>
                                <util:constant
                                    static-field="javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT"/> <!-- (10) -->
                            </key>
                            <value type="java.lang.Boolean">true</value>
                        </entry>
                    </map>
                </property>
            </bean>
        </property>
    </bean>

表 132. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 出力ファイルを設定する。 |  | なし
(2) | encoding | 出力ファイルのエンコーディングを設定する。 未指定の場合のデフォルト値は「UTF-8」。 |  | UTF-8
(3) | rootTagName | ルート要素の名前を設定する。 |  |
(4) | overwriteOutput | trueの場合、既にファイルが存在すれば削除する。 falseの場合、既にファイルが存在すれば例外をスローする。 |  | true
(5) | shouldDeleteIfEmpty | 出力件数が0件であった場合にファイルを削除するかを設定する。 trueの場合、ファイル出力後に出力件数が0件であれば出力対象ファイルを削除する。 他の設定との組み合わせによって意図しない動作をする場合がある。詳細はFlatFileItemWriterのshouldDeleteIfExistsとshouldDeleteIfEmptyの動作の違いを参照。 |  | false
(6) | transactional | トランザクション制御を行うかを設定する。詳細は、トランザクション制御を参照。 |  | true
(7) | standalone | 出力ファイルのstandalone属性を設定する。 |  | なし
(8) | marshaller | マーシャラを設定する。 JAXBを利用する場合は、`org.springframework.oxm.jaxb.Jaxb2Marshaller`を設定する。 |  | なし
(9) | classesToBeBound | 変換対象のクラスをリスト形式で設定する。 |  | なし
(10) | marshallerProperties | JAXB Marshaller プロパティを設定する。 ここでは、フォーマット済みの出力を行うために`javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT`をtrueに設定している。 |  | なし

ヘッダ・フッタの出力

ヘッダとフッタの出力には、`org.springframework.batch.item.xml.StaxWriterCallback`の実装クラスを使用する。

ヘッダの出力は、`headerCallback`、フッタの出力は、`footerCallback`に`StaxWriterCallback`の実装を設定する。

ヘッダはルート要素の開始タグ直後、フッタはルート要素の終了タグ直前に出力される。

`StaxEventItemWriter` のBean定義を以下に示す。

JavaConfig
Bean定義

    public StaxEventItemWriter<Customer> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile,
            WriteHeaderStaxWriterCallback writeHeaderStaxWriterCallback,
            WriteFooterStaxWriterCallback writeFooterStaxWriterCallback) throws Exception {
        // ommited settings
        return new StaxEventItemWriterBuilder<Customer>()
                .name(ClassUtils.getShortName(StaxEventItemWriter.class))
                .resource(new FileSystemResource(outputFile))
                .headerCallback(writeHeaderStaxWriterCallback) // (1)
                .footerCallback(writeFooterStaxWriterCallback) // (2)
                .marshaller(marshaller)
                .build();
    }

XMLConfig
Bean定義

    <!-- (1) (2) -->
    <bean id="writer"
          class="org.springframework.batch.item.xml.StaxEventItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"
          p:headerCallback-ref="writeHeaderStaxWriterCallback"
          p:footerCallback-ref="writeFooterStaxWriterCallback">
        <property name="marshaller">
            <!-- omitted settings -->
        </property>
    </bean>

表 133. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | headerCallback | `StaxWriterCallback`の実装クラスを設定する。 |  |
(2) | footerCallback | `StaxWriterCallback`の実装クラスを設定する。 |  |

`StaxWriterCallback`は以下の要領で実装する。

- `StaxWriterCallback`クラスを実装し、`write`メソッドをオーバーライドする

- 引数で受ける`XMLEventWriter`を用いてヘッダ/フッタを出力する

StaxWriterCallbackの実装例

    @Component
    public class WriteHeaderStaxWriterCallback implements StaxWriterCallback { // (1)
        @Override
        public void write(XMLEventWriter writer) throws IOException {
            XMLEventFactory factory = XMLEventFactory.newInstance();
            try {
                writer.add(factory.createComment(" Customer list header ")); // (2)
            } catch (XMLStreamException e) {
                // omitted exception handling
            }
        }
    }

表 134. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `StaxWriterCallback`クラスを実装し、`write`メソッドをオーバーライドする。
(2) | 引数で受ける`XMLEventWriter`を用いてヘッダ/フッタを出力する。

| XMLEventFactoryを使用したXMLの出力 `XMLEventWriter`クラスを用いたXMLファイルの出力では`XMLEventFactory`クラスを使用することで効率的に`XMLEvent`を生成することができる。 `XMLEventWriter`クラスには`add`メソッドが定義されており、`XMLEvent`オブジェクトを引数に取りXMLファイルの出力を行う。 `XMLEvent`オブジェクトを都度生成するのは非常に手間が掛かるため、`XMLEvent`を容易に生成することができる`XMLEventFactory`クラスを使用する。 `XMLEventFactory`クラスには `createStartDocument`メソッドや`createStartElement`メソッドなど、作成するイベントに対応したメソッドが定義してある。

下記はXMLファイルの出力でStaxEventItemWriterを使用したXMLファイルの出力例である。

出力ファイル例

    <?xml version="1.0" encoding="UTF-8"?>
    <records>
    <!-- Customer list header -->
      <Customer>
        <customerId>001</customerId>
        <customerName>CustomerName001</customerName>
        <customerAddress>CustomerAddress001</customerAddress>
        <customerTel>11111111111</customerTel>
        <chargeBranchId>001</chargeBranchId></Customer>
      <Customer>
        <customerId>002</customerId>
        <customerName>CustomerName002</customerName>
        <customerAddress>CustomerAddress002</customerAddress>
        <customerTel>11111111111</customerTel>
        <chargeBranchId>002</chargeBranchId></Customer>
      <Customer>
        <customerId>003</customerId>
        <customerName>CustomerName003</customerName>
        <customerAddress>CustomerAddress003</customerAddress>
        <customerTel>11111111111</customerTel>
        <chargeBranchId>003</chargeBranchId>
      </Customer>
    <!-- Customer list footer -->
    </records>

##### 5.3.3.4. マルチフォーマット

マルチフォーマットファイルを扱う場合の定義方法を説明する。

マルチフォーマットは、Overviewで説明したとおり(ヘッダn行 + データn行 + トレーラn行)* n + フッタn行 の形式を基本とするが以下のようなパターンも存在する。

- フッタレコードがある場合、ない場合

- 同一レコード区分内でフォーマットが異なるレコードがある場合

  - 例)データ部は項目数が5と6のデータレコードが混在する

マルチフォーマットのパターンはいくつかあるが、実現方式は同じになる。

###### 5.3.3.4.1. 入力

マルチフォーマットファイルの読み込みには、Spring Batchが提供する`org.springframework.batch.item.file.mapping.PatternMatchingCompositeLineMapper`を使用する。
マルチフォーマットファイルでは各レコードのフォーマットごとに異なるBeanにマッピングする必要がある。
`PatternMatchingCompositeLineMapper`は、パターンマッチによってレコードに対して使用する`LineTokenizer`および`FieldSetMapper`を選択することができる。

たとえば、以下のような形で使用する`LineTokenizer`を選択することが可能である。

- `USER*`にマッチする(レコードの先頭が`USER`である)場合は`userTokenizer`を使用する

- `LINEA*`にマッチする(レコードの先頭が`LINEA`である)場合は`lineATokenizer`を使用する

| マルチフォーマットファイルを読み込む際のレコードにかかるフォーマットの制約 マルチフォーマットファイルを読み込むためには、レコード区分がAntPathMatcherによるパターンマッチで判別可能なフォーマットでなければならない。 詳細は`PatternMatcher`のAPIドキュメントを参照。

`PatternMatchingCompositeLineMapper`は以下の要領で実装する。

- 変換対象クラスはレコード区分をもつクラスを定義し、各レコード区分のクラスに継承させる

- 各レコードをBeanにマッピングするための`LineTokenizer`および`FieldSetMapper`を定義する

- `PatternMatchingCompositeLineMapper`を定義する

  - `tokenizers`プロパティに各レコード区分に対応する`LineTokenizer`を設定する

  - `fieldSetMappers`プロパティに各レコード区分に対応する`FieldSetMapper`を設定する

| 変換対象クラスはレコード区分をもつクラスを定義し、各レコード区分のクラスに継承させる `ItemProcessor`は1つの型を引数に取る仕様である。 しかし、単純に`PatternMatchingCompositeLineMapper`にてマルチフォーマットのファイルをレコード区分ごとに異なるBeanにマッピングすると、`ItemProcessor`は1つの型を引数に取るため複数の型を処理することができない。 そのため、変換対象のクラスに継承関係をもたせ、`ItemProcessor`の引数の型にスーパークラスを指定することで解決が可能である。 以下に変換対象クラスのクラス図と`ItemProcessor`の定義例を示す。 図 43. 変換対象クラスのクラス図 ItemProcessorの定義例
    public class MultiFormatItemProcessor implements
            ItemProcessor<SalesPlanDetailMultiFormatRecord, String> {
        @Override
        // (1)
        public String process(SalesPlanDetailMultiFormatRecord item) throws Exception {
            String record = item.getRecord();  // (2)

            switch (record) {  // (3)
            case "H":
                // omitted business logic
            case "D":
                // omitted business logic
            case "T":
                // omitted business logic
            case "E":
                // omitted business logic
            default:
                // omitted exception handling
            }
        }
    }
表 135. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `ItemProcessor`の引数に継承関係をもたせた変換対象クラスのスーパークラスを設定する。
(2) | itemからレコード区分を取得する。 各レコード区分によって実態のクラスは異なるが、ポリモーフィズムによってレコード区分を取得できる。
(3) | レコード区分を判定し、各レコード区分ごとの処理を行う。 適宜、クラスの変換処理等を行うこと。

以下に下記の入力ファイルを読み込むための設定例を示す。実装例を示す。

入力ファイル例

    H,Sales_plan_detail header No.1
    D,000001,2016,1,0000000001,100000000
    D,000001,2016,1,0000000002,200000000
    D,000001,2016,1,0000000003,300000000
    T,000001,3,600000000
    H,Sales_plan_detail header No.2
    D,00002,2016,1,0000000004,400000000
    D,00002,2016,1,0000000005,500000000
    D,00002,2016,1,0000000006,600000000
    T,00002,3,1500000000
    H,Sales_plan_detail header No.3
    D,00003,2016,1,0000000007,700000000
    D,00003,2016,1,0000000008,800000000
    D,00003,2016,1,0000000009,900000000
    T,00003,3,2400000000
    E,3,9,4500000000

下記に変換対象クラスのBean定義例を示す。

変換対象クラス

    /**
     * Model of record indicator of sales plan detail.
     */
    public class SalesPlanDetailMultiFormatRecord {

        protected String record;

        // omitted getter/setter
    }

    /**
     * Model of sales plan detail header.
     */
    public class SalesPlanDetailHeader extends SalesPlanDetailMultiFormatRecord {

        private String description;

        // omitted getter/setter
    }

    /**
     * Model of Sales plan Detail.
     */
    public class SalesPlanDetailData extends SalesPlanDetailMultiFormatRecord {

        private String branchId;
        private int year;
        private int month;
        private String customerId;
        private BigDecimal amount;

        // omitted getter/setter
    }

    /**
     * Model of Sales plan Detail.
     */
    public class SalesPlanDetailTrailer extends SalesPlanDetailMultiFormatRecord {

        private String branchId;
        private int number;
        private BigDecimal total;

        // omitted getter/setter
    }

    /**
     * Model of Sales plan Detail.
     */
    public class SalesPlanDetailEnd extends SalesPlanDetailMultiFormatRecord {
        // omitted getter/setter

        private int headNum;
        private int trailerNum;
        private BigDecimal total;

        // omitted getter/setter
    }

上記のファイルを読む込むための設定は以下のとおり。

JavaConfig
Bean定義例

    // (1)
    @Bean
    public DelimitedLineTokenizer headerDelimitedLineTokenizer() {
        DelimitedLineTokenizer headerDelimitedLineTokenizer = new DelimitedLineTokenizer();
        headerDelimitedLineTokenizer.setNames("record", "description");
        return headerDelimitedLineTokenizer;
    }

    @Bean
    public DelimitedLineTokenizer dataDelimitedLineTokenizer() {
        DelimitedLineTokenizer dataDelimitedLineTokenizer = new DelimitedLineTokenizer();
        dataDelimitedLineTokenizer.setNames("record", "branchId", "year", "month", "customerId", "amount");
        return dataDelimitedLineTokenizer;
    }

    @Bean
    public DelimitedLineTokenizer trailerDelimitedLineTokenizer() {
        DelimitedLineTokenizer trailerDelimitedLineTokenizer = new DelimitedLineTokenizer();
        trailerDelimitedLineTokenizer.setNames("record", "branchId", "number", "total");
        return trailerDelimitedLineTokenizer;
    }

    @Bean
    public DelimitedLineTokenizer endDelimitedLineTokenizer() {
        DelimitedLineTokenizer endDelimitedLineTokenizer = new DelimitedLineTokenizer();
        endDelimitedLineTokenizer.setNames("record", "headNum", "trailerNum", "total");
        return endDelimitedLineTokenizer;
    }

    // (2)
    @Bean
    public BeanWrapperFieldSetMapper<SalesPlanDetailHeader> headerBeanWrapperFieldSetMapper() {
        BeanWrapperFieldSetMapper<SalesPlanDetailHeader> headerBeanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<>();
        headerBeanWrapperFieldSetMapper.setTargetType(SalesPlanDetailHeader.class);
        return headerBeanWrapperFieldSetMapper;
    }

    @Bean
    public BeanWrapperFieldSetMapper<SalesPlanDetailData> dataBeanWrapperFieldSetMapper() {
        BeanWrapperFieldSetMapper<SalesPlanDetailData> dataBeanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<>();
        dataBeanWrapperFieldSetMapper.setTargetType(SalesPlanDetailData.class);
        return dataBeanWrapperFieldSetMapper;
    }

    @Bean
    public BeanWrapperFieldSetMapper<SalesPlanDetailTrailer> trailerBeanWrapperFieldSetMapper() {
        BeanWrapperFieldSetMapper<SalesPlanDetailTrailer> trailerBeanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<>();
        trailerBeanWrapperFieldSetMapper.setTargetType(SalesPlanDetailTrailer.class);
        return trailerBeanWrapperFieldSetMapper;
    }

    @Bean
    public BeanWrapperFieldSetMapper<SalesPlanDetailEnd> endBeanWrapperFieldSetMapper() {
        BeanWrapperFieldSetMapper<SalesPlanDetailEnd> endBeanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<>();
        endBeanWrapperFieldSetMapper.setTargetType(SalesPlanDetailEnd.class);
        return endBeanWrapperFieldSetMapper;
    }

    @Bean
    @StepScope
    public FlatFileItemReader reader(
            @Value("#{jobParameters['inputFile']}") File inputFile,
            LineTokenizer headerDelimitedLineTokenizer,
            LineTokenizer dataDelimitedLineTokenizer,
            LineTokenizer trailerDelimitedLineTokenizer,
            LineTokenizer endDelimitedLineTokenizer,
            FieldSetMapper<SalesPlanDetailHeader> headerBeanWrapperFieldSetMapper,
            FieldSetMapper<SalesPlanDetailData> dataBeanWrapperFieldSetMapper,
            FieldSetMapper<SalesPlanDetailTrailer> trailerBeanWrapperFieldSetMapper,
            FieldSetMapper<SalesPlanDetailEnd> endBeanWrapperFieldSetMapper) {

        Map<String, LineTokenizer> tokenizers = new HashMap<>(); // (4)
        tokenizers.put("H*", headerDelimitedLineTokenizer);
        tokenizers.put("D*", dataDelimitedLineTokenizer);
        tokenizers.put("T*", trailerDelimitedLineTokenizer);
        tokenizers.put("E*", endDelimitedLineTokenizer);

        Map<String, FieldSetMapper> fieldSetMappers = new HashMap<>(); // (5)
        fieldSetMappers.put("H*", headerBeanWrapperFieldSetMapper);
        fieldSetMappers.put("D*", dataBeanWrapperFieldSetMapper);
        fieldSetMappers.put("T*", trailerBeanWrapperFieldSetMapper);
        fieldSetMappers.put("E*", endBeanWrapperFieldSetMapper);

        final PatternMatchingCompositeLineMapper lineMapper = new PatternMatchingCompositeLineMapper(); // (3)
        lineMapper.setTokenizers(tokenizers);
        lineMapper.setFieldSetMappers(fieldSetMappers);

        final FlatFileItemReader reader = new FlatFileItemReader();
        reader.setResource(new FileSystemResource(inputFile));
        reader.setLineMapper(lineMapper);
        return reader;
    }

XMLConfig
Bean定義例

    <!-- (1) -->
    <bean id="headerDelimitedLineTokenizer"
          class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
          p:names="record,description"/>

    <bean id="dataDelimitedLineTokenizer"
          class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
          p:names="record,branchId,year,month,customerId,amount"/>

    <bean id="trailerDelimitedLineTokenizer"
          class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
          p:names="record,branchId,number,total"/>

    <bean id="endDelimitedLineTokenizer"
          class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
          p:names="record,headNum,trailerNum,total"/>

    <!-- (2) -->
    <bean id="headerBeanWrapperFieldSetMapper"
          class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
          p:targetType="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.fileaccess.model.plan.SalesPlanDetailHeader"/>

    <bean id="dataBeanWrapperFieldSetMapper"
          class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
          p:targetType="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.fileaccess.model.plan.SalesPlanDetailData"/>

    <bean id="trailerBeanWrapperFieldSetMapper"
          class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
          p:targetType="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.fileaccess.model.plan.SalesPlanDetailTrailer"/>

    <bean id="endBeanWrapperFieldSetMapper"
          class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
          p:targetType="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.fileaccess.model.plan.SalesPlanDetailEnd"/>

    <bean id="reader"
        class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
        p:resource="file:#{jobParameters['inputFile']}">
        <property name="lineMapper">  <!-- (3) -->
            <bean class="org.springframework.batch.item.file.mapping.PatternMatchingCompositeLineMapper">
                <property name="tokenizers">  <!-- (4) -->
                    <map>
                        <entry key="H*" value-ref="headerDelimitedLineTokenizer"/>
                        <entry key="D*" value-ref="dataDelimitedLineTokenizer"/>
                        <entry key="T*" value-ref="trailerDelimitedLineTokenizer"/>
                        <entry key="E*" value-ref="endDelimitedLineTokenizer"/>
                    </map>
                </property>
                <property name="fieldSetMappers">  <!-- (5) -->
                    <map>
                        <entry key="H*" value-ref="headerBeanWrapperFieldSetMapper"/>
                        <entry key="D*" value-ref="dataBeanWrapperFieldSetMapper"/>
                        <entry key="T*" value-ref="trailerBeanWrapperFieldSetMapper"/>
                        <entry key="E*" value-ref="endBeanWrapperFieldSetMapper"/>
                    </map>
                </property>
            </bean>
        </property>
    </bean>

表 136. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | 各レコードに対応する`LineTokenizer` | 各レコード区分に対応する`LineTokenizer`を定義する。 |  |
(2) | 各レコードに対応する`FieldSetMapper` | 各レコード区分に対応する`FieldSetMapper`を定義する。 |  |
(3) | lineMapper | `org.springframework.batch.item.file.mapping.PatternMatchingCompositeLineMapper`を設定する。 |  | なし
(4) | tokenizers | map形式で各レコード区分に対応する`LineTokenizer`を設定する。 `key`にレコードを判別するパターンを設定し、`value-ref`に使用する`LineTokenizer`を設定する。 |  | なし
(5) | fieldSetMappers | map形式で各レコード区分に対応する`FieldSetMapper`を設定する。 `key`にレコードを判別するパターンを設定し、`value-ref`に使用する`FieldSetMapper`を設定する。 |  | なし

###### 5.3.3.4.2. 出力

マルチフォーマットファイルを扱う場合の定義方法を説明する。

マルチフォーマットファイル読み込みではレコード区分によって使用する`LineTokenizer`および`FieldSetMapper`を判別する`PatternMatchingCompositeLineMapper`を使用することで実現可能である。
しかし、書き込み時に同様の機能をもつコンポーネントは提供されていない。

そのため、`ItemProcessor`内で変換対象クラスをレコード(文字列)に変換する処理までを行い、`ItemWriter`では受け取った文字列をそのまま書き込みを行うことでマルチフォーマットファイルの書き込みを実現する。

マルチフォーマットファイルの書き込みは以下の要領で実装する。

- `ItemProcessor`にて変換対象クラスをレコード(文字列)に変換して`ItemWriter`に渡す

  - 例では、各レコード区分ごとの`LineAggregator`および`FieldExtractor`を定義し、`ItemProcessor`でインジェクトして使用する

- `ItemWriter`では受け取った文字列をそのままファイルへ書き込みを行う

  - `ItemWriter`の`lineAggregator`プロパティに`PassThroughLineAggregator`を設定する

  - `PassThroughLineAggregator`は受け取ったitemの`item.toString()`した結果を返す`LineAggregator`である

以下に下記の出力ファイルを書き出すための設定例を示す。実装例を示す。

出力ファイル例

    H,Sales_plan_detail header No.1
    D,000001,2016,1,0000000001,100000000
    D,000001,2016,1,0000000002,200000000
    D,000001,2016,1,0000000003,300000000
    T,000001,3,600000000
    H,Sales_plan_detail header No.2
    D,00002,2016,1,0000000004,400000000
    D,00002,2016,1,0000000005,500000000
    D,00002,2016,1,0000000006,600000000
    T,00002,3,1500000000
    H,Sales_plan_detail header No.3
    D,00003,2016,1,0000000007,700000000
    D,00003,2016,1,0000000008,800000000
    D,00003,2016,1,0000000009,900000000
    T,00003,3,2400000000
    E,3,9,4500000000

変換対象クラスの定義および`ItemProcessor`定義例、注意点はマルチフォーマットの入力と同様である。

上記のファイルを出力するための設定は以下のとおり。 `ItemProcessor`の定義例をBean定義例の後に示す。

JavaConfig
Bean定義例

    // (1)
    @Bean
    public DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> headerDelimitedLineAggregator() {
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> headerDelimitedLineAggregator = new DelimitedLineAggregator<>();
        BeanWrapperFieldExtractor<SalesPlanDetailMultiFormatRecord> fieldExtractor = new BeanWrapperFieldExtractor<>();
        fieldExtractor.setNames(new String[] {"record", "description"});
        headerDelimitedLineAggregator.setFieldExtractor(fieldExtractor);
        return headerDelimitedLineAggregator;
    }

    @Bean
    public DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> dataDelimitedLineAggregator() {
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> dataDelimitedLineAggregator = new DelimitedLineAggregator<>();
        BeanWrapperFieldExtractor<SalesPlanDetailMultiFormatRecord> fieldExtractor = new BeanWrapperFieldExtractor<>();
        fieldExtractor.setNames(new String[] {"record", "branchId", "year", "month", "customerId", "amount"});
        dataDelimitedLineAggregator.setFieldExtractor(fieldExtractor);
        return dataDelimitedLineAggregator;
    }

    @Bean
    public DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> trailerDelimitedLineAggregator() {
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> trailerDelimitedLineAggregator = new DelimitedLineAggregator<>();
        BeanWrapperFieldExtractor<SalesPlanDetailMultiFormatRecord> fieldExtractor = new BeanWrapperFieldExtractor<>();
        fieldExtractor.setNames(new String[] {"record", "branchId", "number", "total"});
        trailerDelimitedLineAggregator.setFieldExtractor(fieldExtractor);
        return trailerDelimitedLineAggregator;
    }

    @Bean
    public DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> endDelimitedLineAggregator() {
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> endDelimitedLineAggregator = new DelimitedLineAggregator<>();
        BeanWrapperFieldExtractor<SalesPlanDetailMultiFormatRecord> fieldExtractor = new BeanWrapperFieldExtractor<>();
        fieldExtractor.setNames(new String[] {"record", "headNum", "trailerNum", "total"});
        endDelimitedLineAggregator.setFieldExtractor(fieldExtractor);
        return endDelimitedLineAggregator;
    }

    @Bean
    @StepScope
    public FlatFileItemWriter writer(
            @Value("#{jobParameters['outputFile']}") File outputFile) {
        PassThroughLineAggregator lineAggregator = new PassThroughLineAggregator(); // (2)
        return new FlatFileItemWriterBuilder<SalesPlanDetail>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .resource(new FileSystemResource(outputFile))
                .lineAggregator(lineAggregator)
                .build();
    }

XMLConfig
Bean定義例

    <!-- (1) -->
    <bean id="headerDelimitedLineAggregator"
          class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
        <property name="fieldExtractor">
            <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                  p:names="record,description"/>
        </property>
    </bean>

    <bean id="dataDelimitedLineAggregator"
          class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
        <property name="fieldExtractor">
            <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                  p:names="record,branchId,year,month,customerId,amount"/>
        </property>
    </bean>

    <bean id="trailerDelimitedLineAggregator"
          class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
        <property name="fieldExtractor">
            <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                  p:names="record,branchId,number,total"/>
        </property>
    </bean>

    <bean id="endDelimitedLineAggregator"
          class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
        <property name="fieldExtractor">
            <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                  p:names="record,headNum,trailerNum,total"/>
        </property>
    </bean>

    <bean id="writer" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"/>
        <property name="lineAggregator">  <!-- (2) -->
            <bean class="org.springframework.batch.item.file.transform.PassThroughLineAggregator"/>
        </property>
    </bean>

表 137. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | 各レコード区分に対応する`LineAggregator`および`FieldExtractor` | `LineAggregator`および`FieldExtractor`を定義する。 `ItemProcessor`で`LineAggregator`をインジェクトして使用する。 |  |
(2) | lineAggregator | `org.springframework.batch.item.file.transform.PassThroughLineAggregator`を設定する。 |  | なし

`ItemProcessor`の実装例を以下に示す。
例で実装しているのは、受け取ったitemを文字列に変換して`ItemWriter`に渡す処理のみである。

ItemProcessorの定義例

    public class MultiFormatItemProcessor implements
            ItemProcessor<SalesPlanDetailMultiFormatRecord, String> {

        // (1)
        @Inject
        @Named("headerDelimitedLineAggregator")
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> headerDelimitedLineAggregator;

        @Inject
        @Named("dataDelimitedLineAggregator")
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> dataDelimitedLineAggregator;

        @Inject
        @Named("trailerDelimitedLineAggregator")
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> trailerDelimitedLineAggregator;

        @Inject
        @Named("endDelimitedLineAggregator")
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> endDelimitedLineAggregator;

        @Override
        // (2)
        public String process(SalesPlanDetailMultiFormatRecord item) throws Exception {
            String record = item.getRecord();  // (3)

            switch (record) {  // (4)
            case "H":
                return headerDelimitedLineAggregator.aggregate(item);  // (5)
            case "D":
                return dataDelimitedLineAggregator.aggregate(item);  // (5)
            case "T":
                return trailerDelimitedLineAggregator.aggregate(item);  // (5)
            case "E":
                return endDelimitedLineAggregator.aggregate(item);  // (5)
            default:
                throw new IncorrectRecordClassificationException(
                        "Record classification is incorrect.[value:" + record + "]");
            }
        }
    }

表 138. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | 各レコード区分に対応する`LineAggregator`をインジェクトする。
(2) | `ItemProcessor`の引数に継承関係をもたせた変換対象クラスのスーパークラスを設定する。
(3) | itemからレコード区分を取得する。
(4) | レコード区分を判定し、各レコード区分ごとの処理を行う。
(5) | 各レコード区分に対応する`LineAggregator`を使用し変換対象クラスをレコード(文字列)に変換して`ItemWriter`に渡す。

### 5.4. 排他制御

#### 5.4.1. Overview

排他制御とは、複数のトランザクションから同じリソースに対して、同時に更新処理が行われる際に、データの整合性を保つために行う処理のことである。 複数のトランザクションから同じリソースに対して、同時に更新処理が行われる可能性がある場合は、基本的に排他制御を行う必要がある。

ここでの複数トランザクションとは以下のことを指す。

- 複数ジョブの同時実行時におけるトランザクション

- オンライン処理との同時実行時におけるトランザクション

| 複数ジョブの排他制御 複数ジョブを同時実行する場合は、排他制御の必要がないようにジョブ設計を行うことが基本である。 これは、アクセスするリソースや処理対象をジョブごとに分割することが基本であることを意味する。

排他制御に関する概念は、オンライン処理と同様であるため、Macchinetta Server 1.x 開発ガイドラインにある 排他制御を参照。

ここでは、Macchinetta Server 1.xでは説明されていない部分を中心に説明をする。

本機能は、チャンクモデルとタスクレットモデルとで同じ使い方になる。

##### 5.4.1.1. 排他制御の必要性

排他制御の必要性に関しては、Macchinetta Server 1.x 開発ガイドラインにある 排他制御の必要性を参照。

##### 5.4.1.2. ファイルの排他制御

ファイルでの排他制御はファイルロックにより実現するのが一般的である。

ファイルロックとは

  ファイルロックとは、ファイルをあるプログラムで使用している間、ほかのプログラムからの読み書きを制限する仕組みである。 ファイルロックの実施イメージを以下に示す。

シナリオ

- バッチ処理Aがファイルのロックを取得し、ファイルの更新処理を開始する。

- バッチ処理Bが同一のファイルの更新を試みファイルのロック取得を試みるが失敗する。

- バッチ処理Aが処理を終了し、ファイルのロックを解除する

図 44. ファイルロックの実施イメージ

1. バッチ処理A(Batch ProcessA)が排他対象ファイル(TargetFile)のロック取得を試みる。

2. バッチ処理Aが、排他対象ファイルのロック取得に成功する。

3. バッチ処理B(Batch ProcessB)が、排他対象ファイルのロック取得を試みる。

4. バッチ処理Aが、排他対象ファイルに書き込みを行う。

5. バッチ処理Bは、バッチ処理Aがロック中であるため、排他対象ファイルのロック取得に失敗する。

6. バッチ処理Bが、ファイル更新失敗の処理を行う。

7. バッチ処理Aが、排他対象ファイルのロックを開放する。

| デッドロックの予防 ファイルにおいてもデータベースと同様に複数のファイルに対してロックを取得する場合、デッドロックとなる場合がある。 そのため、ファイルの更新順序をルール化することが重要である。 デッドロックの予防に関してはデータベースのテーブル間でのデッドロック防止と同様である。 詳細については、Macchinetta Server 1.x 開発ガイドラインの デッドロックの予防を参照。

##### 5.4.1.3. データベースの排他制御

データベースの排他制御に関しては、Macchinetta Server 1.x 開発ガイドラインにある データベースのロック機能による排他制御 で詳しく説明されているため、そちらを参照。

##### 5.4.1.4. 排他制御方式の使い分け

Macchinetta Batch 2.xでのロック方式と向いているシチュエーションを示す。

表 139. 排他制御方式の使い分け
ロック方式 | 向いているシチュエーション
--- | ---
楽観ロック | 同時実行時におけるトランザクションで、別トランザクションの更新結果を処理対象外にして処理を継続できる場合
悲観ロック | 処理時間が長く、処理中に対象データの状況が変化したことによるやり直しが難しい処理 ファイルに対する排他制御が必要な処理

##### 5.4.1.5. 排他制御とコンポーネントの関係

Macchinetta Batch 2.xが提供する各コンポーネントと排他制御との関係は以下のとおり。

楽観ロック

表 140. 排他制御とコンポーネントの関係
処理モデル | コンポーネント | ファイル | データベース
--- | --- | --- | ---
チャンク | ItemReader | - | Versionカラムなど取得時と更新時とで同じデータであることが確認できるカラムを含めてデータ取得を行う。
ItemProcessor | - | 排他制御は不要である。
ItemWriter | - | 取得時と更新時との差分を確認し、他の処理で更新されていないことを確認した上で更新を行う。
タスクレット | Tasklet | - | データ取得時にはItemReader、データ更新時はItemWriterで説明した処理を実施する。 `Mapper`インタフェースを直接利用する場合も考え方は同じである。

| ファイルに対する楽観ロック ファイルの特性上、ファイルに対して楽観ロックを適用することがない。

悲観ロック

表 141. 排他制御とコンポーネントの関係
処理モデル | コンポーネント | ファイル | データベース
--- | --- | --- | ---
チャンク | ItemReader | - | 悲観ロックを用いずにSELECT文を発行する。 ItemProcessorやItemWriterとは別コネクションになるため、ItemReaderでは排他制御は行わない。 SELECTで取得するデータは、ItemProcessorでデータを取得する条件とする必要最低限のデータ(キー情報)とすることで性能が向上する。
ItemProcessor | - | `Mapper`インタフェースを利用して、ItemReaderで取得したデータ(キー情報)を条件とするSQL文でSELECT FOR UPDATEを発行する。
ItemWriter | - | 悲観ロックを行ったItemProcessorと同トランザクションとなるため、ItemWriterでは排他制御を意識することなくデータを更新する。
タスクレット | Tasklet | ItemStreamReaderでファイルをオープンする直前にファイルロックを取得する。 ItemStreamWriterをクローズした直後にファイルロックを開放する。 | データ取得時にはSELECT FOR UPDATE文を発行するItemReaderか`Mapper`インタフェースを直接利用する。 データ更新時はItemWriterで説明した処理を実施する。`Mapper`インタフェースを直接利用する場合も考え方は同じである。

| チャンクモデルでのデータベースでの悲観ロックによる注意事項 ItemReaderで取得したデータ(キー情報)がItemProcessorへ渡される間は排他制御されず、他のトランザクションによりもとのデータが更新されている可能性がある。 そのため、ItemProcessorがデータを取得する条件は、ItemReaderと同じデータ(キー情報)を取得する条件を含む必要がある。 ItemProcessorでデータが取得できない場合は、他のトランザクションにより更新されている可能性を考慮して、処理の継続または中断を検討し実装する必要がある。

| ファイルに対する悲観ロック ファイルに対する悲観ロックはタスクレットモデルで実装すること。 チャンクモデルではその構造上、チャンク処理の隙間で排他できない期間が存在してしまうためである。 また、ファイルアクセスはItemStreamReader/ItemStreamWriterをInjectして利用することを前提とする。

| データベースでの悲観ロックによる待ち時間 悲観ロックを行う場合、競合により処理が待たされる時間が長くなる可能性がある。 その場合、NO WAITオプションやタイムアウト時間を指定して、悲観ロックを使用するのが妥当である。

#### 5.4.2. How to use

排他制御の使い方をリソース別に説明する。

- ファイルの排他制御

- データベースの排他制御

##### 5.4.2.1. ファイルの排他制御

Macchinetta Batch 2.xにおけるファイルの排他制御はタスクレットを実装することで実現する。 排他の実現手段としては、`java.nio.channels.FileChannel`クラスを使用したファイルロック取得で排他制御を行う。

| FileChannelクラスの詳細 `FileChannel`クラスの詳細、使用方法については Javadocを参照。

ただし、Spring Batchにおいて標準的なファイルの入出力機能を提供するFlatFileItemReader/FlatFileItemWriterから、java.nio.channels.FileChannelクラスを利用することはできない。 そのため、排他対象のファイルと一対一に排他対象ファイルのロックを担当するファイル(以降、ロック用ファイル)を用意し、ロック用ファイルからのファイルロックを取得をもって、排他対象ファイルへの排他的な制御権を取得できたと見なすことで排他制御を実現する。 ロック用ファイルを用いたファイルロックの実施イメージを以下に示す。

図 45. ロック用ファイルを用いたファイルロックの実施イメージ

1. バッチ処理A(Batch ProcessA)がロック用ファイル(LockFile)のロック取得を試みる。

2. バッチ処理Aが、ロック用ファイルのロック取得に成功する。

3. バッチ処理B(Batch ProcessB)が、ロック用ファイルのロック取得を試みる。

4. バッチ処理Aが、排他対象ファイルに書き込みを行う。

5. バッチ処理Bは、バッチ処理Aがロック中であるため、ロック用ファイルのロック取得に失敗する。

6. バッチ処理Bが、ファイル更新失敗の処理を行う。

7. バッチ処理Aが、ロック用ファイルのロックを開放する。

| ロック用ファイルの削除タイミング 後述するTasklet実装の例では、ロック用ファイルの削除について表現していないが、そのタイミングについては注意が必要である。 特にLinux環境では、ジョブの内部でロック用ファイルの削除を行うべきではない。 Linux環境ではファイルロック中のファイルを削除できるため、複数のプロセスでバッチ処理が実行される場合、あるプロセスがファイルロックを取得しているロック用ファイルを、別プロセスが削除する可能性がある。 このようなロック用ファイルの削除が起こると、また別のプロセスはロック用ファイルを作成し、作成したロック用ファイルからファイルロックを取得できる。 結果として、排他対象ファイルに書き込めるプロセスが複数になり、排他制御として機能しなくなる。 これを防ぐため、ロック用ファイルの削除は、排他制御を行うプロセスが存在しなくなったタイミングで行うなどの方法が考えられる。

ItemStreamWriterの`transactional`プロパティは`false`を設定する。 `transactional`プロパティがデフォルトの`true`の場合、ファイル出力のタイミングがTransactionManagerと同期し、排他制御がされていない状態でのファイル出力が行われる。 以下に、ItemStreamWriterの設定例を示す。

JavaConfig

    @Bean
    @StepScope
    public FlatFileItemWriter<SalesPlanDetailWithProcessName> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile) {
        DelimitedLineAggregator<SalesPlanDetailWithProcessName> lineAggregator = new DelimitedLineAggregator<>();
        BeanWrapperFieldExtractor<SalesPlanDetailWithProcessName> fieldExtractor = new BeanWrapperFieldExtractor<>();
        fieldExtractor.setNames(new String[] {"processName", "plan.branchId", "plan.year", "plan.month", "plan.customerId", "plan.amount"});
        lineAggregator.setFieldExtractor(fieldExtractor);
        return new FlatFileItemWriterBuilder<SalesPlanDetailWithProcessName>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .resource(new FileSystemResource(outputFile))
                .lineAggregator(lineAggregator)
                .transactional(false) // (1)
                .build();
    }

XMLConfig

    <bean id="writer" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"
          p:transactional="false">  <!-- (1) -->
        <property name="lineAggregator">
            <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
                <property name="fieldExtractor">
                    <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                          p:names="processName,plan.branchId,plan.year,plan.month,plan.customerId,plan.amount"/>
                </property>
            </bean>
        </property>
    </bean>

表 142. 説明
項番 | 説明
--- | ---
(1) | `FlatFileItemWriterBuilderのtransactionalメソッド/<bean>要素のtransactional属性`に`false`を設定する。

`FileChannel`クラスを使用しファイルのロックを取得する例を示す。

Tasklet実装

    @Component
    @Scope("step")
    public class FileExclusiveTasklet implements Tasklet {

        private String targetPath = null; // (1)

        @Inject
        ItemStreamReader<SalesPlanDetail> reader;

        @Inject
        ItemStreamWriter<SalesPlanDetailWithProcessName> writer;

        @Override
        public RepeatStatus execute(StepContribution contribution,
                ChunkContext chunkContext) throws Exception {

            // omitted.

            File file = new File(targetPath);
            try (FileChannel fc = FileChannel.open(file.toPath(),
                    StandardOpenOption.WRITE,
                    StandardOpenOption.CREATE,
                    StandardOpenOption.APPEND); // (2)
                    FileLock fileLock = fc.tryLock()) {  // (3)

                if (fileLock == null) {
                    logger.error("Failed to acquire lock. [processName={}]", processName);
                    throw new FailedAcquireLockException("Failed to acquire lock");
                }

                reader.open(executionContext);
                writer.open(executionContext);  // (4)

                // (5)
                SalesPlanDetail item;
                List<SalesPlanDetailWithProcessName> items = new ArrayList<>();
                while ((item = reader.read()) != null) {

                    // omitted.

                    items.add(item);
                    if (items.size() >= 10) {
                        writer.write(new Chunk(items));
                        items.clear();
                    }
                }
                if (items.size() > 0) {
                    writer.write(new Chunk(items));
                }

            } catch (IOException e) {
                logger.error("Failure other than lock acquisition", e);
                throw new FailedOtherAcquireLockException("Failure other than lock acquisition", e);

            } finally {
                try {
                    writer.close(); // (6)
                } catch (ItemStreamException e) {
                    // ignore
                }
                try {
                    reader.close();
                } catch (ItemStreamException e) {
                    // ignore
                }
            }

            return RepeatStatus.FINISHED;
        }

        // (7)
        @Value("#{jobParameters['lockFile']}")
        public void setTargetPath(String targetPath) {
            this.targetPath = targetPath;
        }
    }

表 143. 説明
項番 | 説明
--- | ---
(1) | ロック用ファイルのパス。
(2) | ロック用ファイルのファイルチャネルを取得する。 この例では、ファイルの新規作成・追記・書き込みに対するチャネルを取得している。
(3) | ロック用ファイルのファイルロックを取得する。
(4) | ロック用ファイルのファイルロック取得に成功した場合、排他対象のファイルをオープンする。 この例では、排他対象のファイルはジョブ定義ファイルで設定している。 設定方法は、ファイルアクセスを参照。
(5) | ファイル出力を伴うビジネスロジックを実行する。
(6) | 排他対象のファイルをクローズする。
(7) | ロック用ファイルのパスを設定する。 この例では、ジョブパラメータから受け取るようにしている。

| ロック取得に用いるFileChannelのメソッドについて `lock()`メソッドは排他対象ファイルがロック済みの場合ロックが解除されるまで待機するため、待機されない`tryLock()`メソッドを使用することを推奨する。 なおtrylock()は共有ロックと排他ロックが選択できるが、バッチ処理においては、通常は排他ロックを用いる。

| 同一VMでのスレッド間の排他制御 同一VMにおけるスレッド間の排他制御は注意が必要である。 同一VMでのスレッド間でファイルに対する処理を行う場合、`FileChannel`クラスを用いたロック機能では、ファイルが別スレッドの処理にてロックされているかの判定ができない。 そのため、スレッド間での排他制御は機能しない。これを回避するには、ファイルへの書き込みを行う部分で同期化処理をすることでスレッド間の排他制御が行える。 しかし、同期化を行うことで並列処理のメリットが薄れてしまい、単一スレッドで処理することと差異がなくなってしまう。 結果、同一のファイルに対して異なるスレッドで排他制御をして処理することは適していないため、そのような処理設計・実装を行わないこと。

##### 5.4.2.2. データベースの排他制御

Macchinetta Batch 2.xにおけるデータベースの排他制御について説明する。

データベースの排他制御実装は、Macchinetta Server 1.x 開発ガイドラインにある MyBatis3使用時の実装方法が基本である。 本ガイドラインでは、 MyBatis3使用時の実装方法ができている前提で説明を行う。

排他制御とコンポーネントの関係にあるとおり、処理モデル・コンポーネントの組み合わせによるバリエーションがある。

表 144. データベースの排他制御のバリエーション
排他方式 | 処理モデル | コンポーネント
楽観ロック | チャンクモデル | ItemReader/ItemWriter
タスクレットモデル | ItemReader/ItemWriter
Mapperインタフェース
悲観ロック | チャンクモデル | ItemReader/ItemProcessor/ItemWriter
タスクレットモデル | ItemReader/ItemWriter
Mapperインタフェース

タスクレットモデルで`Mapper`インタフェースを使用する場合は、 MyBatis3使用時の実装方法のとおりであるため、説明を割愛する。

タスクレットモデルでItemReader/ItemWriterを使用する場合は、`Mapper`インタフェースでの呼び出し部分がItemReader/ItemWriterに代わるだけなので、これも説明を割愛する。

よって、ここではチャンクモデルの排他制御について説明する。

###### 5.4.2.2.1. 楽観ロック

チャンクモデルでの楽観ロックについて説明する。

MyBatisBatchItemWriterがもつ`assertUpdates`プロパティの設定により、ジョブの振る舞いが変化するので業務要件に合わせて、適切に設定をする必要がある。

楽観ロックを行うジョブ定義を以下に示す。

JavaConfig

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan(value = { "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common",
    "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol"}, scopedProxy = ScopedProxyMode.TARGET_CLASS)
    @MapperScan(basePackages = "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol.repository", sqlSessionFactoryRef = "jobSqlSessionFactory")
    public class ChunkOptimisticLockCheckJobConfig {

        @Bean
        @StepScope
        public MyBatisCursorItemReader<Branch> reader(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
                @Value("#{jobParameters['branchId']}") String branchId) {
            Map<String, Object> parameterValues = new HashMap<>();
            parameterValues.put("branchId", branchId);
            return new MyBatisCursorItemReaderBuilder<Branch>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .parameterValues(parameterValues)
                    .queryId(
                            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol.repository.ExclusiveControlRepository.branchFindOne") // (1)
                    .build();
        }

        @Bean
        @StepScope
        public MyBatisBatchItemWriter<ExclusiveBranch> writer(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
                SqlSessionTemplate batchModeSqlSessionTemplate,
                @Value("#{new Boolean(jobParameters['assertUpdates'])}") boolean assertUpdates) {
            return new MyBatisBatchItemWriterBuilder<ExclusiveBranch>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .statementId(
                            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol.repository.ExclusiveControlRepository.branchExclusiveUpdate") // (2)
                    .sqlSessionTemplate(batchModeSqlSessionTemplate)
                    .assertUpdates(assertUpdates) // (3)
                    .build();
        }

        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           MyBatisCursorItemReader<Branch> reader,
                           MyBatisBatchItemWriter<ExclusiveBranch> writer,
                           BranchEditItemProcessor branchEditItemProcessor) {
            return new StepBuilder("chunkOptimisticLockCheckJob.step01",
                    jobRepository)
                    .<Branch, ExclusiveBranch> chunk(10, transactionManager)
                    .reader(reader)
                    .processor(branchEditItemProcessor)
                    .writer(writer)
                    .build();
        }

        @Bean
        public Job chunkOptimisticLockCheckJob(JobRepository jobRepository,
                                                Step step01,
                                                JobExecutionLoggingListener jobExecutionLoggingListener) {
            return new JobBuilder("chunkOptimisticLockCheckJob",jobRepository)
                    .start(step01)
                    .listener(jobExecutionLoggingListener)
                    .build();
        }
    }

XMLConfig

    <!-- (1) -->
    <bean id="reader"
          class="org.mybatis.spring.batch.MyBatisCursorItemReader" scope="step"
          p:queryId="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol.repository.ExclusiveControlRepository.branchFindOne"
          p:sqlSessionFactory-ref="jobSqlSessionFactory"/>
        <property name="parameterValues">
            <map>
                <entry key="branchId" value="#{jobParameters['branchId']}"/>
            </map>
        </property>
    </bean>

    <!-- (2) -->
    <bean id="writer"
          class="org.mybatis.spring.batch.MyBatisBatchItemWriter" scope="step"
          p:statementId="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol.repository.ExclusiveControlRepository.branchExclusiveUpdate"
          p:sqlSessionTemplate-ref="batchModeSqlSessionTemplate"
          p:assertUpdates="true" />  <!-- (3) -->

    <batch:job id="chunkOptimisticLockCheckJob" job-repository="jobRepository">
        <batch:step id="chunkOptimisticLockCheckJob.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="reader" processor="branchEditItemProcessor"
                             writer="writer" commit-interval="10" />
            </batch:tasklet>
        </batch:step>
    </batch:job>

表 145. 説明
項番 | 説明
--- | ---
(1) | 楽観ロックによるデータ取得のSQLIDを設定する。
(2) | 楽観ロックによるデータ更新のSQLIDを設定する。
(3) | バッチ更新の件数を検証有無を設定する。 `true`(デフォルト)に設定すると、更新件数が0件の場合に例外をスローする。 `false`に設定すると、更新件数が0件の場合でも正常処理とする。

###### 5.4.2.2.2. 悲観ロック

チャンクモデルでの悲観ロックについて説明する。

悲観ロックを行うジョブ定義とItemProcessorを以下に示す。

JavaConfig

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan(value = { "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.common",
    "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol"}, scopedProxy = ScopedProxyMode.TARGET_CLASS)
    @MapperScan(basePackages = "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol.repository", sqlSessionTemplateRef = "batchModeSqlSessionTemplate") // (1)
    public class ChunkPessimisticLockCheckJobConfig {

        @Bean
        @StepScope
        public MyBatisCursorItemReader<String> reader(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
                @Value("#{jobParameters['branchName']}") String branchName) {
            Map<String, Object> parameterValues = new HashMap<>();
            parameterValues.put("branchName", branchName);
            return new MyBatisCursorItemReaderBuilder<String>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .parameterValues(parameterValues)
                    .queryId(
                            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol.repository.ExclusiveControlRepository.branchIdFindByName") // (2)
                    .build();
        }

        @Bean
        @StepScope
        public MyBatisBatchItemWriter<ExclusiveBranch> writer(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
                SqlSessionTemplate batchModeSqlSessionTemplate,
                @Value("#{new Boolean(jobParameters['assertUpdates'])}") boolean assertUpdates) {
            return new MyBatisBatchItemWriterBuilder<ExclusiveBranch>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .statementId(
                            "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol.repository.ExclusiveControlRepository.branchUpdate") // (3)
                    .sqlSessionTemplate(batchModeSqlSessionTemplate)
                    .assertUpdates(assertUpdates)
                    .build();
        }

        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           MyBatisCursorItemReader<String> reader,
                           MyBatisBatchItemWriter<ExclusiveBranch> writer,
                           BranchEditWithkPessimisticLockItemProcessor processor) {
            return new StepBuilder("chunkPessimisticLockCheckJob.step01",
                    jobRepository)
                    .<String, ExclusiveBranch> chunk(10, transactionManager)
                    .reader(reader)
                    .processor(processor) // (4)
                    .writer(writer)
                    .build();
        }

        @Bean
        public Job chunkPessimisticLockCheckJob(JobRepository jobRepository,
                                                Step step01,
                                                JobExecutionLoggingListener jobExecutionLoggingListener) {
            return new JobBuilder("chunkPessimisticLockCheckJob",jobRepository)
                    .start(step01)
                    .listener(jobExecutionLoggingListener)
                    .build();
        }
    }

XMLConfig

    <!-- (1) -->
    <mybatis:scan
            base-package="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol.repository"
            template-ref="batchModeSqlSessionTemplate"/>

    <!-- (2) -->
    <bean id="reader" class="org.mybatis.spring.batch.MyBatisCursorItemReader" scope="step"
          p:queryId="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol.repository.ExclusiveControlRepository.branchIdFindByName"
          p:sqlSessionFactory-ref="jobSqlSessionFactory">
        <property name="parameterValues">
            <map>
                <entry key="branchName" value="#{jobParameters['branchName']}"/>
            </map>
        </property>
    </bean>

    <!-- (3) -->
    <bean id="writer" class="org.mybatis.spring.batch.MyBatisBatchItemWriter" scope="step"
          p:statementId="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch05.exclusivecontrol.repository.ExclusiveControlRepository.branchUpdate"
          p:sqlSessionTemplate-ref="batchModeSqlSessionTemplate"
          p:assertUpdates="#{new Boolean(jobParameters['assertUpdates'])}"/>

    <batch:job id="chunkPessimisticLockCheckJob" job-repository="jobRepository">
        <batch:step id="chunkPessimisticLockCheckJob.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <!-- (4) -->
                <batch:chunk reader="reader" processor="branchEditWithkPessimisticLockItemProcessor"
                             writer="writer" commit-interval="3"/>
            </batch:tasklet>
        </batch:step>
    </batch:job>

悲観ロックを行うItemProcessor

    @Component
    @Scope("step")
    public class BranchEditWithkPessimisticLockItemProcessor implements ItemProcessor<String, ExclusiveBranch> {

        // (5)
        @Inject
        ExclusiveControlRepository exclusiveControlRepository;

        // (6)
        @Value("#{jobParameters['branchName']}")
        private String branchName;

        // omitted.

        @Override
        public ExclusiveBranch process(String item) throws Exception {

          // (7)
            Branch branch = exclusiveControlRepository.branchFindOneByNameWithNowWaitLock(item, branchName);

            if (branch != null) {
                ExclusiveBranch updatedBranch = new ExclusiveBranch();

                updatedBranch.setBranchId(branch.getBranchId());
                updatedBranch.setBranchName(branch.getBranchName() + " - " + identifier);
                updatedBranch.setBranchAddress(branch.getBranchAddress() + " - " + identifier);
                updatedBranch.setBranchTel(branch.getBranchTel());
                updatedBranch.setCreateDate(branch.getUpdateDate());
                updatedBranch.setUpdateDate(LocalDateTime.now(clock));
                updatedBranch.setOldBranchName(branch.getBranchName());

                return updatedBranch;
            } else {
                // (8)
                logger.warn("An update by another user occurred. [branchId: {}]", item);
                return null;
            }
        }
    }

表 146. 説明
項番 | 説明
--- | ---
(1) | `Mapper`インタフェースがItemWriterと同じ更新モードになるように、`batchModeSqlSessionTemplate`を設定する。
(2) | 悲観ロックを用いないデータ取得のSQLIDを設定する。 抽出条件として、ジョブ起動パラメータから、`branchName`を設定する。 このSQLによる取得項目は、(6)でデータを一意に特定するのに必要最低限に絞り込むことで性能を向上させることができる。
(3) | 排他制御をしないデータ更新のSQLと同じSQLIDを設定する。
(4) | 悲観ロックによるデータ取得を行うItemProcessorを設定する。
(5) | 悲観ロックによるデータ取得を行う`Mapper`インタフェースをインジェクションする。
(6) | 悲観ロックの抽出条件とするため、ジョブ起動パラメータから、`branchName`を設定する。
(7) | 悲観ロックによるデータ取得のメソッドを呼び出す。 (2)の抽出条件と同じ条件を設定しているため、キー情報(id)の他にジョブ起動パラメータ`branchName`を引数に渡している。 NO WAITやタイムアウトを設定して悲観ロックを行い、他のトランザクションにより排他された時は、ここで例外が発生する。
(8) | 他のトランザクションにより対象データが先に更新されて対象データを取得できない場合、悲観ロックによるデータ取得のメソッドがnullを返却する。 悲観ロックによるデータ取得のメソッドがnullを返却した場合、例外を発生させて処理を中断するなど業務要件に合せた処理が必要となる。 ここでは、WARNログを出力してnullを返却することで後続の処理を継続させる。

| タスクレットモデルでの悲観ロックを行うコンポーネントについて タスクレットモデルで悲観ロックを行う場合は、悲観ロックを行うSQL発行するItemReaderを用いる。`Mapper`インタフェースを直接利用する場合も同様である。

## 6. 異常系への対応

### 6.1. 入力チェック

#### 6.1.1. Overview

本節では、ジョブの入力データに対する妥当性のチェック(以降、入力チェックと呼ぶ)について説明する。

本機能は、チャンクモデルとタスクレットモデルとで同じ使い方になる。

一般的に、バッチ処理における入力チェックは、他システム等から受領したデータに対して、 自システムにおいて妥当であることを確認するために実施する事が多い。
反対に、自システム内の信頼できるデータ(たとえば、データベースに格納されたデータ)に対して、 入力チェックを実施することは不要と言える。

入力チェックはMacchinetta Server 1.xの内容と重複するため、Macchinetta Server 1.x 開発ガイドラインの 入力チェックも合わせて参照。 以下に、主な比較について示す。

表 147. 主な比較一覧
比較対象 | Macchinetta Server 1.x | Macchinetta Batch 2.x
--- | --- | ---
使用できる入力チェックルール | Macchinetta Server 1.xと同様
ルールを付与する対象 | `フォームクラス` | `DTO`
チェックの実行方法 | `Controllerに@Validatedアノテーションを付与する` | `ValidatorクラスのAPIをコールする`
エラーメッセージの設定 | Macchinetta Server 1.x 開発ガイドラインの エラーメッセージの定義と概ね同様だが、 Macchinetta Server 1.xではメッセージキーにフォーム名を含められるのに対し、Macchinetta Batch 2.xではDTO名をメッセージキーに含めることができない。 この差異はチェックの実行方法の違いによるものである。
エラーメッセージの出力先 | 画面 | ログ等

なお、本節で説明対象とする入力チェックは、主にステップが処理する入力データを対象とする。
ジョブパラメータのチェックについてはパラメータの妥当性検証を参照。

##### 6.1.1.1. 入力チェックの分類

入力チェックは、単項目チェック、相関項目チェックに分類される。

表 148. 設定内容の項目一覧
種類 | 説明 | 例 | 実現方法
--- | --- | --- | ---
単項目チェック | 単一のフィールドで完結するチェック | 入力必須チェック 桁チェック 型チェック | Bean Validation(実装ライブラリとしてHibernate Validatorを使用) Hibernate ValidatorはBean Validationで定義されたアノテーションに加え、独自の検証用アノテーションを提供
相関項目チェック | 複数のフィールドを比較するチェック | 数値の大小比較 日付の前後比較 | `org.springframework.validation.Validator`インタフェースを実装した`Validation`クラス または Bean Validation

Springは、Java標準であるBean Validationをサポートしている。 単項目チェックには、このBean Validationを利用する。 相関項目チェックの場合は、Bean ValidationまたはSpringが提供している`org.springframework.validation.Validator`インタフェースを利用する。

この点は、 Macchinetta Server 1.x 開発ガイドラインの 入力チェックの分類 と同様である。

##### 6.1.1.2. 入力チェックの全体像

チャンクモデル、タスクレットモデルにて入力チェックを行うタイミングは以下のとおりである。

- チャンクモデルの場合は`ItemProcessor`で行う。

- タスクレットモデルの場合は`Tasklet#execute()`にて、任意のタイミングで行う。

チャンクモデル、タスクレットモデルにおいて入力チェックの実装方法は同様となるため、 ここではチャンクモデルの`ItemProcessor`で入力チェックを行う場合について説明する。

まず、入力チェックの全体像を説明する。入力チェックに関連するクラスの関係は以下のとおりである。

図 46. 入力チェックの関連クラス

- `ItemProcessor`に、`org.springframework.batch.item.validator.Validator`の実装である `org.springframework.batch.item.validator.SpringValidator`をインジェクションし`validate`メソッドを実行する。

  - `SpringValidator`は内部に`org.springframework.validation.Validator`を保持し、`validate`メソッドを実行する。
    いわば、`org.springframework.validation.Validator`のラッパーといえる。
    `org.springframework.validation.Validator`の実装は、 `org.springframework.validation.beanvalidation.LocalValidatorFactoryBean`となる。 このクラスを通じてHibernate Validatorを使用する。

- 何件目のデータで入力チェックエラーになったのかを判別するために`org.springframework.batch.item.ItemCountAware`を入力DTOに実装する。

| データ件数の設定 `ItemCountAware#setItemCount`は`AbstractItemCountingItemStreamItemReader`によって設定される。 よって、タスクレットモデルで`ItemReader`を使わない場合、更新されない。 この場合は何件目のデータでエラーになったかはユーザにて設定すること。

| jakarta.validation.Validatorやorg.springframework.validation.Validatorといったバリデータは直接使用しない。 `jakarta.validation.Validator`や`org.springframework.validation.Validator`といったバリデータは直接使用せず、 `org.springframework.batch.item.validator.SpringValidator`を使用する。 `SpringValidator`は`org.springframework.validation.Validator`のラッパーである。 `SpringValidator`は発生した例外を`BindException`にラップし、`ValidationException`としてスローする。 そのため、`ValidationException`を通して`BindException`にアクセスでき、柔軟なハンドリングがしやすくなる。 一方、`jakarta.validation.Validator`や`org.springframework.validation.Validator`といったバリデータを直接使用すると、バリデーションエラーになった情報を処理する際に煩雑なロジックになってしまう。

| org.springframework.batch.item.validator.ValidatingItemProcessorは使用しない `org.springframework.validation.Validator`による入力チェックは、 Spring Batchが提供する`ValidatingItemProcessor`を使用しても実現可能である。 しかし、以下の理由により状況によっては拡張を必要としてしまうため、 実装方法を統一する観点より使用しないこととする。 入力チェックエラーをハンドリングし処理を継続することができない。 入力チェックエラーとなったデータに対して柔軟な対応を行うことができない。 入力チェックエラーとなったデータに対しての処理は、利用者によって多種多様(ログ出力のみ、エラーデータを別ファイルに退避する、など)となると想定される。

#### 6.1.2. How to use

先にも述べたが、入力チェックの実現方法は以下のとおりMacchinetta Server 1.xと同様である。

- 単項目チェックは、Bean Validationを利用する。

- 相関項目チェックは、Bean ValidationまたはSpringが提供している`org.springframework.validation.Validator`インタフェースを利用する。

入力チェックの方法について以下の順序で説明する。

- 各種設定

- 入力チェックルールの定義

- 入力チェックの実施

- 入力チェックエラーのハンドリング

##### 6.1.2.1. 各種設定

入力チェックにはHibernate Validatorを使用する。 ライブラリの依存関係にHibernate Validatorの定義があり、必要なBean定義が存在することを確認する。 これらは、Macchinetta Batch 2.xが提供するブランクプロジェクトではすでに設定済である。

依存ライブラリの設定例

    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
    </dependency>

JavaConfig
jp.co.ntt.fw.macchinetta.batch.functionaltest.config.LaunchContextConfig.java

    @Bean
    public SpringValidator<?> validator(Validator beanValidator) {
        final SpringValidator<?> springValidator = new SpringValidator<>();
        springValidator.setValidator(beanValidator);
        return springValidator;
    }

    @Bean
    public Validator beanValidator() {
        try (LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean()) {
            localValidatorFactoryBean.afterPropertiesSet();
            return localValidatorFactoryBean;
        }
    }

XMLConfig
META-INF/spring/launch-context.xml

    <bean id="validator" class="org.springframework.batch.item.validator.SpringValidator"
          p:validator-ref="beanValidator"/>

    <bean id="beanValidator"
          class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />

エラーメッセージの設定

先にも述べたが、エラーメッセージの設定については、 Macchinetta Server 1.x 開発ガイドラインの エラーメッセージの定義を参照。

##### 6.1.2.2. 入力チェックルールの定義

入力チェックのルールを実装する対象は`ItemReader`を通じて取得するDTOである。 `ItemReader`を通じて取得するDTOは以下の要領で実装する。

- 何件目のデータで入力チェックエラーになったのかを判別するために、`org.springframework.batch.item.ItemCountAware`を実装する。

  - `setItemCount`メソッドにて引数で受けた現在処理中のitemが読み込み何件目であるかをあらわす数値をクラスフィールドに保持する。

- 入力チェックルールを定義する。

  - Macchinetta Server 1.x 開発ガイドラインの 入力チェック を参照。

以下に、入力チェックルールを定義したDTOの例を示す。

入力チェックルールを定義したDTOの例

    public class VerificationSalesPlanDetail implements ItemCountAware {  // (1)

        private int count;

        @NotEmpty
        @Size(min = 1, max = 6)
        private String branchId;

        @NotNull
        @Min(1)
        @Max(9999)
        private int year;

        @NotNull
        @Min(1)
        @Max(12)
        private int month;

        @NotEmpty
        @Size(min = 1, max = 10)
        private String customerId;

        @NotNull
        @DecimalMin("0")
        @DecimalMax("9999999999")
        private BigDecimal amount;

        @Override
        public void setItemCount(int count) {
            this.count = count;  // (2)
        }

        // omitted getter/setter
    }

表 149. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `ItemCountAware`クラスを実装し、`setItemCount`メソッドをオーバーライドする。 `ItemCountAware#setItemCount()`は、`ItemReader`が読み込んだデータが何件目であるかを引数に渡される。
(2) | 引数で受ける`count`をクラスフィールドに保持する。 この値は、何件目のデータで入力チェックエラーになったのかを判別するため使用する。

##### 6.1.2.3. 入力チェックの実施

入力チェックの実施方法について説明する。 入力チェック実施は以下の要領で実装する。

- `ItemProcessor`の実装にて、`org.springframework.batch.item.validator.Validator#validate()`を実行する。

  - `Validator`には`SpringValidator`のインスタンスをインジェクトして使用する。

- 入力チェックエラーをハンドリングする。詳細は入力チェックエラーのハンドリングを参照。

入力チェックの実施例を以下に示す。

入力チェックを実施する例

    @Component
    public class ValidateAndContinueItemProcessor implements ItemProcessor<VerificationSalesPlanDetail, SalesPlanDetail> {
        @Inject  // (1)
        Validator<VerificationSalesPlanDetail> validator;

        @Override
        public SalesPlanDetail process(VerificationSalesPlanDetail item) throws Exception {
            try {  // (2)
                validator.validate(item);  // (3)
            } catch (ValidationException e) {
              // omitted exception handling
            }

            SalesPlanDetail salesPlanDetail = new SalesPlanDetail();
            // omitted business logic

            return salesPlanDetail;
        }
    }

表 150. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `SpringValidator`のインスタンスをインジェクトする。 `org.springframework.batch.item.validator.Validator`の型引数には、`ItemReader`を通じて取得するDTOを設定する。
(2) | 入力チェックエラーをハンドリングする。 例では例外をtry/catchで捕捉する方法で処理している。 詳細は入力チェックエラーのハンドリングを参照。
(3) | `ItemReader`を通じて取得するDTOを引数として`Validator#validate()`を実行する。

##### 6.1.2.4. 入力チェックエラーのハンドリング

入力チェックエラーが発生した場合の選択肢は以下の2択となる。

1. 入力チェックエラーが発生した時点で処理を打ち切り、ジョブを異常終了させる。

2. 入力チェックエラーが発生したことをログ等に残し、後続データの処理は継続する。その後、ジョブ終了時に、ジョブを警告終了させる。

###### 6.1.2.4.1. 処理を異常終了する場合

例外発生時に処理を異常終了するためには、`java.lang.RuntimeException`またはそのサブクラスをスローする。

例外発生時にログ出力等の処理を行う方法は以下の2とおりがある。

1. 例外をtry/catchで捕捉し、例外をスローする前に行う。

2. 例外をtry/catchで捕捉せず、`ItemProcessListener`を実装し`onProcessError`メソッドにて行う。

   - `ItemProcessListener#onProcessError()`は`@OnProcessError`アノテーションを使用して実装してもよい。 詳細は、リスナーを参照。

例外発生時に、例外情報をログ出力し、処理を異常終了する例を以下に示す。

try/catchによるハンドリング例

    @Component
    public class ValidateAndAbortByTryCatchItemProcessor implements ItemProcessor<VerificationSalesPlanDetail, SalesPlanDetail> {
        /**
         * Logger.
         */
        private static final Logger logger = LoggerFactory.getLogger(ValidateAndAbortByTryCatchItemProcessor.class);

        @Inject
        Validator<VerificationSalesPlanDetail> validator;

        @Override
        public SalesPlanDetail process(VerificationSalesPlanDetail item) throws Exception {
            try {  // (1)
                validator.validate(item);  // (2)
                return convert(item);
            } catch (ValidationException e) {
                // (3)
                logger.error("Exception occurred in input validation at the {} th item. [message:{}]",
                        item.getCount(), e.getMessage());
                throw e;  // (4)
            }
        }
        private SalesPlanDetail convert(VerificationSalesPlanDetail item) {

            SalesPlanDetail salesPlanDetail = new SalesPlanDetail();
            // omitted business logic

            return salesPlanDetail;
        }
    }

表 151. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | try/catchにて例外を捕捉する。
(2) | 入力チェックを実行する。
(3) | 例外をスローする前にログ出力処理を行う。
(4) | 例外をスローする。 `org.springframework.batch.item.validator.ValidationException`は`RuntimeException`のサブクラスであるため、 そのままスローしなおしてよい。

ItemProcessListener#OnProcessErrorによるハンドリング例

    @Component
    public class ValidateAndAbortItemProcessor implements ItemProcessor<VerificationSalesPlanDetail, SalesPlanDetail> {

        /**
         * Logger.
         */
        private static final Logger logger = LoggerFactory.getLogger(ValidateAndAbortItemProcessor.class);

        @Inject
        Validator<VerificationSalesPlanDetail> validator;

        @Override
        public SalesPlanDetail process(VerificationSalesPlanDetail item) throws Exception {
            validator.validate(item);  // (1)

            SalesPlanDetail salesPlanDetail = new SalesPlanDetail();
            // omitted business logic

            return salesPlanDetail;
        }

        @OnProcessError  // (2)
        void onProcessError(VerificationSalesPlanDetail item, Exception e) {
            // (3)
            logger.error("Exception occurred in input validation at the {} th item. [message:{}]", item.getCount() ,e.getMessage());
        }
    }

表 152. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | 入力チェックを実行する。
(2) | `ItemProcessListener#onProcessError()`を`@OnProcessError`アノテーションを使用して実装する。
(3) | 例外をスローする前にログ出力処理を行う。

| ItemProcessListener#onProcessError()使用時の注意点 `onProcessError`メソッドの利用は業務処理と例外ハンドリングを切り離すことができるためソースコードの可読性、保守性の向上等に有用である。 しかし、上記の例でハンドリング処理を行っている`ValidationException`以外の例外が発生した場合も同じメソッドが実行されるため注意が必要である。 `ItemProcessor#process()`におけるログ出力を例外によって出力し分ける場合は、 `onProcessError`メソッドにて発生した例外の種類を判定して例外処理を行う必要がある。 これが煩雑である場合は、try/catchによるハンドリングにて入力チェックエラーのみを処理し、それ以外はリスナーに移譲するように責務を分担するとよい。

###### 6.1.2.4.2. エラーレコードをスキップする場合

入力チェックエラーが発生したレコードの情報をログ出力等を行った後、エラーが発生したレコードをスキップして後続データの処理を継続する場合は以下の要領で実装する。

- 例外をtry/catchで捕捉する。

- 例外発生時のログ出力等を行う。

- `ItemProcessor#process()`の返り値として`null`を返却する。

  - `null`を返却することで入力チェックエラーが発生したレコードは後続の処理対象(`ItemWriter`による出力)に含まれなくなる。

ItemProcessorによるスキップ例

    @Component
    public class ValidateAndContinueItemProcessor implements ItemProcessor<VerificationSalesPlanDetail, SalesPlanDetail> {
        /**
         * Logger.
         */
        private static final Logger logger = LoggerFactory.getLogger(ValidateAndContinueItemProcessor.class);

        @Inject
        Validator<VerificationSalesPlanDetail> validator;

        @Override
        public SalesPlanDetail process(VerificationSalesPlanDetail item) throws Exception {
            try {  // (1)
                validator.validate(item);  // (2)
            } catch (ValidationException e) {
                // (3)
                logger.warn("Skipping item because exception occurred in input validation at the {} th item. [message:{}]",
                        item.getCount(), e.getMessage());
                // (4)
                return null;  // skipping item
            }

            SalesPlanDetail salesPlanDetail = new SalesPlanDetail();
            // omitted business logic

            return salesPlanDetail;
        }
    }

表 153. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | try/catchにて例外を捕捉する。
(2) | 入力チェックを実行する。
(3) | `null`を返却する前にログ出力処理を行う。
(4) | `null`を返却することで当該データをスキップし次のデータ処理へ移る。

###### 6.1.2.4.3. 終了コードの設定

入力チェックエラーが発生した場合、入力チェックエラーが発生しなかった場合とジョブの状態を区別するために必ず正常終了ではない終了コードを設定すること。
入力チェックエラーが発生したデータをスキップした場合、異常終了した場合においても終了コードの設定は必須である。

終了コードの設定方法については、ジョブの管理を参照。

###### 6.1.2.4.4. エラーメッセージの出力

入力チェックエラーが発生した場合にMessageSourceを使用することで、任意のエラーメッセージを出力することができる。 エラーメッセージの設定については、Macchinetta Server 1.x 開発ガイドラインの エラーメッセージの定義を参照。 エラーメッセージを出力する場合は以下の要領で実装する。

エラーメッセージを出力する方法としては、以下の2とおりがある。

1. レコード内の各項目についてエラーメッセージを出力

2. エラーメッセージをまとめて出力

レコード内の各項目についてエラーメッセージを出力する場合の要領と実装例を以下に示す。

- 入力チェックでエラーが発生した項目に対して、`MessageSource`を用いてエラーメッセージのログ出力を行う。

MessageSourceによるエラーメッセージ出力例

    @Component
    public class ValidateAndMessageItemProcessor implements ItemProcessor<VerificationSalesPlanDetail, SalesPlanDetail> {
        /**
         * Logger.
         */
        private static final Logger logger = LoggerFactory.getLogger(ValidateAndMessageItemProcessor.class);

        @Inject
        Validator<VerificationSalesPlanDetail> validator;

        @Inject
        MessageSource messageSource;  // (1)

        @Override
        public SalesPlanDetail process(VerificationSalesPlanDetail item) throws Exception {
            try {  // (2)
                validator.validate(item);  // (3)
            } catch (ValidationException e) {
                // (4)
                BindException errors = (BindException) e.getCause();

                // (5)
                for (FieldError fieldError : errors.getFieldErrors()) {
                    // (6)
                    logger.warn(messageSource.getMessage(fieldError, Locale.getDefault()) +
                                    "Skipping item because exception occurred in input validation at the {} th item. [message:{}]",
                                        item.getCount(), e.getMessage());
                // (7)
                return null;  // skipping item
            }

            return convert(item);
        }
    }

表 154. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `ResourceBundleMessageSource`のインスタンスをインジェクトする。 `MassageSorce`のBean定義はメッセージ管理を参照。
(2) | try/catchにて例外を捕捉する。
(3) | 入力チェックを実行する。
(4) | `getCause()`で`org.springframework.validation.BindException`を取得する。
(5) | `getFieldErrors()`で1件分の`FiledError`を取得する。
(6) | 取得した`FieldError`を引数にして、`messageSource`でエラーメッセージの出力処理を行う。 1レコード内に3項目のエラーがある場合、3件のエラーメッセージを繰り返し出力する。
(7) | `null`を返却することで当該データをスキップし次のデータ処理へ移る。

エラーメッセージをまとめて出力する場合の要領と実装例を以下に示す。

- `StepExecutionContext`を利用し、入力チェックでエラーが発生した項目のエラーメッセージをリストに格納しておく。

- `AfterStep`で`StepExecutionContext`からリストを取得し、まとめてエラーメッセージのログ出力を行う。

StepExecutionContextを利用したエラーメッセージの一括出力例

    @Component
    @Scope("step")  // (1)
    public class ValidateAndBulkMessageItemProcessor implements ItemProcessor<VerificationSalesPlanDetail, SalesPlanDetail> {

        /**
         * Logger.
         */
        private static final Logger logger = LoggerFactory.getLogger(ValidateAndBulkMessageItemProcessor.class);

        private StepExecution stepExecution;  // (2)

        @Inject
        Validator<VerificationSalesPlanDetail> validator;

        @Inject
        MessageSource messageSource;

        @BeforeStep  // (3)
        public void beforeStep(StepExecution stepExecution) {
            this.stepExecution = stepExecution;  // (4)
        }

        @Override
        public SalesPlanDetail process(VerificationSalesPlanDetail item) throws Exception {
            try {
                validator.validate(item);
            } catch (ValidationException e) {

                BindException errors = (BindException) e.getCause();

                List<String> errorMessageList = new ArrayList<>();  // (5)
                // (6)
                if (stepExecution.getExecutionContext().containsKey("errorMessageList")) {
                    errorMessageList = (List<String>) stepExecution.getExecutionContext().get("errorMessageList");
                }

                // (7)
                for (FieldError fieldError : errors.getFieldErrors()) {
                    String itemNumber = item.getCount() + " th item";
                    String errorMessage = messageSource.getMessage(fieldError, Locale.getDefault());
                    String detailErrorMessage = e.getMessage();

                    String message = MessageFormat
                            .format("{0} Skipping item because exception occurred in input validation at the {1}. [message:{2}]",
                                    errorMessage, itemNumber, detailErrorMessage);

                    errorMessageList.add(message);
                }

                stepExecution.getExecutionContext().put("errorMessageList", errorMessageList);  // (8)

                return null; // skipping item
            }

            return convert(item);
        }

        @AfterStep  // (9)
        public void afterStep(StepExecution stepExecution) {
            ExecutionContext executionContext = stepExecution.getExecutionContext();  // (10)

            List<String> errorMessageList = (List<String>) executionContext.get("errorMessageList");  // (11)
            //  (12)
            for (String errorMessage : errorMessageList) {
                logger.warn(errorMessage);
            }
        }
    }

表 155. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | クラスに`@Scope`アノテーションを付与してスコープを指定する。 スコープは本クラス内で利用する`StepExecution` に合わせて`step`とする。
(2) | `StepExecution`を保持するためのフィールドを定義する。
(3) | `beforeStep`メソッドを実装し、`@BeforeStep`アノテーションを付与する。 シグネチャは`void beforeStep(StepExecution stepExecution)`とする。 `StepExecutionListener`クラスを実装し、`beforeStep`メソッドをオーバーライドする方法でもよい。
(4) | `StepExecution`を取得してクラスフィールドに保持する。
(5) | エラーメッセージを格納するためのリストを定義する。
(6) | `StepExecution`から`stepExecutionContext`を取得し、その中に`errorMessageList`というキーが存在するかチェックする。 `stepExecutionContext`から`errorMessageList`をキーとする値を取得し、(5)で定義したリストに代入する。 その後、`stepExecutionContext`から削除する。
(7) | `getFieldErrors()`で1レコード中で発生したエラーを取得する。 ログ出力用エラーメッセージを生成し、`errorMessageList`に追加する。
(8) | `StepExecution`から`stepExecutionContext`を取得し、`errorMessageList`というキーを指定して`stepExecutionContext`にエラーメッセージを格納したリストを登録する。
(9) | `afterStep`メソッドを実装し、`@AfterStep`アノテーションを付与する。 シグネチャは`void afterStep(StepExecution stepExecution)`とする。 `StepExecutionListener`クラスを実装し、`afterStep`メソッドをオーバーライドする方法でもよい。
(10) | `StepExecution`から`stepExecutionContext`を取得する。
(11) | `errorMessageList`というキーを指定して`stepExecutionContext`からエラーメッセージを格納したリストを取得する。
(12) | エラーメッセージを繰り返しログ出力する。

### 6.2. 例外ハンドリング

#### 6.2.1. Overview

ジョブ実行時に発生する例外のハンドリング方法について説明する。

本機能は、チャンクモデルとタスクレットモデルとで使い方が異なるため、それぞれについて説明する。

まず、例外の分類について説明し、例外の種類に応じたハンドリング方法を説明する。

##### 6.2.1.1. 例外の分類

ジョブ実行時に発生する例外は、以下の3つに分類される。

表 156. 例外の分類一覧
項番 | 分類 | 説明 | 例外の種類
(1) | ジョブの再実行(パラメータ、入力データの変更/修正など)によって発生原因が解消できる例外 | ジョブの再実行で発生原因が解消できる例外は、アプリケーションコードで例外をハンドリングし、例外処理を行う。 | ビジネス例外 正常稼働時に発生するライブラリ例外
(2) | ジョブの再実行によって発生原因が解消できない例外 | ジョブの再実行で発生原因が解消できる例外は、以下のパターンにてハンドリングする。 1. StepListenerで例外の捕捉が可能な場合は、 アプリケーションコードで例外をハンドリングする。 2. StepListenerで例外の捕捉が不可能な場合は、 フレームワークで例外処理をハンドリングする。 | システム例外 予期しないシステム例外 致命的なエラー
(3) | (非同期実行時に)ジョブ要求のリクエスト不正により発生する例外 | ジョブ要求のリクエスト不正により発生する例外は、フレームワークで例外処理をハンドリングし、例外処理を行う。 非同期実行(DBポーリング)の場合は、 ポーリング処理ではジョブ要求に対する妥当性検証をしない。そのため、ジョブ要求を登録するアプリケーションで事前にリクエストに対する入力チェックが行われていることが望ましい。 非同期実行(Webコンテナ)の場合は、 Webアプリケーションにより事前にリクエストに対する入力チェックが行われていることを前提としている。 そのため、ジョブ要求やリクエストを受け付けるアプリケーションで例外ハンドリングを行う。 | ジョブ要求リクエスト不正エラー

| 例外処理内でトランザクショナルな処理は避ける 例外処理内でデータベースへの書き込みを始めとするトランザクショナルな処理を行うと、 二次例外を引き起こしてしまう可能性がある。 例外処理は、解析用ログ出力と終了コード設定を基本とすること。

##### 6.2.1.2. 例外の種類

例外の種類について説明する。

###### 6.2.1.2.1. ビジネス例外

ビジネス例外とは、ビジネスルールの違反を検知したことを通知する例外である。
本例外は、ステップのロジック内で発生させる。
アプリケーションとして想定される状態なので、システム運用者による対処は不要である。

ビジネス例外の例

- 在庫引当時に在庫切れの場合

- 予定日より日数が超過した場合

- etc …​

| 該当する例外クラス `java.lang.RuntimeException`またはそのサブクラス ビジネス例外クラスをユーザにて作成することを推奨する

###### 6.2.1.2.2. 正常稼働時に発生するライブラリ例外

正常稼働時に発生するライブラリ例外とは、フレームワーク、およびライブラリ内で発生する例外のうち、システムが正常稼働している時に発生する可能性のある例外のことを指す。
フレームワーク、およびライブラリ内で発生する例外とは、Spring Frameworkや、その他のライブラリ内で発生する例外クラスを対象とする。
アプリケーションとして想定される状態なので、システム運用者による対処は不要である。

正常稼働時に発生するライブラリ例外の例

- オンライン処理との排他制御で発生する楽観ロック例外

- 複数ジョブやオンライン処理からの同一データを同時登録する際に発生する一意制約例外

- etc …​

| 該当する例外クラス `org.springframework.dao.EmptyResultDataAccessException` (楽観ロックをした時、データ更新件数が0件の場合に発生する例外) `org.springframework.dao.DuplicateKeyException` (一意制約違反となった場合に発生する例外) etc …​

###### 6.2.1.2.3. システム例外

システム例外とは、システムが正常稼働している時に、発生してはいけない状態を検知したことを通知する例外である。
本例外は、ステップのロジック内で発生させる。
システム運用者による対処が必要となる。

システム例外の例

- 事前に存在しているはずのマスタデータ、ディレクトリ、ファイルなどが存在しない場合。

- フレームワーク、ライブラリ内で発生する検査例外のうち、システム異常に分類される例外を捕捉した場合(ファイル操作時のIOExceptionなど)。

- etc…​

| 該当する例外クラス `java.lang.RuntimeException`またはそのサブクラス システム例外クラスを作成することを推奨する

###### 6.2.1.2.4. 予期しないシステム例外

予期しないシステム例外とは、システムが正常稼働している時には発生しない非検査例外である。
システム運用者による対処、またはシステム開発者による解析が必要となる。

予期しないシステム例外は、以下の処理をする以外はハンドリングしない。ハンドリングした場合は、例外を再度スローすること。

- 捕捉例外を解析用にログ出力を行い、該当する終了コードの設定する。

予期しないシステム例外の例

- アプリケーション、フレームワーク、ライブラリにバグが潜んでいる場合。

- データベースサーバなどがダウンしている場合。

- etc…​

| 該当する例外クラス `java.lang.NullPointerException` (バグ起因で発生する例外) `org.springframework.dao.DataAccessResourceFailureException`(データベースサーバがダウンしている場合に発生する例外) etc …​

###### 6.2.1.2.5. 致命的なエラー

致命的なエラーとは、システム(アプリケーション)全体に影響を及ぼす、致命的な問題が発生している事を通知するエラーである。
システム運用者、またはシステム開発者による対処・リカバリが必要となる。

致命的なエラーは、以下の処理をする以外はハンドリングしない。ハンドリングした場合は、例外を再度スローすること。

- 捕捉例外を解析用にログ出力を行い、該当する終了コードの設定する。

致命的なエラーの例

- Java仮想マシンで使用できるメモリが不足している場合。

- etc…​

| 該当する例外クラス `java.lang.Error`を継承しているクラス java.lang.OutOfMemoryError (メモリ不足時に発生するエラー)など etc …​

###### 6.2.1.2.6. ジョブ要求リクエスト不正エラー

ジョブ要求リクエスト不正エラーとは、非同期実行時にジョブ要求のリクエストに問題が発生していることを通知するエラーである。
システム運用者による対処・リカバリが必要となる。

ジョブ要求リクエスト不正エラーは、ジョブ要求のリクエストを処理するアプリケーションでの例外ハンドリングを前提にするため、 本ガイドラインでは説明はしない。

##### 6.2.1.3. 例外への対応方法

例外への対応方法について説明する。

例外への対応パターンは次のとおり。

1. 例外発生時にジョブの継続可否を決める (3種類)

2. 中断したジョブの再実行方法を決める (2種類)

表 157. ジョブの継続可否を決定する方法
項番 | 例外への対応方法 | 説明
--- | --- | ---
(1) | スキップ | エラーレコードをスキップし、処理を継続する。
(2) | リトライ | エラーレコードを指定した条件(回数、時間等)に達するまで再処理する。
(3) | 処理中断 | 処理を中断する。

| 例外が発生していなくても、ジョブが想定以上の処理時間になったため処理途中で停止する場合がある。 この場合は、ジョブの停止を参照。

表 158. 中断したジョブの再実行方法
項番 | 例外への対応方法 | 説明
--- | --- | ---
(1) | ジョブのリラン | 中断したジョブを最初から再実行する。
(2) | ジョブのリスタート | 中断したジョブを中断した箇所から再実行する。

中断したジョブの再実行方法についての詳細は、処理の再実行を参照。

###### 6.2.1.3.1. スキップ

スキップとは、バッチ処理を止めずにエラーデータを飛ばして処理を継続する方法である。

スキップを行う例

- 入力データ内に不正なレコードが存在する場合

- ビジネス例外が発生した場合

- etc …​

| スキップレコードの再処理 スキップを行う場合は、スキップした不正なレコードについてどのように対応するか設計すること。 不正なレコードを抽出して再処理する場合、次回実行時に含めて処理する場合、などといった方法が考えられる。

###### 6.2.1.3.2. リトライ

リトライとは、特定の処理に失敗したレコードに対して指定した回数や時間に達するまで再試行を繰り返す対応方法である。
処理失敗の原因が実行環境に依存しており、かつ、時間の経過により解決される見込みのある場合にのみ用いる。

リトライを行う例

- 排他制御により、処理対象のレコードがロックされている場合

- ネットワークの瞬断によりメッセージ送信が失敗する場合

- etc …​

| リトライの適用 リトライをあらゆる場面で適用してしまうと、異常発生時に処理時間がむやみに伸びてしまい、異常の検出が遅れる危険がある。 よって、リトライは処理のごく一部に適用することが望ましく、 その対象は外部システム連携など信頼性が担保しにくいものに限定するとよい。

###### 6.2.1.3.3. 処理中断

処理中断とは、文字どおり処理を途中で中断する対応方式である。
処理の継続が不可能な内容のエラーが検知された場合や、レコードのスキップを許容しない要件の場合に用いる。

処理中断を行う例

- 入力データ内に不正なレコードが存在する場合

- ビジネス例外が発生した場合

- etc …​

#### 6.2.2. How to use

例外ハンドリングの実現方法について説明をする。

バッチアプリケーション運用時のユーザインタフェースはログが主体である。よって、例外発生の監視もログを通じて行うことになる。

Spring Batch では、ステップ実行時に例外が発生した場合はログを出力し異常終了するため、ユーザにて追加実装をせずとも要件を満たせる可能性がある。 以降の説明は、ユーザにてシステムに応じたログ出力を行う必要があるときのみ、ピンポイントに実装するとよい。 すべての処理を実装しなくてはならないケースは基本的にはない。

例外ハンドリングの共通であるログ設定については、ロギングを参照。

##### 6.2.2.1. ステップ単位の例外ハンドリング

ステップ単位での例外ハンドリング方法について説明する。

ChunkListenerインタフェースによる例外ハンドリング

  処理モデルによらず、発生した例外を統一的にハンドリングしたい場合は、 `ChunkListener`インタフェースを利用する。
  チャンクよりスコープの広い、ステップやジョブのリスナーを利用しても実現できるが、 出来る限り発生した直後にハンドリングすることを重視し、`ChunkListener`を採用する。

各処理モデルごとの例外ハンドリング方法は以下のとおり。

チャンクモデルにおける例外ハンドリング

  Spring Batch提供の各種`Listener`インタフェースを使用して機能を実現する。

タスクレットモデルにおける例外ハンドリング

  タスクレット実装内にて独自に例外ハンドリングを実装する。

| ChunkListenerで統一的にハンドリングできるのはなぜか `ChunkListener`によってタスクレット実装内で発生した例外をハンドリングできることに違和感を感じるかもしれない。 これは、Spring Batch においてビジネスロジックの実行はチャンクを基準に考えられており、 1回のタスクレット実行は、1つのチャンク処理として扱われているためである。 この点は`org.springframework.batch.core.step.tasklet.Tasklet`のインタフェースにも表れている。
    public interface Tasklet {
      RepeatStatus execute(StepContribution contribution,
              ChunkContext chunkContext) throws Exception;
    }

###### 6.2.2.1.1. ChunkListenerインタフェースによる例外ハンドリング

`ChunkListener`インタフェースの`afterChunkError`メソッドを実装する。
`afterChunkError`メソッドの引数である`ChunkContext`から`ChunkListener.ROLLBACK_EXCEPTION_KEY`をキーにしてエラー情報を取得する。

リスナーの設定方法については、リスナーの設定を参照。

ChunkListenerの実装例

    @Component
    public class ChunkAroundListener implements ChunkListener {

        private static final Logger logger =
                LoggerFactory.getLogger(ChunkAroundListener.class);

        @Override
        public void beforeChunk(ChunkContext context) {
            logger.info("before chunk. [context:{}]", context);
        }

        @Override
        public void afterChunk(ChunkContext context) {
            logger.info("after chunk. [context:{}]", context);
        }

        // (1)
        @Override
        public void afterChunkError(ChunkContext context) {
            logger.error("Exception occurred while chunk. [context:{}]",
                    context.getAttribute(ChunkListener.ROLLBACK_EXCEPTION_KEY)); // (2)
        }
    }

表 159. 説明
項番 | 説明
--- | ---
(1) | `afterChunkError`メソッドを実装する。
(2) | `ChunkContext`から`ChunkListener.ROLLBACK_EXCEPTION_KEY`をキーにしてエラー情報を取得する。 この例では、取得した例外のスタックトレースをログ出力している。

| 処理モデルの違いによる`ChunkListener`の挙動の違い チャンクモデルでは、リソースのオープン･クローズで発生した例外は、`ChunkListener`インタフェースが捕捉するスコープ外となる。 そのため、`afterChunkError`メソッドでハンドリングが行われない。 概略図を以下に示す。 図 47. チャンクモデルでの例外ハンドリング概略図 タスクレットモデルでは、リソースのオープン･クローズで発生した例外は、`ChunkListener`インタフェースが捕捉するスコープ内となる。 そのため、`afterChunkError`メソッドでハンドリングが行わる。 概略図を以下に示す。 図 48. タスクレットモデルでの例外ハンドリング概略図 この挙動の差を吸収して統一的に例外をハンドリングしたい場合は、 `StepExecutionListener`インタフェースで例外の発生有無をチェックすることで実現できる。 ただし、`ChunkListener`よりも実装が少々複雑になる。 StepExecutionListenerの実装例
    @Component
    public class StepErrorLoggingListener implements StepExecutionListener {

        private static final Logger logger =
                LoggerFactory.getLogger(StepErrorLoggingListener.class);

        @Override
        public void beforeStep(StepExecution stepExecution) {
            // do nothing.
        }

        // (1)
        @Override
        public ExitStatus afterStep(StepExecution stepExecution) {
            // (2)
            List<Throwable> exceptions = stepExecution.getFailureExceptions();
            // (3)
            if (exceptions.isEmpty()) {
                return ExitStatus.COMPLETED;
            }

            // (4)
            logger.info("This step has occurred some exceptions as follow. " +
                    "[step-name:{}] [size:{}]",
                    stepExecution.getStepName(), exceptions.size());
            for (Throwable th : exceptions) {
                logger.error("exception has occurred in job.", th);
            }
            return ExitStatus.FAILED;
        }
表 160. 説明
項番 | 説明
--- | ---
(1) | `afterStep`メソッドを実装する。
(2) | 引数の`stepExecution`からエラー情報を取得する。複数の例外をまとめて扱う必要がある点に注意する。
(3) | エラー情報がない場合は、正常終了とする。
(4) | エラー情報がある場合は、例外ハンドリングを行う。 この例では、発生した例外をすべてスタックトレース付きのログ出力を行っている。

###### 6.2.2.1.2. チャンクモデルにおける例外ハンドリング

チャンクモデルでは、 StepListenerを継承したListenerで例外ハンドリングする。

リスナーの設定方法については、リスナーの設定を参照。

コーディングポイント(ItemReader編)

`ItemReadListener`インタフェースの `onReadError`メソッドを実装することで、ItemReader内で発生した例外をハンドリングする。

ItemReadListener#onReadErrorの実装例

    @Component
    public class ChunkComponentListener implements ItemReadListener<Object> {

        private static final Logger logger =
                LoggerFactory.getLogger(ChunkComponentListener.class);

        // omitted.

        // (1)
        @Override
        public void onReadError(Exception ex) {
            logger.error("Exception occurred while reading.", ex);  // (2)
        }

        // omitted.
    }

表 161. 説明
項番 | 説明
--- | ---
(1) | `onReadError`メソッドを実装する。
(2) | 例外ハンドリングを実装する この例では、引数から取得した例外のスタックトレースをログ出力している。

コーディングポイント(ItemProcessor編)

ItemProcessorでの例外ハンドリングには、2つの方法があり、要件に応じて使い分ける。

1. ItemProcessor 内でtry～catchをする方法

2. `ItemProcessListener`インタフェースを使用する方法

使い分ける理由について説明する。
ItemProcessorの処理内で例外発生時に実行される`onProcessError`メソッドの引数は、処理対処のアイテムと例外の2つである。
システムの要件によっては、`ItemProcessListener`インタフェース内でログ出力等の例外をハンドリングする際に、この2つの引数で要件を満たせない場合が出てくる。 その場合は、ItemProcessor内でtry～catchにて例外をcatchし例外ハンドリング処理を行うことを推奨する。
注意点として、ItemProcessor内でtry～catchを実装した上で、`ItemProcessListener`インタフェースを実装すると二重処理になる場合があるため、注意が必要である。
きめ細かい例外ハンドリングを行いたい場合は、ItemProcessor内でtry～catchをする方法を採用すること。

それぞれの方法について説明する。

ItemProcessor 内でtry～catchする方法

  きめ細かい例外ハンドリングが必要になる場合はこちらを使用する。
  後述するスキップの項で説明するが、エラーレコードのスキップを行う際にはこちらを使用することとなる。

ItemProcessor内でtry～catchする実装例

    @Component
    public class AmountCheckProcessor implements
            ItemProcessor<SalesPerformanceDetail, SalesPerformanceDetail> {

        // omitted.

        @Override
        public SalesPerformanceDetail process(SalesPerformanceDetail item)
                throws Exception {
            // (1)
            try {
                checkAmount(item.getAmount(), amountLimit);
            } catch (ArithmeticException ae) {
                // (2)
                logger.error(
                    "Exception occurred while processing. [item:{}]", item, ae);
                // (3)
                throw new IllegalStateException("check error at processor.", ae);
            }
            return item;
        }
    }

表 162. 説明
項番 | 説明
--- | ---
(1) | `try～catch`で実装する。ここでは、特定の例外(`ArithmeticException`)のみ特別なハンドリングをしている。
(2) | 例外ハンドリングを実装する この例では、引数から取得した例外のスタックトレースをログ出力している。
(3) | トランザクションのロールバック例外をスローする。 また、この例外スローにより`ItemProcessListener`で共通の例外ハンドリングをすることもできる。

ItemProcessListenerインタフェースを使用する方法

  業務例外に対するハンドリングが共通化できる場合はこちらを使用する。

ItemProcessListener#onProcessErrorの実装例

    @Component
    public class ChunkComponentListener implements ItemProcessListener<Object, Object> {

        private static final Logger logger =
                LoggerFactory.getLogger(ChunkComponentListener.class);

        // omitted.

        // (1)
        @Override
        public void onProcessError(Object item, Exception e) {
            // (2)
            logger.error("Exception occurred while processing. [item:{}]", item, e);
        }

        // omitted.
    }

表 163. 説明
項番 | 説明
--- | ---
(1) | `onProcessError`メソッドを実装する。
(2) | 例外ハンドリングを実装する この例では、引数から取得した処理対象データと例外のスタックトレースをログ出力している。

コーディングポイント(ItemWriter編)

`ItemWriteListener`インタフェースの `onWriteError`メソッドを実装することで、ItemWriter内で発生した例外をハンドリングする。

ItemWriteListener#onWriteErrorの実装例

    @Component
    public class ChunkComponentListener implements ItemWriteListener<Object> {

        private static final Logger logger =
                LoggerFactory.getLogger(ChunkComponentListener.class);

        // omitted.

        // (1)
        @Override
        public void onWriteError(Exception ex, Chunk item) {
            // (2)
            logger.error("Exception occurred while processing. [items:{}]", item, ex);
        }

        // omitted.
    }

表 164. 説明
項番 | 説明
--- | ---
(1) | `onWriteError`メソッドを実装する。
(2) | 例外ハンドリングを実装する この例では、引数から取得した出力対象のチャンクと例外のスタックトレースをログ出力している。

###### 6.2.2.1.3. タスクレットモデルにおける例外ハンドリング

タスクレットモデルの例外ハンドリングはタスクレット内で独自に実装する。

トランザクション処理を行う場合は、ロールバックさせるために必ず例外を再度スローすること。

タスクレットモデルでの例外ハンドリング実装例

    @Component
    public class SalesPerformanceTasklet implements Tasklet {

        private static final Logger logger =
                LoggerFactory.getLogger(SalesPerformanceTasklet.class);

        // omitted.

        @Override
        public RepeatStatus execute(StepContribution contribution,
                ChunkContext chunkContext) throws Exception {

            // (1)
            try {
                reader.open(chunkContext.getStepContext().getStepExecution()
                        .getExecutionContext());

                List<SalesPerformanceDetail> items = new ArrayList<>(10);
                SalesPerformanceDetail item = null;
                do {
                    // Pseudo operation of ItemReader
                    // omitted.

                    // Pseudo operation of ItemProcessor
                    checkAmount(item.getAmount(), amountLimit);

                    // Pseudo operation of ItemWriter
                    // omitted.

                } while (item != null);
            } catch (Exception e) {
                logger.error("exception in tasklet.", e);   // (2)
                throw e;    // (3)
            } finally {
                try {
                    reader.close();
                } catch (Exception e) {
                    // do nothing.
                }
            }

            return RepeatStatus.FINISHED;
        }
    }

表 165. 説明
項番 | 説明
--- | ---
(1) | `try-catch`を実装する。
(2) | 例外ハンドリングを実装する この例では、発生した例外のスタックトレースをログ出力している。
(3) | トランザクションをロールバックするため、例外を再度スローする。

##### 6.2.2.2. ジョブ単位の例外ハンドリング

ジョブ単位に例外ハンドリング方法を説明する。
チャンクモデルとタスクレットモデルとで共通のハンドリング方法となる。

システム例外や致命的エラー等エラーはジョブ単位に `JobExecutionListener`インタフェースの実装を行う。

例外ハンドリング処理を集約して定義するために、ステップごとにハンドリング処理を定義はせずジョブ単位でハンドリングを行う。
ここでの例外ハンドリングは、ログ出力、およびExitCodeの設定を行い、トランザクション処理は実装しないこと。

| トランザクション処理の禁止 `JobExecutionListener`で行われる処理は、業務トランザクション管理の範囲外となる。 よってジョブ単位の例外ハンドリングでトランザクション処理を実施することは禁止する。

ここでは、ItemProcessorで例外が発生したときのハンドリング例を示す。 リスナーの設定方法については、リスナーの設定を参照。

ItemProcessorの実装例

    @Component
    public class AmountCheckProcessor implements
            ItemProcessor<SalesPerformanceDetail, SalesPerformanceDetail> {

        // omitted.

        private StepExecution stepExecution;

        // (1)
        @BeforeStep
        public void beforeStep(StepExecution stepExecution) {
            this.stepExecution = stepExecution;
        }

        @Override
        public SalesPerformanceDetail process(SalesPerformanceDetail item)
                throws Exception {
            // (2)
            try {
                checkAmount(item.getAmount(), amountLimit);
            } catch (ArithmeticException ae) {
                // (3)
                stepExecution.getExecutionContext().put("ERROR_ITEM", item);
                // (4)
                throw new IllegalStateException("check error at processor.", ae);
            }
            return item;
        }
    }

JobExecutionListenerでの例外ハンドリング実装

    @Component
    public class JobErrorLoggingListener implements JobExecutionListener {

        private static final Logger logger =
                LoggerFactory.getLogger(JobErrorLoggingListener.class);

        @Override
        public void beforeJob(JobExecution jobExecution) {
            // do nothing.
        }

        // (5)
        @Override
        public void afterJob(JobExecution jobExecution) {

            // whole job execution
            List<Throwable> exceptions = jobExecution.getAllFailureExceptions(); // (6)
            // (7)
            if (exceptions.isEmpty()) {
                return;
            }
            // (8)
            logger.info("This job has occurred some exceptions as follow. " +
                    "[job-name:{}] [size:{}]",
                    jobExecution.getJobInstance().getJobName(), exceptions.size());
            for (Throwable th : exceptions) {
                logger.error("exception has occurred in job.", th);
            }
            // (9)
            for (StepExecution stepExecution : jobExecution.getStepExecutions()) {
                Object errorItem = stepExecution.getExecutionContext()
                        .get("ERROR_ITEM"); // (10)
                if (errorItem != null) {
                    logger.error("detected error on this item processing. " +
                            "[step:{}] [item:{}]", stepExecution.getStepName(),
                            errorItem);
                }
            }

        }
    }

表 166. 説明
項番 | 説明
--- | ---
(1) | `JobExecutionListener`でエラーデータを出力するため、ステップ実行前に`StepExecution`インスタンスを取得する。
(2) | `try-catch`を実装する。
(3) | 例外ハンドリングを実装する この例では、`StepExecution`インスタンスのコンテキストにエラーデータを`ERROR_ITEM`というキーで格納している。
(4) | `JobExecutionListener`で例外ハンドリングをするために、例外をスローする。
(5) | `afterJob`メソッドに例外ハンドリングを実装する。
(6) | 引数の`jobExecution`からジョブ全体で発生したエラー情報を取得する。
(7) | エラー情報がない場合は、正常終了とする。
(8) | エラー情報がある場合は、例外ハンドリングを行う。 この例では、発生した例外をすべてスタックトレース付きのログ出力を行っている。
(9) | この例では、エラーデータがある場合はログ出力を行うようにしている。 ジョブで定義されたすべてのステップから`StepExecution`インスタンスを取得し、`ERROR_ITEM`というキーでエラーデータが格納されているかチェックする。 格納されていた場合は、エラーデータとしてログ出力する。

| ExecutionContextへ格納するオブジェクト `ExecutionContext`へ格納するオブジェクトは、`java.io.Serializable`を実装したクラスでなければならない。 これは、`ExecutionContext`が`JobRepository`へ格納されるためである。

##### 6.2.2.3. 処理継続可否の決定

例外発生時にジョブの処理継続可否を決定する実装方法を説明する。

処理継続可否方法一覧

- スキップ

- リトライ

- 処理中断

###### 6.2.2.3.1. スキップ

エラーレコードをスキップして、処理を継続する方法を説明する。

チャンクモデル

チャンクモデルでは、各処理のコンポーネントで実装方法が異なる。

| ここで説明する内容を適用する前に、必ず<skippable-exception-classes>を使わない理由についてを一読すること。

- ItemReaderでのスキップ

- ItemProcessorでのスキップ

- ItemWriterでのスキップ

ItemReaderでのスキップ

  `StepBuilderのskipPolicyメソッド/<batch:chunk>のskip-policy属性`にスキップ方法を指定する。 `skipメソッド/<batch:skippable-exception-classes>`に、スキップ対象とするItemReaderで発生する例外クラスを指定する。
  `skipPolicyメソッドの引数skipPolicy/skip-policy属性`には、Spring Batchが提供している下記に示すいづれかのクラスを使用する。

表 167. skip-policy一覧
クラス名 | 説明
--- | ---
AlwaysSkipItemSkipPolicy | 常にスキップをする。
NeverSkipItemSkipPolicy | スキップをしない。
LimitCheckingItemSkipPolicy | 指定したスキップ数の上限に達するまでスキップをする。 上限値に達した場合は、以下の例外が発生する。 `org.springframework.batch.core.step.skip.SkipLimitExceededException` `skip-policy`を省略した時にデフォルトで使われるスキップ方法である。
ExceptionClassifierSkipPolicy | 例外ごとに適用する`skip-policy`を変えたい場合に利用する。

スキップの実装例を説明する。

`FlatFileItemReader`でCSVファイルを読み込む際、不正なレコードが存在するケースを扱う。
なお、この時以下の例外が発生する。

- `org.springframework.batch.item.ItemReaderException`(ベースとなる例外クラス)

  - `org.springframework.batch.item.file.FlatFileParseException` (発生する例外クラス)

`skip-policy`別に定義方法を示す。

JavaConfig

    @Bean
    @StepScope
    public FlatFileItemReader<SalesPerformanceDetail> detailCSVReader(
            @Value("#{jobParameters['inputFile']}") File inputFile) {
        final DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
        tokenizer.setNames("branchId", "year", "month", "customerId", "amount");
        final BeanWrapperFieldSetMapper<SalesPerformanceDetail> fieldSetMapper = new BeanWrapperFieldSetMapper<SalesPerformanceDetail>();
        fieldSetMapper.setTargetType(SalesPerformanceDetail.class);
        final DefaultLineMapper<SalesPerformanceDetail> lineMapper = new DefaultLineMapper<SalesPerformanceDetail>();
        lineMapper.setLineTokenizer(tokenizer);
        lineMapper.setFieldSetMapper(fieldSetMapper);
        return new FlatFileItemReaderBuilder<SalesPerformanceDetail>()
                .name(ClassUtils.getShortName(FlatFileItemReader.class))
                .resource(new FileSystemResource(inputFile))
                .lineMapper(lineMapper)
                .build();
    }

XMLConfig

    <bean id="detailCSVReader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
        <property name="resource" value="file:#{jobParameters['inputFile']}"/>
        <property name="lineMapper">
            <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
                <property name="lineTokenizer">
                    <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
                          p:names="branchId,year,month,customerId,amount"/>
                </property>
                <property name="fieldSetMapper">
                    <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
                          p:targetType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.performance.SalesPerformanceDetail"/>
                </property>
            </bean>
        </property>
    </bean>

AlwaysSkipItemSkipPolicy

  .AlwaysSkipItemSkipPolicyの指定例

JavaConfig

    // (1)
    @Bean
    public AlwaysSkipItemSkipPolicy skipPolicy() {
        AlwaysSkipItemSkipPolicy skipPolicy = new AlwaysSkipItemSkipPolicy();
        return skipPolicy;
    }

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       @Qualifier("detailCSVReader") ItemReader<SalesPerformanceDetail> reader,
                       @Qualifier("amountCheckProcessor") ItemProcessor<SalesPerformanceDetail, SalesPerformanceDetail> processor,
                       @Qualifier("detailWriter") ItemWriter<SalesPerformanceDetail> writer,
                       AlwaysSkipItemSkipPolicy skipPolicy,
                       SkipLoggingListener listener) {
        return new StepBuilder("jobSalesPerfAtSkipAllReadError.step01",
                jobRepository)
                .<SalesPerformanceDetail, SalesPerformanceDetail> chunk(10,
                        transactionManager)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .faultTolerant()
                .skipPolicy(skipPolicy) // (2)
                .listener(listener)
                .build();
    }

    @Bean
    public Job jobSalesPerfAtSkipAllReadError(JobRepository jobRepository,
                                              Step step01,
                                              JobExecutionLoggingListener listener) {
        return new JobBuilder("jobSalesPerfAtSkipAllReadError", jobRepository)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig

    <!-- (1) -->
    <bean id="skipPolicy"
          class="org.springframework.batch.core.step.skip.AlwaysSkipItemSkipPolicy"/>

    <batch:job id="jobSalesPerfAtSkipAllReadError" job-repository="jobRepository">
        <batch:step id="jobSalesPerfAtSkipAllReadError.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="detailCSVReader"
                             processor="amountCheckProcessor"
                             writer="detailWriter" commit-interval="10"
                             skip-policy="skipPolicy"> <!-- (2) -->
                </batch:chunk>
            </batch:tasklet>
        </batch:step>
    </batch:job>

表 168. 説明
項番 | 説明
--- | ---
(1) | `AlwaysSkipItemSkipPolicy`をBean定義する。
(2) | `StepBuilderのskipPolicyメソッド/<batch:chunk>のskip-policy属性`に(1)で定義したBeanを設定する。

NeverSkipItemSkipPolicy

  .NeverSkipItemSkipPolicyの指定例

JavaConfig

    // (1)
    @Bean
    public NeverSkipItemSkipPolicy skipPolicy() {
        return new NeverSkipItemSkipPolicy();
    }

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       @Qualifier("detailCSVReader") ItemReader<SalesPerformanceDetail> reader,
                       @Qualifier("amountCheckProcessor") ItemProcessor<SalesPerformanceDetail, SalesPerformanceDetail> processor,
                       @Qualifier("detailWriter") ItemWriter<SalesPerformanceDetail> writer,
                       SkipLoggingListener skipLoggingListener,
                       NeverSkipItemSkipPolicy skipPolicy) {
        return new StepBuilder("jobSalesPerfAtSkipNeverReadError.step01",
                jobRepository)
                .<SalesPerformanceDetail, SalesPerformanceDetail> chunk(10,
                        transactionManager)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .faultTolerant()
                .listener(skipLoggingListener)
                .skipPolicy(skipPolicy) // (2)
                .build();
    }

    @Bean
    public Job jobSalesPerfAtSkipNeverReadError(JobRepository jobRepository,
                                              Step step01,
                                              JobExecutionLoggingListener listener) {
        return new JobBuilder("jobSalesPerfAtSkipNeverReadError", jobRepository)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig

    <!-- (1) -->
    <bean id="skipPolicy"
          class="org.springframework.batch.core.step.skip.NeverSkipItemSkipPolicy"/>

    <batch:job id="jobSalesPerfAtSkipNeverReadError" job-repository="jobRepository">
        <batch:step id="jobSalesPerfAtSkipNeverReadError.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="detailCSVReader"
                             processor="amountCheckProcessor"
                             writer="detailWriter" commit-interval="10"
                             skip-policy="skipPolicy"> <!-- (2) -->
                </batch:chunk>
            </batch:tasklet>
        </batch:step>
    </batch:job>

表 169. 説明
項番 | 説明
--- | ---
(1) | `NeverSkipItemSkipPolicy`をBean定義する。
(2) | `StepBuilderのskipPolicyメソッド/<batch:chunk>のskip-policy属性`に(1)で定義したBeanを設定する。

LimitCheckingItemSkipPolicy

  .LimitCheckingItemSkipPolicyの指定例

JavaConfig

    // (1)
    // @Bean
    // public LimitCheckingItemSkipPolicy skipPolicy() {
    //     return new LimitCheckingItemSkipPolicy();
    // }

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       @Qualifier("detailCSVReader") ItemReader<SalesPerformanceDetail> reader,
                       @Qualifier("amountCheckProcessor") ItemProcessor<SalesPerformanceDetail, SalesPerformanceDetail> processor,
                       @Qualifier("detailWriter") ItemWriter<SalesPerformanceDetail> writer,
                       SkipLoggingListener listener) {
        return new StepBuilder("jobSalesPerfAtValidSkipReadError.step01",
                jobRepository)
                .<SalesPerformanceDetail, SalesPerformanceDetail> chunk(10,
                        transactionManager)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .faultTolerant()
                .skipLimit(2) // (2)
                .skip(ItemReaderException.class) // (3) (4)
                .listener(listener)
                .build();
    }

    @Bean
    public Job jobSalesPerfAtValidSkipReadError(JobRepository jobRepository,
                                                Step step01,
                                                JobExecutionLoggingListener listener) {
        return new JobBuilder("jobSalesPerfAtValidSkipReadError", jobRepository)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig

    <!-- (1) -->
    <!--
    <bean id="skipPolicy"
          class="org.springframework.batch.core.step.skip.LimitCheckingItemSkipPolicy"/>
    -->

    <batch:job id="jobSalesPerfAtValidSkipReadError" job-repository="jobRepository">
        <batch:step id="jobSalesPerfAtValidSkipReadError.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="detailCSVReader"
                             processor="amountCheckProcessor"
                             writer="detailWriter" commit-interval="10"
                             skip-limit="2">  <!-- (2) -->
                    <!-- (3) -->
                    <batch:skippable-exception-classes>
                        <!-- (4) -->
                        <batch:include
                            class="org.springframework.batch.item.ItemReaderException"/>
                    </batch:skippable-exception-classes>
                </batch:chunk>
            </batch:tasklet>
        </batch:step>
    </batch:job>

表 170. 説明
項番 | 説明
--- | ---
(1) | `LimitCheckingItemSkipPolicy`をBean定義する。 `StepBuilderのskipPolicyメソッド/skip-policy属性`省略時のデフォルトであるため、定義しなくてもよい。
(2) | `StepBuilderのskipLimitメソッド/<batch:chunk>のskip-limit属性`にスキップ数の上限値を設定する。 `skipPolicyメソッド/skip-policy属性`はデフォルトを使用ため省略。
(3) | `StepBuilderのskipメソッド/<batch:skippable-exception-classes>`を定義し、要素内に対象となる例外を設定する。
(4) | `ItemReaderException`をスキップ対象クラスとして設定を行う。

ExceptionClassifierSkipPolicy

  .ExceptionClassifierSkipPolicyの指定例

JavaConfig

    // (1)
    @Bean
    public ExceptionClassifierSkipPolicy skipPolicy(
            AlwaysSkipItemSkipPolicy alwaysSkip) {
        ExceptionClassifierSkipPolicy skipPolicy = new ExceptionClassifierSkipPolicy();
        Map<Class<? extends Throwable>, SkipPolicy> policyMap = new HashMap<>();
        policyMap.put(ItemReaderException.class, alwaysSkip); // (2)
        skipPolicy.setPolicyMap(policyMap);
        return skipPolicy;
    }

    // (3)
    @Bean
    public AlwaysSkipItemSkipPolicy alwaysSkip() {
        return new AlwaysSkipItemSkipPolicy();
    }

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       @Qualifier("detailCSVReader") ItemReader<SalesPerformanceDetail> reader,
                       @Qualifier("amountCheckProcessor") ItemProcessor<SalesPerformanceDetail, SalesPerformanceDetail> processor,
                       @Qualifier("detailWriter") ItemWriter<SalesPerformanceDetail> writer,
                       SkipLoggingListener skipLoggingListener,
                       ExceptionClassifierSkipPolicy skipPolicy) {
        return new StepBuilder("jobSalesPerfAtValidNolimitSkipReadError.step01",
                jobRepository)
                .<SalesPerformanceDetail, SalesPerformanceDetail> chunk(10,
                        transactionManager)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .faultTolerant()
                .skipPolicy(skipPolicy) // (4)
                .listener(skipLoggingListener)
                .build();
    }

    @Bean
    public Job jobSalesPerfAtValidNolimitSkipReadError(JobRepository jobRepository,
                                              Step step01,
                                              JobExecutionLoggingListener listener) {
        return new JobBuilder("jobSalesPerfAtValidNolimitSkipReadError", jobRepository)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig

    <!-- (1) -->
    <bean id="skipPolicy"
          class="org.springframework.batch.core.step.skip.ExceptionClassifierSkipPolicy">
        <property name="policyMap">
            <map>
                <!-- (2) -->
                <entry key="org.springframework.batch.item.ItemReaderException"
                       value-ref="alwaysSkip"/>
            </map>
        </property>
    </bean>
    <!-- (3) -->
    <bean id="alwaysSkip"
          class="org.springframework.batch.core.step.skip.AlwaysSkipItemSkipPolicy"/>

    <batch:job id="jobSalesPerfAtValidNolimitSkipReadError"
               job-repository="jobRepository">
        <batch:step id="jobSalesPerfAtValidNolimitSkipReadError.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <!-- skip-limit value is dummy. -->
                <batch:chunk reader="detailCSVReader"
                             processor="amountCheckProcessor"
                             writer="detailWriter" commit-interval="10"
                             skip-policy="skipPolicy"> <!-- (4) -->
                </batch:chunk>
            </batch:tasklet>
        </batch:step>
    </batch:job>

表 171. 説明
項番 | 説明
--- | ---
(1) | `ExceptionClassifierSkipPolicy`をBean定義する。
(2) | `policyMap`に、キーを例外クラス、値をスキップ方法にしたマップを設定する。 この例では、`ItemReaderException`が発生したときに(3)で定義したスキップ方法になるように設定している。
(3) | 例外別に実行したいスキップ方法を定義する。 この例では、`AlwaysSkipItemSkipPolicy`を定義している。
(4) | `StepBuilderのskipPolicyメソッド/<batch:chunk>のskip-policy属性`に(1)で定義したBeanを設定する。

ItemProcessorでのスキップ

  ItemProcessor内でtry～catchをして、nullを返却する。
  `skip-policy`によるスキップは、ItemProcessorで再処理が発生するため利用しない。
  詳細は、<skippable-exception-classes>を使わない理由についてを参照。

| ItemProcessorにおける例外ハンドリンクの制約 <skippable-exception-classes>を使わない理由についてにあるように、 ItemProcessorでは、`skipメソッド/<batch:skippable-exception-classes>`を利用したスキップは禁止している。 そのため、コーディングポイント(ItemProcessor編)で説明している 「`ItemProcessListener`インタフェースを使用する方法」を応用したスキップはできない。

スキップの実装例を説明する。

コーディングポイント(ItemProcessor編)の ItemProcessor内でtry～catchする実装例を スキップに対応させる。

ItemProcessor 内でtry～catchする例

    @Component
    public class AmountCheckProcessor implements
            ItemProcessor<SalesPerformanceDetail, SalesPerformanceDetail> {

        // omitted.

        @Override
        public SalesPerformanceDetail process(SalesPerformanceDetail item) throws Exception {
            // (1)
            try {
                checkAmount(item.getAmount(), amountLimit);
            } catch (ArithmeticException ae) {
                logger.warn("Exception occurred while processing. Skipped. [item:{}]",
                        item, ae); // (2)
                return null; // (3)
            }
            return item;
        }
    }

表 172. 説明
項番 | 説明
--- | ---
(1) | `try～catch`で実装する。
(2) | 例外ハンドリングを実装する この例では、引数から取得した例外のスタックトレースをログ出力している。
(3) | nullを返却することでエラーデータをスキップする。

ItemWriterでのスキップ

  ItemWriterにおいてスキップ処理は原則として行わない。
  スキップが必要な場合でも、 `skip-policy`によるスキップは、チャンクサイズが変動するので利用しない。
  詳細は、<skippable-exception-classes>を使わない理由についてを参照。

タスクレットモデル

ビジネスロジック内で例外をハンドリングし、独自にエラーレコードをスキップする処理を実装する。

タスクレットモデルにおける例外ハンドリングの 実装例を スキップ対応させる。

タスクレットモデルでの実装例

    @Component
    public class SalesPerformanceTasklet implements Tasklet {

        private static final Logger logger =
                LoggerFactory.getLogger(SalesPerformanceTasklet.class);

        // omitted.

        @Override
        public RepeatStatus execute(StepContribution contribution,
                ChunkContext chunkContext) throws Exception {

            // (1)
            try {
                reader.open(chunkContext.getStepContext().getStepExecution()
                        .getExecutionContext());

                List<SalesPerformanceDetail> items = new ArrayList<>(10);
                SalesPerformanceDetail item = null;
                do {
                    // Pseudo operation of ItemReader
                    // omitted.

                    // Pseudo operation of ItemProcessor
                    checkAmount(item.getAmount(), amountLimit);

                    // Pseudo operation of ItemWriter
                    // omitted.

                } while (item != null);
            } catch (Exception e) {
                logger.warn("exception in tasklet. Skipped.", e);   // (2)
                continue;    // (3)
            } finally {
                try {
                    reader.close();
                } catch (Exception e) {
                    // do nothing.
                }
            }

            return RepeatStatus.FINISHED;
        }
    }

表 173. 説明
項番 | 説明
--- | ---
(1) | `try-catch`を実装する。
(2) | 例外ハンドリングを実装する この例では、発生した例外のスタックトレースをログ出力している。
(3) | continueにより、エラーデータの処理をスキップする。

###### 6.2.2.3.2. リトライ

例外を検知した場合に、規定回数に達するまで再処理する方法を説明する。

リトライには、状態管理の有無やリトライが発生するシチュエーションなどさまざまな要素を考慮する必要があり、 確実な方法は存在しないうえに、むやみにリトライするとかえって状況を悪化させてしまう。

そのため、本ガイドラインでは、局所的なリトライを実現する`org.springframework.retry.support.RetryTemplate`を利用する方法を説明する。

| スキップと同様に`<retryable-exception-classes>`で対象となる例外クラスを指定する方法もある。 しかし、<skippable-exception-classes>を使わない理由についてと同様に 性能劣化を招く副作用があるため、Macchinetta Batch 2.xでは利用しない。

RetryTemplate実装コード

    public class RetryableAmountCheckProcessor implements
            ItemProcessor<SalesPerformanceDetail, SalesPerformanceDetail> {

        // omitted.

        // (1)
        private RetryPolicy retryPolicy;

        @Override
        public SalesPerformanceDetail process(SalesPerformanceDetail item)
                throws Exception {

            // (2)
            RetryTemplate rt = new RetryTemplate();
            if (retryPolicy != null) {
                rt.setRetryPolicy(retryPolicy);
            }

            try {
                // (3)
                rt.execute(new RetryCallback<SalesPerformanceDetail, Exception>() {
                    @Override
                    public SalesPerformanceDetail doWithRetry(RetryContext context) throws Exception {
                        logger.info("execute with retry. [retry-count:{}]", context.getRetryCount());
                        // retry mocking
                        if (context.getRetryCount() == adjustTimes) {
                            item.setAmount(item.getAmount().divide(new BigDecimal(10)));
                        }
                        checkAmount(item.getAmount(), amountLimit);
                        return null;
                    }
                });
            } catch (ArithmeticException ae) {
                // (4)
                throw new IllegalStateException("check error at processor.", ae);
            }
            return item;
        }

        public void setRetryPolicy(RetryPolicy retryPolicy) {
            this.retryPolicy = retryPolicy;
        }
    }

JavaConfig

    @Bean
    @StepScope
    public RetryableAmountCheckProcessor amountCheckProcessor(
            @Value("#{jobParameters['adjust-times']}") int adjustTimes,
            SimpleRetryPolicy retryPolicy) {
        RetryableAmountCheckProcessor processor = new RetryableAmountCheckProcessor();
        processor.setAmountLimit(BigDecimal.valueOf(10000));
        processor.setAdjustTimes(adjustTimes);
        processor.setRetryPolicy(retryPolicy); // (5)
        return processor;
    }

    // (6)
    @Bean
    public SimpleRetryPolicy retryPolicy(
            Map<Class<? extends Throwable>, Boolean> exceptionMap) {
        return new SimpleRetryPolicy(3, exceptionMap); // (7) (8)
    }

    // (9)
    @Bean
    public Map<Class<? extends Throwable>, Boolean> exceptionMap() {
        Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();
        exceptionMap.put(UnsupportedOperationException.class, true);
        return exceptionMap;
    }

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       @Qualifier("detailCSVReader") ItemReader<SalesPerformanceDetail> reader,
                       @Qualifier("amountCheckProcessor") ItemProcessor<SalesPerformanceDetail, SalesPerformanceDetail> processor,
                       @Qualifier("detailWriter") ItemWriter<SalesPerformanceDetail> writer) {
        return new StepBuilder("jobSalesPerfWithRetryPolicy.step01",
                jobRepository)
                .<SalesPerformanceDetail, SalesPerformanceDetail> chunk(10,
                        transactionManager)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .build();
    }

    @Bean
    public Job jobSalesPerfWithRetryPolicy(JobRepository jobRepository,
                                              Step step01,
                                              JobExecutionLoggingListener listener) {
        return new JobBuilder("jobSalesPerfWithRetryPolicy", jobRepository)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig

    <!-- omitted -->

    <bean id="amountCheckProcessor"
          class="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch06.exceptionhandling.RetryableAmountCheckProcessor"
          scope="step"
          p:retryPolicy-ref="retryPolicy"/> <!-- (5) -->

    <!-- (6) (7) (8)-->
    <bean id="retryPolicy" class="org.springframework.retry.policy.SimpleRetryPolicy"
          c:maxAttempts="3"
          c:retryableExceptions-ref="exceptionMap"/>

    <!-- (9) -->
    <util:map id="exceptionMap">
        <entry key="java.lang.ArithmeticException" value="true"/>
    </util:map>

    <batch:job id="jobSalesPerfWithRetryPolicy" job-repository="jobRepository">
        <batch:step id="jobSalesPerfWithRetryPolicy.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="detailCSVReader"
                             processor="amountCheckProcessor"
                             writer="detailWriter" commit-interval="10"/>
            </batch:tasklet>
        </batch:step>
    </batch:job>

表 174. 説明
項番 | 説明
--- | ---
(1) | リトライ条件を格納する
(2) | RetryTemplateのインスタンスを作成する。 デフォルトは、リトライ回数=3、すべての例外がリトライ対象である。
(3) | `RetryTemplate#execute`メソッドで、リトライを行いたいビジネスロジックを実行するようにする。 ビジネスロジック全体ではなく、リトライしたい部分のみを`RetryTemplate#execute`メソッドで実行するようにする。
(4) | リトライ回数が規定回数を超えた場合の例外ハンドリング。 ビジネスロジックで発生する例外がそのままスローされてくる。
(5) | (6)で定義するリトライ条件を指定する。
(6) | リトライ条件を、`org.springframework.retry.RetryPolicy`を実装したクラスで定義する。 この例では、Spring Batchから提供されている`SimpleRetryPolicy`を利用している。
(7) | コンストラクタ引数の`maxAttempts`にリトライ回数の指定をする。
(8) | コンストラクタ引数の`retryableExceptions`に(9)で定義するリトライ対象の例外を定義したマップを指定する。
(9) | キーにリトライ対象の例外クラス、値に真偽値を設定したマップを定義する。 真偽値が`true`であれば、リトライ対象の例外となる。

###### 6.2.2.3.3. 処理中断

ステップ実行を打ち切りたい場合、スキップ・リトライ対象以外のRuntimeExceptionもしくはそのサブクラスをスローする。

LimitCheckingItemSkipPolicyをもとに、スキップの実装例を示す。

JavaConfig

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       @Qualifier("detailCSVReader") ItemReader<SalesPerformanceDetail> reader,
                       @Qualifier("amountCheckProcessor") ItemProcessor<SalesPerformanceDetail, SalesPerformanceDetail> processor,
                       @Qualifier("detailWriter") ItemWriter<SalesPerformanceDetail> writer,
                       SkipLoggingListener listener) {
        return new StepBuilder("jobSalesPerfAtValidSkipReadError.step01",
                jobRepository)
                .<SalesPerformanceDetail, SalesPerformanceDetail> chunk(10,
                        transactionManager)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .faultTolerant()
                .skipLimit(2)
                .skip(ValidationException.class) // (1)
                .listener(listener)
                .build();
    }

    @Bean
    public Job jobSalesPerfAtValidSkipReadError(JobRepository jobRepository,
                                                Step step01,
                                                JobExecutionLoggingListener listener) {
        return new JobBuilder("jobSalesPerfAtValidSkipReadError", jobRepository)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig

    <batch:job id="jobSalesPerfAtValidSkipReadError" job-repository="jobRepository">
        <batch:step id="jobSalesPerfAtValidSkipReadError.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="detailCSVReader"
                             processor="amountCheckProcessor"
                             writer="detailWriter" commit-interval="10"
                             skip-limit="2">
                    <batch:skippable-exception-classes>
                        <!-- (1) -->
                        <batch:include class="org.springframework.batch.item.validator.ValidationException"/>
                    </batch:skippable-exception-classes>
                </batch:chunk>
            </batch:tasklet>
        </batch:step>
    </batch:job>

表 175. 説明
項番 | 説明
--- | ---
(1) | `ValidationException`以外の例外が発生すれば処理が中断される。

リトライをもとに、リトライの実装例を示す。

JavaConfig

    @Bean
    public SimpleRetryPolicy retryPolicy(
            Map<Class<? extends Throwable>, Boolean> exceptionMap) {
        return new SimpleRetryPolicy(3, exceptionMap);
    }

    @Bean
    public Map<Class<? extends Throwable>, Boolean> exceptionMap() {
        Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();
        exceptionMap.put(UnsupportedOperationException.class, true); // (1)
        return exceptionMap;
    }

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       @Qualifier("detailCSVReader") ItemReader<SalesPerformanceDetail> reader,
                       @Qualifier("amountCheckProcessor") ItemProcessor<SalesPerformanceDetail, SalesPerformanceDetail> processor,
                       @Qualifier("detailWriter") ItemWriter<SalesPerformanceDetail> writer) {
        return new StepBuilder("jobSalesPerfWithRetryPolicy.step01",
                jobRepository)
                .<SalesPerformanceDetail, SalesPerformanceDetail> chunk(10,
                        transactionManager)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .build();
    }

    @Bean
    public Job jobSalesPerfWithRetryPolicy(JobRepository jobRepository,
                                              Step step01,
                                              JobExecutionLoggingListener listener) {
        return new JobBuilder("jobSalesPerfWithRetryPolicy", jobRepository)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig

    <!-- omitted -->

    <bean id="retryPolicy" class="org.springframework.retry.policy.SimpleRetryPolicy"
          c:maxAttempts="3"
          c:retryableExceptions-ref="exceptionMap"/>

    <util:map id="exceptionMap">
        <!-- (1) -->
        <entry key="java.lang.UnsupportedOperationException" value="true"/>
    </util:map>

    <batch:job id="jobSalesPerfWithRetryPolicy" job-repository="jobRepository">
        <batch:step id="jobSalesPerfWithRetryPolicy.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="detailCSVReader"
                             processor="amountCheckProcessor"
                             writer="detailWriter" commit-interval="10"/>
            </batch:tasklet>
        </batch:step>
    </batch:job>

表 176. 説明
項番 | 説明
--- | ---
(1) | `UnsupportedOperationException`以外の例外が発生すれば処理が中断される。

#### 6.2.3. Appendix

##### 6.2.3.1. <skippable-exception-classes>を使わない理由について

Spring Batchでは、ジョブ全体を対象としてスキップする例外を指定し、例外が発生したアイテムへの処理をスキップして処理を継続させる機能を提供している。

その機能は、以下のように`StepBuilderのskipメソッド/<chunk>要素配下に<skippable-exception-classes>要素`を設定し、スキップ対象の例外を指定する形で実装する。

JavaConfig

    @Bean
    public Step retryStep(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader itemReader,
                       ItemProcessor itemProcessor,
                       ItemWriter itemWriter) {
        return new StepBuilder("flowJob.retryStep",
                jobRepository)
                .chunk(20, transactionManager)
                .reader(itemReader)
                .processor(itemProcessor)
                .writer(itemWriter)
                .faultTolerant()
                .skipLimit(10)
                .skip(Exception.class)
                .noSkip(NullPointerException.class)
                .build();
    }

    @Bean
    public Job flowJob(JobRepository jobRepository,
                                                Step retryStep,
                                                JobExecutionLoggingListener listener) {
        return new JobBuilder("flowJob", jobRepository)
                .start(retryStep)
                .listener(listener)
                .build();
    }

XMLConfig

    <job id="flowJob">
        <step id="retryStep">
            <tasklet>
                <chunk reader="itemReader" writer="itemWriter"
                       processor="itemProcessor" commit-interval="20"
                       skip-limit="10">
                    <skippable-exception-classes>
                        <!-- specify exceptions to the skipped -->
                        <include class="java.lang.Exception"/>
                        <exclude class="java.lang.NullPointerException"/>
                    </skippable-exception-classes>
                </chunk>
            </tasklet>
        </step>
    </job>

この機能を利用することによって、入力チェックエラーが発生したレコードをスキップして後続データの処理を継続することは可能だが、 Macchinetta Batch 2.xでは以下の理由により使用しない。

- `skipメソッド/<skippable-exception-classes>要素`を利用して例外をスキップした場合、 1つのチャンクに含まれるデータ件数が変動するため、性能劣化を引き起こす可能性がある。

  - これは、例外の発生箇所(`ItemReader`/`ItemProcessor`/`ItemWriter`)によって変わる。詳細は後述する。

| skip/<skippable-exception-classes>を定義せずにSkipPolicyを利用することは必ず避ける 暗黙的にすべての例外が登録された状況になり、性能劣化の可能性が飛躍的に高まる。

例外発生箇所(`ItemReader`/`ItemProcessor`/`ItemWriter`)ごとの挙動についてそれぞれ説明する。
なお、トランザクションの動作は例外の発生箇所によらず、例外が発生した場合は必ずロールバックした後、再度処理される。

ItemReaderで例外が発生した場合

  - `ItemReader`の処理内で例外が発生した場合は、次のitemへ処理対象が移る。

  - これによる副作用はない

ItemProcessorで例外が発生した場合

  - `ItemProcessor`の処理内で例外が発生した場合は、チャンクの最初に戻り1件目から再処理する。

  - 再処理の対象にスキップされるitemは含まれない。

  - 1度目の処理と再処理時のチャンクサイズは変わらない。

ItemWriterで例外が発生した場合

  - `ItemWriter`の処理内で例外が発生した場合は、チャンクの最初に戻り1件目から再処理する。

  - 再処理は`ChunkSize=1`に固定し、1件ずつ実行される。

  - 再処理対象にスキップされるitemも含まれる。

`ItemProcessor`にて例外が発生した場合、`ChunkSize=1000`の場合を例に考えると、 1000件目で例外が発生すると1件目から再処理が行われ、合計で1999件分の処理が実行されてしまう。

`ItemWriter`にて例外が発生した場合、`ChunkSize=1`に固定し再処理される。 仮に`ChunkSize=1000`の場合を例に考えると、 本来1回のトランザクションにも関わらず1000回のトランザクションに分割し処理されてしまう。

これらはジョブ全体の処理時間が長期化することを意味し、異常時に状況を悪化させる可能性が高い。 また、二重処理すること自体が問題化する可能性を秘めており、設計製造に追加の考慮事項を生む。

よって、`skipメソッド/<skippable-exception-classes>`を使用することは推奨しない。 `ItemReader`でエラーになったデータをスキップすることはこれらの問題を引き起こさないが、 事故を未然に防ぐためには基本的に避けるようにし、どうしても必要な場合に限定的に適用すること。

### 6.3. 処理の再実行

#### 6.3.1. Overview

障害発生などに起因してジョブが異常終了した後に、ジョブを再実行することで回復する手段について説明する。

本機能は、チャンクモデルとタスクレットモデルとで使い方が異なるため、それぞれについて説明する。

ジョブの再実行には、以下の方法がある。

1. ジョブのリラン

2. ジョブのリスタート

   - ステートレスリスタート

     - 件数ベースリスタート

   - ステートフルリスタート

     - 処理状態を判断し、未処理のデータを抽出して処理するリスタート

       - 処理状態を識別するための処理を別途実装する必要がある

以下に用語を定義する。

リラン

  ジョブを最初からやり直すこと。
  事前作業として、データ初期化など障害発生前のジョブ開始時点に状態を回復する必要がある。

リスタート

  ジョブが中断した箇所から処理を再開すること。
  処理再開位置の保持・取得方法、再開位置までのデータスキップ方法などをあらかじめ設計/実装する必要がある。
  リスタートには、ステートレスとステートフルの2種類がある。

ステートレスリスタート

  個々の入力データに対する状態(未処理/処理済)を考慮しないリスタート方法。

件数ベースリスタート

  ステートレスリスタートの1つ。
  処理した入力データ件数を保持し、リスタート時にその件数分入力データをスキップする方法。
  出力が非トランザクショナルなリソースの場合は、出力位置を保持し、リスタート時にその位置まで書き込み位置を移動することも必要になる。

ステートフルリスタート

  個々の入力データに対する状態(未処理/処理済)を判断し、未処理のデータのみを取得条件とするリスタート方法。
  出力が非トランザクショナルなリソースの場合は、リソースを追記可能にして、リスタート時には前回の結果へ追記していくようにする。

一般的に、再実行の方法はリランがもっとも簡単である。 リラン < ステートレスリスタート < ステートフルリスタートの順に、設計や実装が難しくなる。 無論、可能であれば常にリランとすることが好ましいが、 ユーザが実装するジョブ1つ1つに対して、許容するバッチウィンドウや処理特性に応じてどの方法を適用するか検討してほしい。

#### 6.3.2. How to use

リランとリスタートの実現方法について説明する。

##### 6.3.2.1. ジョブのリラン

ジョブのリランを実現する方法を説明する。

1. リラン前にデータの初期化などデータ回復の事前作業を実施する。

2. 失敗したジョブを同じ条件(同じパラメータ)で再度実行する。

   - Spring Batchでは同じパラメータでジョブを実行すると二重実行と扱われるが、Macchinetta Batch 2.xでは別ジョブとして扱う。
     詳細は、"パラメータ変換クラスについて"を参照。

##### 6.3.2.2. ジョブのリスタート

ジョブのリスタート方法を説明する。

ジョブのリスタートを行う場合は、同期実行したジョブに対して行うことを基本とする。

非同期実行したジョブは、リスタートではなくリランで対応するジョブ設計にすることを推奨する。 これは、「意図したリスタート実行」なのか「意図しない重複実行」であるかの判断が難しく、 運用で混乱をきたす可能性があるからである。

非同期実行ジョブでリスタート要件がどうしても外せない場合は、 「意図したリスタート実行」を明確にするために、以下の方法を利用できる。

- `CommandLineJobRunner`の`-restart`によるリスタート

  - 非同期実行したジョブを別途同期実行によりリスタートする。逐次で回復処理を進めていく際に有効となる。

- `JobOperator#restart(JobExecutionId)`によるリスタート

  - 非同期実行したジョブを、再度非同期実行の仕組み上でリスタートする。一括で回復処理を進めていく際に有効となる。

    - 非同期実行(DBポーリング)はリスタートをサポートしていない。そのため、別途ユーザにて実装する必要がある。

    - 非同期実行(Webコンテナ)はリスタートの実現方法をガイドしている。この記述にしたがって、ユーザにて実装すること。

| 入力チェックがある場合のリスタートについて 入力チェックエラーは、チェックエラーの原因となる入力リソースを修正しない限り回復不可能である。 参考までに、入力チェックエラーが発生した際の入力リソース修正例を以下に示す。 入力チェックエラーが発生した場合は、対象データが特定できるようにログ出力を行う。 出力されたログ情報にもとづいて、入力データの修正を行う。 入力データの順番が変わらないようにする。 修正方法は入力リソースの生成方法により対応が異なる。 手動で修正 ジョブなどで再作成 連携元からの再送 修正した入力データを配備して、リスタートを実行する。

| 多重処理(Partition Step)の場合について "多重処理(Partition Step)"でリスタートする場合、 再び分割処理から処理が実施される。 データを分割した結果、すべて処理済みであった場合、無駄な分割処理が行われ`JobRepository`上には記録されるが、 これによるデータ不整合などの問題は発生しない。

##### 6.3.2.3. ステートレスリスタート

ステートレスリスタートを実現する方法を説明する。

Macchinetta Batch 2.xでのステートレスリスタートは、件数ベースのリスタートを指す。これは、Spring Batchの仕組みをそのまま利用することで実現する。
件数ベースのリスタートは、チャンクモデルのジョブ実行で使用できる。 また、件数ベースのリスタートは、`JobRepository`に登録される入出力に関するコンテキスト情報を利用する。 よって、件数ベースのリスタートでは、`JobRepository`はインメモリデータベースではなく、永続性が担保されているデータベースを使用することを前提とする。

| JobRepositoryの障害発生時について データソースの設定で説明したとおり、 `JobRepository`への更新は業務処理とは独立したトランザクションで行われるため、 `JobRepository`に障害が発生した場合は実際の処理件数とずれる可能性がある。 これは、リスタート時に二重処理の危険性があることを意味する。 よって、`JobRepository`の可用性を検討したり、次点の方法としてリランの方法を検討しておいたりといった、 障害時の対処方法を検討する必要がある。

リスタート時の入力

  Spring Batchが提供しているItemReaderのほとんどが件数ベースのリスタートに対応しているため、特別な対応は不要である。
  件数ベースのリスタート可能なItemReaderを自作する場合は、リスタート処理が実装されている抽象クラス `org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader` を拡張すればよい。件数ベースリスタートは、あくまで件数のみを基準としてリスタート開始点を決定するため、処理済みの入力データの変更/追加/削除を検出することができない。
  新たに入力データを追加する際に、以下のようなケースに留意すること。

  - データの取得順を変更する

    - リスタート時に、重複処理や未処理となるデータが発生してしまい、リランした結果と異なる回復結果になるため、決して行ってはいけない。

  - 処理済みデータを更新する

    - リスタート時に更新したデータは読み飛ばされるので、リランした結果とリスタートした結果で回復結果が変わるため好ましくない場合がある。

  - 未処理データを更新または追加する

    - リランした結果と同じ回復結果になるため許容する。ただし、初回実行で正常終了した結果とは異なる。これは異常なデータを緊急対処的にパッチする場合や、実行時点で受領したデータを可能な限り多く処理する際に限定して使うとよい。

リスタート時の出力

  非トランザクショナルなリソースへの出力には注意が必要である。たとえば、ファイルではどの位置まで出力していたかを把握し、その位置から出力を行わなければいけない。
  Spring Batchが提供している`FlatFileItemWriter`は、コンテキストから前回の出力位置を取得して、リスタート時にはその位置から出力を行ため、特別な対応は不要である。
  トランザクショナルなリソースについては、失敗時にロールバックが行われているため、リスタート時には特に対処することなく処理を行うことができる。

上記の条件を満たしていれば、失敗したジョブに`-restart`のオプションを付加して再度実行すればよい。 以下にジョブのリスタート例を示す。

同期実行したジョブのリスタート例

    $ # (1)
    $ java -cp dependency/* org.springframework.batch.core.launch.support.CommandLineJobRunner <jobPath> <jobName> -restart

表 177. 説明
項番 | 説明
(1) | `CommandLineJobRunner`へ失敗したジョブと同じジョブBeanのパスとジョブ名を指定し、`-restart`を付加して実行する。 ジョブパラメータは、`JobRepository`から復元されるため指定は不要。

| 実運用時の考慮 リスタート時に限らず、本番運用においては上記のように直にコマンドを打つような運用は避けること。 これは、誤ったコマンドの実行を防ぐためである。 誤ったコマンドの実行は、監視に不必要な情報をログに残したり、監視ツールが不要なアラートを発する原因になる可能性がある。 例えば、実行するべきコマンドをスクリプト化し、これを実行する運用とするなど、出来る限り直にコマンドを入力しない運用を検討してほしい。 なお、ジョブのリスタートを防止する場合には、ステートフルリスタートで後述するように、 ジョブのBean定義で、`restartable`属性を`false`にすることも考えられる。 これにより、誤って`-restart`オプションをつけて起動した場合に、エラーにすることができる。

非同期実行(DBポーリング)で実行したジョブのリスタート例を以下に示す。

非同期実行(DBポーリング)で実行したジョブのリスタート例

    $ # (1)
    $ java -cp dependency/* org.springframework.batch.core.launch.support.CommandLineJobRunner <jobPath> <jobExecutionId> -restart

表 178. 説明
項番 | 説明
(1) | `CommandLineJobRunner`へ失敗したジョブと同じジョブ実行ID(JobExecutionId)を指定し、`-restart`を付加して実行する。 ジョブパラメータは、`JobRepository`から復元されるため指定は不要。 ジョブ実行IDは、ジョブ要求テーブルから取得することができる。 ジョブ要求テーブルについては、"ポーリングするテーブルについて"を参照。

| ジョブ実行IDのログ出力 異常終了したジョブのジョブ実行IDを迅速に特定するため、 ジョブ終了時や例外発生時にジョブ実行IDをログ出力するリスナーや例外ハンドリングクラスを実装することを推奨する。

非同期実行(Webコンテナ)でのリスタート例を以下に示す。

非同期実行(Webコンテナ)で実行したジョブのリスタート例

    public long restart(long JobExecutionId) throws Execption {
      return jobOperator.restart(JobExecutionId); // (1)
    }

表 179. 説明
項番 | 説明
(1) | `JobOperator`へ失敗したジョブと同じジョブ実行ID(JobExecutionId)を指定し、`restart`メソッドで実行する。 ジョブパラメータは、`JobRepository`から復元される。 ジョブ実行IDは、WebAPでジョブ実行した際に取得したIDを利用するか、`JobRepository`から取得することができる。 取得方法は、"ジョブの状態管理"を参照。

##### 6.3.2.4. ステートフルリスタート

ステートフルリスタートを実現する方法を説明する。

ステートフルリスタートとは、実行時に入出力結果を付きあわせて未処理データだけ取得することで再処理する方法である。 この方法は、状態保持･未処理判定など設計が難しいが、データの変更に強い特徴があるため、時々用いられることがある。

ステートフルリスタートでは、リスタート条件を入出力リソースから判定するため、`JobRepository`の永続化は不要となる。

リスタート時の入力

  入出力結果を付きあわせて未処理データだけ取得するロジックを実装したItemReaderを用意する。

リスタート時の出力

  ステートレスリスタートと同様に非トランザクショナルなリソースへ出力には注意が必要になる。
  ファイルの場合、コンテキストを使用しないことを前提にすると、ファイルの追記を許可するような設計が必要になる。

ステートフルリスタートは、ジョブのリランと同様に失敗時のジョブと同じ条件でジョブを再実行する。
ステートレスリスタートとは異なり、`-restart`のオプションは使用しない。

簡単ステートフルなリスタートの実現例を下記に示す。

処理仕様

1. 入力対象のテーブルに処理済カラムを定義し、処理が成功したらNULL以外の値で更新する。

   - 未処理データの抽出条件は、処理済カラムの値がNULLとなる。

2. 処理結果をファイルに出力する。

RestartOnConditionRepository.xml

    <!-- (1) -->
    <select id="findByProcessedIsNull"
            resultType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.plan.SalesPlanDetail">
        <![CDATA[
        SELECT
            branch_id AS branchId, year, month, customer_id AS customerId, amount
        FROM
            sales_plan_detail
        WHERE
            processed IS NULL
        ORDER BY
            branch_id ASC, year ASC, month ASC, customer_id ASC
        ]]>
    </select>

    <!-- (2) -->
    <update id="update" parameterType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.plan.SalesPlanDetail">
        <![CDATA[
        UPDATE
            sales_plan_detail
        SET
            processed = '1'
        WHERE
            branch_id = #{branchId}
        AND
            year = #{year}
        AND
            month = #{month}
        AND
            customer_id = #{customerId}
        ]]>
    </update>

JavaConfig

    // (3)
    @Bean
    public MyBatisCursorItemReader<SalesPlanDetail> reader(
            @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory) {
        return new MyBatisCursorItemReaderBuilder<SalesPlanDetail>()
                .sqlSessionFactory(jobSqlSessionFactory)
                .queryId(
                        "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch06.reprocessing.repository.RestartOnConditionRepository.findByZeroOrLessAmount")
                .build();
    }

    // (4)
    @Bean
    public MyBatisBatchItemWriter<SalesPlanDetail> dbWriter(
            @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
            SqlSessionTemplate batchModeSqlSessionTemplate) {
        return new MyBatisBatchItemWriterBuilder<SalesPlanDetail>()
                .sqlSessionFactory(jobSqlSessionFactory)
                .statementId(
                        "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch06.reprocessing.repository.RestartOnConditionRepository.update")
                .sqlSessionTemplate(batchModeSqlSessionTemplate)
                .build();
    }

    @Bean
    @StepScope
    public FlatFileItemWriter<SalesPlanDetail> fileWriter(
            @Value("#{jobParameters['outputFile']}") File outputFile) {
        DelimitedLineAggregator<SalesPlanDetail> lineAggregator = new DelimitedLineAggregator<>();
        BeanWrapperFieldExtractor<SalesPlanDetail> fieldExtractor = new BeanWrapperFieldExtractor<>();
        fieldExtractor.setNames(new String[] {"branchId", "year", "month", "customerId", "amount"});
        lineAggregator.setFieldExtractor(fieldExtractor);
        return new FlatFileItemWriterBuilder<SalesPlanDetail>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .resource(new FileSystemResource(outputFile))
                .lineAggregator(lineAggregator)
                .append(true) // (5)
                .build();
    }

    // (6)
    @Bean(destroyMethod="")
    public CompositeItemWriter<SalesPlanDetail> compositeWriter(
            @Qualifier("fileWriter") FlatFileItemWriter<SalesPlanDetail> fileWriter,
            @Qualifier("dbWriter") MyBatisBatchItemWriter<SalesPlanDetail> dbWriter) throws Exception {

        List<ItemWriter<? super SalesPlanDetail>> list = new ArrayList<>();
        list.add(fileWriter);
        list.add(dbWriter);

        return new CompositeItemWriterBuilder<SalesPlanDetail>()
                .delegates(list)
                .build();
    }

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<SalesPlanDetail> reader,
                       @Qualifier("amountUpdateItemProcessor") ItemProcessor<SalesPlanDetail, SalesPlanDetail> processor,
                       @Qualifier("compositeWriter") ItemWriter<SalesPlanDetail> compositeWriter,
                       LoggingItemReaderListener listener) {
        return new StepBuilder("restartOnConditionBasisJob.step01",
                jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(10, transactionManager)
                .reader(reader)
                .processor(processor)
                .writer(compositeWriter)
                .listener(listener)
                .build();
    }

    @Bean
    public Job restartOnConditionBasisJob(JobRepository jobRepository,
                                            Step step01,
                                            JobExecutionLoggingListener listener) {
        return new JobBuilder("restartOnConditionBasisJob",
                jobRepository)
                .preventRestart() // (7)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig

    <!-- (3) -->
    <bean id="reader" class="org.mybatis.spring.batch.MyBatisCursorItemReader"
          p:queryId="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch06.reprocessing.repository.RestartOnConditionRepository.findByZeroOrLessAmount"
          p:sqlSessionFactory-ref="jobSqlSessionFactory"/>

    <!-- (4) -->
    <bean id="dbWriter" class="org.mybatis.spring.batch.MyBatisBatchItemWriter"
          p:statementId="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch06.reprocessing.repository.RestartOnConditionRepository.update"
          p:sqlSessionTemplate-ref="batchModeSqlSessionTemplate"/>

    <bean id="fileWriter"
          class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"
          p:appendAllowed="true"> <!-- (5) -->
        <property name="lineAggregator">
            <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
                <property name="fieldExtractor">
                    <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                          p:names="branchId,year,month,customerId,amount"/>
                </property>
            </bean>
        </property>
    </bean>
    <!-- (6) -->
    <bean id="compositeWriter" class="org.springframework.batch.item.support.CompositeItemWriter">
        <property name="delegates">
            <list>
                <ref bean="fileWriter"/>
                <ref bean="dbWriter"/>
            </list>
        </property>
    </bean>

    <batch:job id="restartOnConditionBasisJob"
               job-repository="jobRepository" restartable="false"> <!-- (7) -->

        <batch:step id="restartOnConditionBasisJob.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="reader" processor="amountUpdateItemProcessor"
                             writer="compositeWriter" commit-interval="10" />
            </batch:tasklet>
        </batch:step>

    </batch:job>

リスタートのコマンド実行例

    $ # (8)
    $ java -cp dependency/* org.springframework.batch.core.launch.support.CommandLineJobRunner <jobPath> <jobName> <jobParameters> ...

表 180. 説明
項番 | 説明
--- | ---
(1) | 処理済カラムがNULLのデータのみ抽出するようにSQLを定義する。
(2) | 処理済カラムをNULL以外で更新するSQLを定義する。
(3) | ItemReaderには、(1)で定義したSQLIDを設定する。
(4) | データベースへ更新は、(2)で定義したSQLIDを設定する。
(5) | リスタート時に前回中断箇所から書き込み可能にするため、ファイルの追記を許可する。
(6) | ファイル出力 → データベース更新の順序で処理されるように`CompositeItemWriter`を定し、chunkのwriterに設定する。
(7) | 必須ではないが、誤って`-restart`オプションをつけて起動された場合にエラーになるように`JobBuilderのpreventRestartメソッドの追加/restartable属性をfalseに設定`しておく。
(8) | 失敗したジョブの実行条件で再度実行を行う。

| ジョブのrestartable属性について `restartable`がtrueの場合、ステートレスリスタートで説明したとおり、コンテキスト情報を使い入出力データの読み飛ばしを行う。 ステートフルリスタートでSpring Batch提供のItemReaderやItemWriterを使用している場合、この動作により期待した処理が行われなくなる可能性がある。 そのため、`restartable`をfalseにすることで、`-restart`オプションによる起動はエラーとなり、誤動作を防止することができる。

## 7. ジョブの管理

### 7.1. Overview

ジョブの実行を管理する方法について説明する。

本機能は、チャンクモデルとタスクレットモデルとで同じ使い方になる。

#### 7.1.1. ジョブの実行管理とは

ジョブの起動状態や実行結果を記録しバッチシステムを維持することを指す。 特に、異常発生時の検知や次に行うべき行動(異常終了後のリラン・リスタート等)を判断するために、必要な情報を確保することが重要である。
バッチアプリケーションの特性上、起動直後にその結果をユーザインタフェースで確認できることは稀である。 よって、ジョブスケジューラ/RDBMS/アプリケーションログといった、ジョブの実行とは別に実行状態・結果の記録を行う仕組みが必要となる。

本ガイドラインではジョブスケジューラを使用したジョブ管理については説明しないが、ジョブスケジューラを使用するのに適したジョブについては以下に記す。

ジョブスケジューラを使用するのに適したジョブ

  Macchinetta Batch 2.xの同期型ジョブで`JobRepository`による状態管理を必要としない(非永続化のRDBMSを使用)
  `JobRepository`を前提としたリスタート機能を使わないジョブ

上記の条件を満たす場合、ジョブの停止・リスタートを含めた、ジョブの実行管理をすべてジョブスケジューラで行うことが適している。

ジョブスケジューラを使用したジョブの実行管理については各製品のマニュアルを参照。

以降、Spring Batchが提供する機能を利用したジョブの実行管理について説明をする。

##### 7.1.1.1. Spring Batch が提供する機能

Spring Batchは、ジョブの実行管理向けに以下のインタフェースを提供している。

表 181. ジョブの管理機能一覧
機能 | 対応するインタフェース
--- | ---
ジョブの実行状態・結果の記録 | `org.springframework.batch.core.repository.JobRepository`
ジョブの終了コードとプロセス終了コードの変換 | `org.springframework.batch.core.launch.support.ExitCodeMapper`

Spring Batch はジョブの起動状態・実行結果の記録に`JobRepository`を使用する。 Macchinetta Batch 2.xでは、以下のすべてに該当する場合は永続化は任意としてよい。

- 同期型ジョブ実行のみでMacchinetta Batch 2.xを使用する。

- ジョブの停止・リスタートを含め、ジョブの実行管理はすべてジョブスケジューラに委ねる。

  - Spring Batchがもつ`JobRepository`を前提としたリスタートを利用しない。

これらに該当する場合は`JobRepository`が使用するRDBMSの選択肢として、インメモリ・組み込み型データベースである`H2`を利用する。
一方で非同期実行を利用する場合や、Spring Batchの停止・リスタートを活用する場合は、ジョブの実行状態・結果を永続化可能なRDBMSが必要となる。

| デフォルトのトランザクション分離レベル Spring Batchが提供するxsdでは、`JobRepository`のトランザクション分離レベルは`SERIALIZABLE`をデフォルト値としている。 しかし、この場合、同期/非同期にかかわらず複数のジョブを同時に実行した際に`JobRepository`の更新で例外が発生してしまう。 そのため、ブランクプロジェクトでは、あらかじめ`JobRepository`のトランザクション分離レベルを`READ_COMMITTED`に設定している。

| IndexによるJobRepositoryの性能改善 Indexを作成することで、`JobRepository`の性能改善が期待できる。 どのSQL文のどの列に対してIndexを作成するかは、実行計画を確認するなどして適切に判断してほしい。 Spring Batchのリファレンス B.10 Recommendations for Indexing Meta Data Tables では、Spring Batchが提供するDaoの実装によって、WHERE句でどの列が利用されているか、およびそれらの使用頻度を示しているので参考にするとよい。 `JobRepository`の永続化を行う場合は、Indexを作成することを検討されたい。

ジョブスケジューラを使用したジョブの実行管理については各製品のマニュアルを参照。

本ガイドラインではMacchinetta Batch 2.x内部で`JobRepository`を用いたジョブの状態を管理するうえで関連する、 以下の項目について説明する。

Macchinetta Batch内部での状態管理に関する項目

- ジョブの状態管理

  - 状態を永続化する方法

  - 状態を確認する方法

  - ジョブを手動停止する方法

- 終了コードのカスタマイズ

- 二重起動防止

- ロギング

- メッセージ管理

### 7.2. How to use

`JobRepository`はSpring BatchによりRDBMSへ自動的に新規登録・更新を行う。
ジョブの状態・実行結果の確認を行う場合は、意図しない変更処理がバッチアプリケーションの内外から行われることのないよう、以下のいずれかの方法を選択する。

- ジョブの状態管理に関するテーブルに対しクエリを発行する

- `org.springframework.batch.core.explore.JobExplorer`を使用する

#### 7.2.1. ジョブの状態管理

`JobRepository`を用いたジョブの状態管理方法を説明する。
Spring Batchにより、以下のEntityがRDBMSのテーブルに登録される。

表 182. JobRepositoryで管理されるEntityクラスとテーブル名
項番 | Entityクラス | テーブル名 | 生成単位 | 説明
--- | --- | --- | --- | ---
(1) | `JobExecution` | `BATCH_JOB_EXECUTION` | 1回のジョブ実行 | ジョブの状態・実行結果を保持する。
(2) | `JobExecutionContext` | `BATCH_JOB_EXECUTION_CONTEXT` | 1回のジョブ実行 | ジョブ内部のコンテキストを保持する。
(3) | `JobExecutionParams` | `BATCH_JOB_EXECUTION_PARAMS` | 1回のジョブ実行 | 起動時に与えられたジョブパラメータを保持する。
(4) | `StepExecution` | `BATCH_STEP_EXECUTION` | 1回のステップ実行 | ステップの状態・実行結果、コミット・ロールバック件数を保持する。
(5) | `StepExecutionContext` | `BATCH_STEP_EXECUTION_CONTEXT` | 1回のステップ実行 | ステップ内部のコンテキストを保持する。
(6) | `JobInstance` | `BATCH_JOB_INSTANCE` | ジョブ名とジョブパラメータの組み合わせ | ジョブ名、およびジョブパラメータをシリアライズした文字列を保持する。

たとえば、1回のジョブ起動で3つのステップを実行した場合、以下の差が生じる

- `JobExecution`、`JobExecutionContext`、`JobExecutionParams`は1レコード登録される

- `StepExecution`、`StepExecutionContext`は3レコード登録される

また、`JobInstance`は過去に起動した同名ジョブ・同一パラメータよる二重実行を抑止するために使用されるが、 Macchinetta Batch 2.xではこのチェックを行わない。詳細は二重起動防止を参照。

| `JobRepository`による各テーブルの構成は、 Spring Batchのアーキテクチャにて説明している。

| チャンク方式におけるStepExecutionの件数項目について 以下のように、不整合が発生しているように見えるが、仕様上妥当なケースがある。 `StepExecution(BATCH_STEP_EXECUTIONテーブル)`のトランザクション発行回数が入力データ件数と一致しない場合がある。 トランザクション発行回数は`BATCH_STEP_EXECUTION`の`COMMIT_COUNT`と`ROLLBACK_COUNT`の総和を指す。 ただし、入力データ件数がチャンクサイズで割り切れる場合`COMMIT_COUNT`が+1となる。 これは入力データ件数分を読み込んだ後、終端を表すnullも入力データとカウントされて空処理されるためである。 `BATCH_STEP_EXECUTION`と`BATCH_STEP_EXECUTION_CONTEXT`の処理件数が異なることがある。 `BATCH_STEP_EXECUTION`テーブルの`READ_COUNT`、`WRITE_COUNT`はそれぞれ`ItemReader`、`ItemWriter`による読み込み・書き込みを行った件数が記録される。 `BATCH_STEP_EXECUTION_CONTEXT`テーブルの`SHORT_CONTEXT`カラムはBase64でエンコードされたMap形式で `ItemReader`による読み込み処理件数が記録される。しかし、必ずしも`BATCH_STEP_EXECUTION`による処理件数と一致しない。 これはチャンク方式による`BATCH_STEP_EXECUTION`テーブルが成功・失敗を問わず読み込み・書き込み件数を記録するのに対し、 `BATCH_STEP_EXECUTION_CONTEXT`テーブルは処理途中で失敗した場合のリスタートで再開される位置として記録するためである。

##### 7.2.1.1. 状態の永続化

外部RDBMSを使用することで`JobRepository`によるジョブの実行管理情報を永続化させることができる。 batch-application.propertiesの以下項目を外部RDBMS向けのデータソース、スキーマ設定となるよう修正する。

batch-application.properties

    # (1)
    # Admin DataSource settings.
    admin.jdbc.driver=org.postgresql.Driver
    admin.jdbc.url=jdbc:postgresql://serverhost:5432/admin
    admin.jdbc.username=postgres
    admin.jdbc.password=postgres

    # (2)
    spring-batch.schema.script=classpath:org/springframework/batch/core/schema-postgresql.sql

表 183. 設定内容の項目一覧(PostgreSQL)
項番 | 説明
--- | ---
(1) | 接頭辞`admin`が付与されているプロパティの値として、接続する外部RDBMSの設定をそれぞれ記述する。
(2) | アプリケーション起動時に`JobRepository`としてスキーマの自動生成を行うスクリプトファイルを指定する。

| 管理用/業務用データソースの補足 データベースへの接続設定は、管理用と業務用データソースとして別々に定義する。 ブランクプロジェクトでは別々に定義した上で、 `JobRepository`は、プロパティ接頭辞に`admin`が付与された管理用データソースを使用するよう設定済みである。 非同期実行(DBポーリング)を使用する場合は、ジョブ要求テーブルも同じ管理用データソース、スキーマ生成スクリプトを指定すること。 詳細は非同期実行(DBポーリング)を参照。

##### 7.2.1.2. ジョブの状態・実行結果の確認

`JobRepository`からジョブの実行状態を確認する方法について説明する。
いずれの方法も、あらかじめ確認対象のジョブ実行IDが既知であること。

###### 7.2.1.2.1. クエリを直接発行する

RDBMSコンソールを用い、`JobRepository`が永続化されたテーブルに対して直接クエリを発行する。

SQLサンプル

    admin=# select JOB_EXECUTION_ID, START_TIME, END_TIME, STATUS, EXIT_CODE from BATCH_JOB_EXECUTION where JOB_EXECUTION_ID = 1;
     job_execution_id |       start_time        |        end_time         |  status   | exit_code
    ------------------+-------------------------+-------------------------+-----------+-----------
                    1 | 2017-02-14 17:57:38.486 | 2017-02-14 18:19:45.421 | COMPLETED | COMPLETED
    (1 row)
    admin=# select JOB_EXECUTION_ID, STEP_EXECUTION_ID, START_TIME, END_TIME, STATUS, EXIT_CODE from BATCH_STEP_EXECUTION where JOB_EXECUTION_ID = 1;
     job_execution_id | step_execution_id |       start_time        |        end_time        |  status   | exit_code
    ------------------+-------------------+-------------------------+------------------------+-----------+-----------
                    1 |                 1 | 2017-02-14 17:57:38.524 | 2017-02-14 18:19:45.41 | COMPLETED | COMPLETED
    (1 row)

###### 7.2.1.2.2. `JobExplorer`を利用する

バッチアプリケーションと同じアプリケーションコンテキストを共有可能な環境下で、`JobExplorer`をインジェクションすることでジョブの実行状態を確認する。

APIコールサンプル

    // omitted.

    @Inject
    private JobExplorer jobExplorer;

    private void monitor(long jobExecutionId) {

        // (1)
        JobExecution jobExecution = jobExplorer.getJobExecution(jobExecutionId);

        // (2)
        String jobName = jobExecution.getJobInstance().getJobName();
        LocalDateTime jobStartTime = jobExecution.getStartTime();
        LocalDateTime jobEndTime = jobExecution.getEndTime();
        BatchStatus jobBatchStatus = jobExecution.getStatus();
        String jobExitCode = jobExecution.getExitStatus().getExitCode();

        // omitted.

        // (3)
        for (StepExecution stepExecution : jobExecution.getStepExecutions()) {
            String stepName = stepExecution.getStepName();
            LocalDateTime stepStartTime = stepExecution.getStartTime();
            LocalDateTime stepEndTime = stepExecution.getEndTime();
            BatchStatus stepStatus = stepExecution.getStatus();
            String stepExitCode = stepExecution.getExitStatus().getExitCode();

            // omitted.
        }
    }

表 184. 設定内容の項目一覧(PostgreSQL)
項番 | 説明
--- | ---
(1) | インジェクションされた`JobExplorer`からジョブ実行IDを指定し`JobExecution`を取得する。
(2) | `JobExecution`によるジョブの実行結果を取得する。
(3) | `JobExecution`から、ジョブ内で実行されたステップのコレクションを取得し、個々の実行結果を取得する。

##### 7.2.1.3. ジョブの停止

ジョブの停止とは`JobRepository`の実行中ステータスを停止中ステータスに更新し、ステップの境界や チャンク方式によるチャンクコミット時にジョブを停止させる機能である。
リスタートと組み合わせることで、停止された位置からの処理を再開させることができる。

| リスタートの詳細は"ジョブのリスタート"を参照。

| 「ジョブの停止」は仕掛かり中のジョブを直ちに中止する機能ではなく、`JobRepository`の実行中ステータスを停止中に更新する機能である。 ジョブに対して即座に仕掛かり中スレッドに対して割り込みするといったような、何らかの停止処理を行うわけではない。 このため、ジョブの停止は「チャンクの切れ目など、節目となる処理が完了した際に停止するよう予約する」ことともいえる。 たとえば以下の状況下でジョブ停止を行っても、期待する動作とはならない。 単一ステップで`Tasklet`により構成されたジョブ実行。 チャンク方式で、データ入力件数 < `commit-interval`のとき。 処理内で無限ループが発生している場合。

以下、ジョブの停止方法を説明する。

- コマンドラインからの停止

  - 同期型ジョブ・非同期型ジョブのどちらでも利用できる

  - `CommandLineJobRunner`の`-stop`を利用する

JavaConfig

    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        com.example.batch.jobs.Job01Config job01 -stop

XMLConfig

    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        classpath:/META-INF/jobs/job01.xml job01 -stop

- ジョブ名指定によるジョブ停止は同名のジョブが並列で起動することが少ない同期バッチ実行時に適している。

JavaConfig

    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        com.example.batch.jobs.Job01Config 3 -stop

XMLConfig

    $ java org.springframework.batch.core.launch.support.CommandLineJobRunner \
        classpath:/META-INF/jobs/job01.xml 3 -stop

- ジョブ実行ID指定によるジョブ停止は同名のジョブが並列で起動することの多い非同期バッチ実行時に適している。

| JobExecutionIdの確認方法はジョブの状態・実行結果の確認を参照。 ジョブ実行IDをもとにジョブ停止を行う場合は`JobOpertion#stop()`を利用してもよい。 `JobOperation#stop()`を用いたジョブの停止は "非同期実行ジョブの停止とリスタート"を参照。

#### 7.2.2. 終了コードのカスタマイズ

同期実行によりジョブ終了時、javaプロセスの終了コードをジョブやステップの終了コードに合わせてカスタマイズできる。 javaプロセスの終了コードをカスタマイズするのに必要な作業を以下に示す。

1. ステップの終了コードを変更する。

2. ステップの終了コードに合わせて、ジョブの終了コードを変更する。

3. ジョブの終了コードとjavaプロセスの終了コードをマッピングする。

| 終了コードの意味合いについて 本節では、終了コードは2つの意味合いで扱われており、それぞれの説明を以下に示す。 COMPLETED、FAILEDなどの文字列の終了コードは、ジョブやステップの終了コードである。 0、255などの数値の終了コードは、Javaプロセスの終了コードである。

##### 7.2.2.1. ステップの終了コードの変更

処理モデルごとにステップの終了コードを変更する方法を以下に示す。

チャンクモデルにおけるステップの終了コードの変更

  ステップ終了時の処理として、`StepExecutionListener`の`afterStep`メソッドもしくは`@AfterStep`アノテーションを付与したメソッドを実装し、 任意のステップの終了コードを返却する。

リスナーを含むItemProcessor実装例

    @Component
    @Scope("step")
    public class CheckAmountItemProcessor implements ItemProcessor<SalesPlanSummary, SalesPlanSummary> {

        // omitted.

        @Override
        public SalesPlanSummary process(SalesPlanSummary item) throws Exception {
            if (item.getAmount().signum() == -1) {
                logger.warn("amount is negative. skip item [item: {}]", item);

                if (!stepExecution.getExecutionContext().containsKey(ERROR_ITEMS_KEY)) {
                    stepExecution.getExecutionContext().put(ERROR_ITEMS_KEY, new ArrayList<SalesPlanSummary>());
                }
                @SuppressWarnings("unchecked")
                List<SalesPlanSummary> errorItems = (List<SalesPlanSummary>) stepExecution.getExecutionContext().get(ERROR_ITEMS_KEY);
                errorItems.add(item);

                return null;
            }
            return item;
        }

        @AfterStep
        public ExitStatus afterStep(StepExecution stepExecution) {
            if (stepExecution.getExecutionContext().containsKey(ERROR_ITEMS_KEY)) {
                logger.info("Change status 'STEP COMPLETED WITH SKIPS'");
                // (1)
                return new ExitStatus("STEP COMPLETED WITH SKIPS");
            }
            return stepExecution.getExitStatus();
        }
    }

表 185. 実装内容の一覧
項番 | 説明
--- | ---
(1) | ステップの実行結果に応じて独自の終了コードを設定する。

タスクレットモデルにおけるステップの終了コードの変更

  Taskletの`execute`メソッドの引数である`StepContribution`に任意のステップの終了コードを設定する。

Tasklet実装例

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {

        // omitted.
        if (errorCount > 0) {
            contribution.setExitStatus(new ExitStatus("STEP COMPLETED WITH SKIPS"));  // (1)
        }
        return RepeatStatus.FINISHED;
    }

表 186. 実装内容の一覧
項番 | 説明
--- | ---
(1) | タスクレットの実行結果に応じて独自の終了コードを設定する。

##### 7.2.2.2. ジョブの終了コードの変更

ジョブ終了時の処理として`JobExecutionListener`の`afterJob`メソッドを実装し、最終的なジョブの終了コードを各ステップの終了コードによって設定する。

JobExecutionListener実装例

    @Component
    public class JobExitCodeChangeListener implements JobExecutionListener {

        @Override
        public void afterJob(JobExecution jobExecution) {
            // (1)
            for (StepExecution stepExecution : jobExecution.getStepExecutions()) {
                if ("STEP COMPLETED WITH SKIPS".equals(stepExecution.getExitStatus().getExitCode())) {
                    jobExecution.setExitStatus(new ExitStatus("JOB COMPLETED WITH SKIPS"));
                    logger.info("Change status 'JOB COMPLETED WITH SKIPS'");
                    break;
                }
            }
        }
    }

JavaConfig

    @Bean
    public Job exitstatusjob(JobRepository jobRepository,
                                         Step step,
                                         jobExitCodeChangeListener listener) {
        return new JobBuilder("exitstatusjob", jobRepository)
                .start(step)
                .listener(listener)
                .build();
    }

XMLConfig

    <batch:job id="exitstatusjob" job-repository="jobRepository">
        <batch:step id="exitstatusjob.step">
            <!-- omitted -->
        </batch:step>
        <batch:listeners>
            <batch:listener ref="jobExitCodeChangeListener"/>
        </batch:listeners>
    </batch:job>

表 187. 実装内容の一覧
項番 | 説明
--- | ---
(1) | ジョブの実行結果に応じて、最終的なジョブの終了コードを`JobExecution`に設定する。 ここではステップから返却された終了コードのいずれかに`STEP COMPLETED WITH SKIPS`が含まれている場合、 終了コード`JOB COMPLETED WITH SKIPS`としている。

##### 7.2.2.3. 終了コードのマッピング

ジョブの終了コードとプロセスの終了コードをマッピング定義を行う。

JavaConfig
jp.co.ntt.fw.macchinetta.batch.functionaltest.config.LaunchContextConfig.java

    // exitCodeMapper
    @Bean
    public ExitCodeMapper exitCodeMapper() {
        final SimpleJvmExitCodeMapper simpleJvmExitCodeMapper = new SimpleJvmExitCodeMapper();
        final Map<String, Integer> exitCodeMapper = new HashMap<>();
        // ExitStatus
        exitCodeMapper.put("NOOP", 0);
        exitCodeMapper.put("COMPLETED", 0);
        exitCodeMapper.put("STOPPED", 255);
        exitCodeMapper.put("FAILED", 255);
        exitCodeMapper.put("UNKNOWN", 255);
        exitCodeMapper.put("JOB COMPLETED WITH SKIPS", 100);
        simpleJvmExitCodeMapper.setMapping(exitCodeMapper);
        return simpleJvmExitCodeMapper;
    }

XMLConfig
META-INF/spring/launch-context.xml

    <!-- exitCodeMapper -->
    <bean id="exitCodeMapper"
          class="org.springframework.batch.core.launch.support.SimpleJvmExitCodeMapper">
        <property name="mapping">
            <util:map id="exitCodeMapper" key-type="java.lang.String"
                      value-type="java.lang.Integer">
                <!-- ExitStatus -->
                <entry key="NOOP" value="0" />
                <entry key="COMPLETED" value="0" />
                <entry key="STOPPED" value="255" />
                <entry key="FAILED" value="255" />
                <entry key="UNKNOWN" value="255" />
                <entry key="JOB COMPLETED WITH SKIPS" value="100" />
            </util:map>
        </property>
    </bean>

| プロセスの終了コードに1は厳禁 一般的にJavaプロセスはVMクラッシュやSIGKILLシグナル受信などによりプロセスが強制終了した際、 終了コードとして1を返却することがある。 正常・異常を問わずバッチアプリケーションの終了コードとは明確に区別すべきであるため、 アプリケーション内ではプロセスの終了コードとして1を定義しないこと。

| 終了ステータスと終了コードの違いについて `JobRepository`で管理されるジョブとステップの状態として、「ステータス(`STATUS`)」と「終了コード(`EXIT_CODE`)」があるが、以下の点で異なる。 ステータスはSpring Batchの内部制御で用いられ enum型の`BatchStatus`による具体値が定義されているためカスタマイズできない。 終了コードはジョブのフロー制御やプロセス終了コードの変更で使用することができ、カスタマイズできる。

#### 7.2.3. 二重起動防止

Spring Batchではジョブを起動する際、`JobRepositry`から`JobInstance(BATCH_JOB_INSTANCEテーブル)`に対して 以下の組み合わせが存在するか確認する。

- 起動対象となるジョブ名

- ジョブパラメータ

Macchinetta Batch 2.xではジョブ・ジョブパラメータの組み合わせが一致しても複数回起動可能としている。
つまり、二重起動を許容する。 詳細は、ジョブの起動パラメータを参照。

二重起動を防止する場合は、ジョブスケジューラやアプリケーション内で実施する必要がある。
詳細な手段については、ジョブスケジューラ製品や業務要件に強く依存するため割愛する。
個々のジョブについて、二重起動を抑止する必要があるかについて、検討すること。

#### 7.2.4. ロギング

ログの設定方法について説明する。

ログの出力、設定、考慮事項はMacchinetta Server 1.xと共通点が多い。まずは、 ロギングを参照。

ここでは、Macchinetta Batch 2.x特有の考慮点について説明する。

##### 7.2.4.1. ログ出力元の明確化

バッチ実行時のログは出力元のジョブやジョブ実行を明確に特定できるようにしておく必要がある。 そのため、スレッド名、ジョブ名、実行ジョブIDを出力するとよい。 特に非同期実行時は同名のジョブが異なるスレッドで並列に動作することになるため、 ジョブ名のみの記録はログ出力元を特定しにくくなる恐れがある。

それぞれの要素は、以下の要領で実現できる。

スレッド名

  `logback.xml`の出力パターンである`%thread`を指定する

ジョブ名・実行ジョブID

  `JobExecutionListener`を実装したコンポーネントを作成し、ジョブの開始・終了時に記録する

JobExecutionListener実装例

    // package and import omitted.

    @Component
    public class JobExecutionLoggingListener implements JobExecutionListener {
        private static final Logger logger =
                LoggerFactory.getLogger(JobExecutionLoggingListener.class);

        @Override
        public void beforeJob(JobExecution jobExecution) {
            // (1)
            logger.info("job started. [JobName:{}][jobExecutionId:{}]",
                jobExecution.getJobInstance().getJobName(), jobExecution.getId());
        }

        @Override
        public void afterJob(JobExecution jobExecution) {
            // (2)
            logger.info("job finished.[JobName:{}][jobExecutionId:{}][ExitStatus:{}]"
                    , jobExecution.getJobInstance().getJobName(),
                    , jobExecution.getId(), jobExecution.getExitStatus().getExitCode());
        }

    }

JavaConfig

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       Tasklet tasklet) {
        return new StepBuilder("loggingJob.step01",
                jobRepository)
                .tasklet(tasklet)
                .build();
    }

    @Bean
    public Job loggingJob(JobRepository jobRepository,
                                         Step step01,
                                         JobExecutionLoggingListener listener) {
        return new JobBuilder("loggingJob", jobRepository)
                .start(step01)
                .listener(listener) // (3)
                .build();
    }

XMLConfig

    <!-- omitted. -->
    <batch:job id="loggingJob" job-repository="jobRepository">
        <batch:step id="loggingJob.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <!-- omitted. -->
            </batch:tasklet>
        </batch:step>
        <batch:listeners>
            <!-- (3) -->
            <batch:listener ref="jobExecutionLoggingListener"/>
        </batch:listeners>
    </batch:job>
    <!-- omitted. -->

表 188. ジョブ名、ジョブ実行IDのログ出力実装例
項番 | 説明
--- | ---
(1) | ジョブの開始前にジョブ名とジョブ実行IDをINFOログに出力している。
(2) | ジョブの終了時は(1)に加えて終了コードも出力している。
(3) | コンポーネントとして登録されている`JobExecutionLoggingListener`を特定ジョブのBean定義に関連づけている。

##### 7.2.4.2. ログ監視

バッチアプリケーションは運用時のユーザインタフェースはログが主体となる。 監視対象と発生時のアクションを明確に設計しておかないと、 フィルタリングが困難となり、対処に必要なログが埋もれてしまう危険がある。 このため、ログの監視対象としてキーワードとなるメッセージやコード体系をあらかじめ決めておくとよい。 ログに出力するメッセージ管理については、後述の"メッセージ管理"を参照。

##### 7.2.4.3. ログ出力先

バッチアプリケーションにおけるログの出力先について、どの単位でログを分散/集約するのかを設計するとよい。 たとえばフラットファイルにログを出力する場合でも以下のように複数パターンが考えられる。

- 1ジョブあたり1ファイルに出力する

- 複数ジョブを1グループにまとめた単位で1ファイルに出力する

- 1サーバあたり1ファイルに出力する

- 複数サーバをまとめて1ファイルに出力する

いずれも対象システムにおける、ジョブ総数/ログ総量/発生する入出力レートなどによって、 どの単位でまとめるのが最適かが分かれる。 また、ログを確認する方法にも依存する。ジョブスケジューラ上から参照することが多いか、コンソールから参照することが多いか、 といった活用方法によっても選択肢が変わると想定する。

重要なことは、運用設計にてログ出力を十分検討し、試験にてログの有用性を確認することに尽きる。

#### 7.2.5. メッセージ管理

メッセージ管理について説明する。

コード体系のばらつき防止や、監視対象のキーワードとしての抽出を設計しやすくするため、 一定のルールに従ってメッセージを付与することが望ましい。

なお、ログと同様、メッセージ管理についても基本的にはMacchinetta Server 1.xと同様であり、 プロパティファイルからメッセージを使用するために`MessageSource`を使用することができる。 具体的な設定・実装例についてはMacchinetta Server 1.xガイドラインにあるログメッセージの一元管理を参照。

##### 7.2.5.1. Macchinetta Server 1.xとの相違点

以下はMacchinetta Server 1.xとの相違点となる。

- Macchinetta Server 1.xではログ出力のサンプルとして Spring MVC のコントローラーのケースにそって例示されているが、 Macchinetta Server 1.xではSpring Batchの任意のコンポーネントに読み換えてほしい。

- Macchinetta Server 1.xでは`MessageSource`のインスタンスを独自に生成しているが、Macchinetta Batch 2.xではその必要はない。 `ApplicationContext`が生成された後でのみ、各コンポーネントにアクセスされるためである。 なお、ブランクプロジェクトには以下のとおり設定済みである。

JavaConfig
jp.co.ntt.fw.macchinetta.batch.functionaltest.config.LaunchContextConfig.java

    @Bean
    public MessageSource messageSource() {
        final ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
        resourceBundleMessageSource.setBasename("i18n/application-messages");
        return resourceBundleMessageSource;
    }

XMLConfig
META-INF/spring/launch-context.xml

    <bean id="messageSource"
          class="org.springframework.context.support.ResourceBundleMessageSource"
          p:basenames="i18n/application-messages" />

## 8. フロー制御と並列･多重処理

### 8.1. フロー制御

#### 8.1.1. Overview

1つの業務処理を実装する方法として、1つのジョブに集約して実装するのではなく、 複数のジョブに分割し組み合わせることで実装することがある。 このとき、ジョブ間の依存関係を定義したものをジョブネットと呼ぶ。

ジョブネットを定義することのメリットを下記に挙げる。

- 処理の進行状況が可視化しやすくなる

- ジョブの部分再実行、実行保留、実行中止が可能になる

- ジョブの並列実行が容易になる

以上より、バッチ処理を設計する場合はジョブネットも併せてジョブ設計を行うことが一般的である。

| 処理内容とジョブネットの適性 分割するまでもないシンプルな業務処理やオンライン処理と連携する処理に対して、ジョブネットは適さないことが多い。

本ガイドラインでは、ジョブネットでジョブ同士の流れを制御することをフロー制御と呼ぶ。 また処理の流れにおける前のジョブを先行ジョブ、後のジョブを後続ジョブと呼び、 先行ジョブと後続ジョブの依存関係を、先行後続関係と呼ぶ。

フロー制御の概念図を以下に示す。

図 49. フロー制御の概念図

上図のとおり、フロー制御はジョブスケジューラ、Macchinetta Batch 2.xのどちらでも実施可能である。 しかし、以下の理由によりできる限りジョブスケジューラを活用することが望ましい。

Macchinetta Batch 2.xで実現した場合

- 1ジョブの処理や状態が多岐に渡る傾向が強まり、ブラックボックス化しやすい。

- ジョブスケジューラとジョブの境界があいまいになってしまう

- ジョブスケジューラ上から異常時の状況がみえにくくなってしまう

ただし、ジョブスケジューラに定義するジョブ数が多くなると、以下の様なデメリットが生じることも一般に知られている。

- ジョブスケジューラによる以下のようなコストが累積し、システム全体の処理時間が伸びる

  - ジョブスケジューラ製品固有の通信、実行ノードの制御、など

  - ジョブごとのJavaプロセス起動に伴うオーバーヘッド

- ジョブ登録数の限界

このため、以下を方針とする。

- 基本的にはジョブスケジューラによりフロー制御を行う。

- ジョブ数が多いことによる弊害がある場合に限り、以下のとおり対処する。

  - Macchinetta Batch 2.xにてシーケンシャルな複数の処理を1ジョブにまとめる。

    - シンプルな先行後続関係を1ジョブに集約するのみとする。

    - ステップ終了コードの変更と、この終了コードに基づく後続ステップ起動の条件分岐は機能上利用可能だが、 ジョブの実行管理が複雑化するため、ジョブ終了時のプロセス終了コード決定に限り原則利用する。
      どうしても条件分岐を使わないと問題を解消できない場合に限り使用を許容するが、 シンプルな先行後続関係を維持するよう配慮すること。

| ジョブの終了コードの決定について、詳細は"終了コードのカスタマイズ"を参照。

また、以下に先行後続を実現する上で意識すべきポイントを示す。

図 50. ジョブスケジューラによるフロー制御
意識すべきポイント

- ジョブスケジューラがシェル等を介してjavaプロセスを起動する。

- 1ジョブが1javaプロセスとなる。

  - 処理全体では、4つのjavaプロセスが起動する。

- ジョブスケジューラが各処理の起動順序を制御する。ぞれぞれのjavaプロセスは独立している。

- 後続ジョブの起動判定として、先行ジョブのプロセス終了コードが用いられる。

- ジョブ間のデータ受け渡しは、ファイルやデータベースなど外部リソースを使用する必要がある。

図 51. Macchinetta Batch 2.xによるフロー制御
意識すべきポイント

- ジョブスケジューラがシェル等を介してjavaプロセスを起動する。

- 1ジョブが1javaプロセスとなる。

  - 処理全体では、1つのjavaプロセスしか使わない。

- 1javaプロセス内で各ステップの起動順序を制御する。それぞれのステップは独立している。

- 後続ステップの起動判定として、先行ステップの終了コードが用いられる。

- ステップ間のデータはインメモリで受け渡しが可能である。

以降、Macchinetta Batch 2.xによるフロー制御の実現方法について説明する。
ジョブスケジューラでのフロー制御は製品仕様に強く依存するためここでは割愛する。

| フロー制御の応用例 複数ジョブの並列化・多重化は、一般的にジョブスケジューラとジョブネットによって実現することが多い。 しかし、Macchinetta Batch 2.xではフロー制御の機能を応用し、複数ジョブの並列化、多重化を実現する方法を説明している。 詳細は、並列処理と多重処理を参照。

本機能は、チャンクモデルとタスクレットモデルとで同じ使い方になる。

#### 8.1.2. How to use

Macchinetta Batch 2.xでのフロー制御方法を説明する。

##### 8.1.2.1. シーケンシャルフロー

シーケンシャルフローとは先行ステップと後続ステップを直列に連結したフローである。
何らかの業務処理がシーケンシャルフロー内のステップで異常終了した場合、後続ステップは実行されずにジョブが中断する。 このとき、`JobRepository`によりジョブ実行IDに紐付けられる当該のステップとジョブのステータス・終了コードは `FAILED`として記録される。
失敗原因の回復後にリスタートを実施することで、異常終了したステップから処理をやり直すことができる。

| ジョブのリスタート方法についてはジョブのリスタートを参照。

ここでは3つのステップからなるジョブのシーケンシャルフローを設定する。

JavaConfig

    // tasklet definition is omitted.

    TaskletStepBuilder parentStepBuilder(String stepName,
                                         JobRepository jobRepository,
                                         SequentialFlowTasklet tasklet,
                                         PlatformTransactionManager transactionManager) {
        return new StepBuilder(stepName, jobRepository)
                .tasklet(tasklet, transactionManager);
    }

    @Bean
    public Step step01(JobRepository jobRepository,
                       SequentialFlowTasklet tasklet,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return parentStepBuilder("jobSequentialFlow.step1", jobRepository,
                tasklet, transactionManager)
                .build();
    }

    @Bean
    public Step step02(JobRepository jobRepository,
                       SequentialFlowTasklet tasklet,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return parentStepBuilder("jobSequentialFlow.step2", jobRepository,
                tasklet, transactionManager)
                .build();
    }

    @Bean
    public Step step03(JobRepository jobRepository,
                       SequentialFlowTasklet tasklet,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return parentStepBuilder("jobSequentialFlow.step3", jobRepository,
                tasklet, transactionManager)
                .build();
    }

    @Bean
    public Job jobSequentialFlow(JobRepository jobRepository,
                                 Step step01, Step step02, Step step03) {
        Flow flow = new FlowBuilder<Flow>("jobSequentialFlow")
                .from(step01)
                .next(step02) // (1)
                .next(step03) // (1)
                .build(); // (2)

        return new JobBuilder("jobSequentialFlow", jobRepository)
                .start(flow)
                .end()
                .build();
    }

XMLConfig

    <!-- tasklet definition is omitted. -->

    <batch:step id="parentStep">
        <batch:tasklet ref="sequentialFlowTasklet"
                       transaction-manager="jobTransactionManager"/>
    </batch:step>

    <batch:job id="jobSequentialFlow" job-repository="jobRepository">
        <batch:step id="jobSequentialFlow.step1"
                    next="jobSequentialFlow.step2" parent="parentStep"/> <!-- (1) -->
        <batch:step id="jobSequentialFlow.step2"
                    next="jobSequentialFlow.step3" parent="parentStep"/> <!-- (1) -->
        <batch:step id="jobSequentialFlow.step3" parent="parentStep"/>   <!-- (2) -->
    </batch:job>

項番 | 説明
--- | ---
(1) | `FlowBuilderのnextメソッド/<batch:step>`で、このステップの正常終了後に起動する後続ステップを指定する。 `nextメソッドの引数step/next属性`に後続ステップのidを設定する。
(2) | フローの末端になるステップには、`nextメソッド/next属性`は不要となる。

これにより、 以下の順でステップが直列に起動する。
`jobSequentialFlow.step1` → `jobSequentialFlow.step2` → `jobSequentialFlow.step3`

上述の例では`jobSequentialFlowメソッド/<batch:job>`内に直接フローを定義した。 いっぽうで、フロー定義を`jobSequentialFlowメソッド/<batch:job>`の外で記述する方法もある。 詳しくは、外部でのフロー定義方法を参照のこと。

##### 8.1.2.2. ステップ間のデータの受け渡し

Spring Batchには、ステップ、ジョブそれぞれのスコープで利用できる実行コンテキストの`ExecutionContext`が用意されている。 ステップ実行コンテキストを利用することでステップ内のコンポーネント間でデータを共有できる。 このとき、ステップ実行コンテキストはステップ間で共有できないため、先行のステップ実行コンテキストは後続のステップ実行コンテキストからは参照できない。 ジョブ実行コンテキストを利用すれば実現可能だが、すべてのステップから参照可能になるため、慎重に扱う必要がある。 ステップ間の情報を引き継ぐ必要があるときは、以下の手順により対応できる。

1. 先行ステップの後処理で、ステップ実行コンテキストに格納した情報をジョブ実行コンテキストに移す。

2. 後続ステップがジョブ実行コンテキストから情報を取得する。

最初の手順は、Spring Batchから提供されている`ExecutionContextPromotionListener`を利用することで、 実装をせずとも、引き継ぎたい情報をリスナーに指定するだけ実現できる。

| ExecutionContextを使用する上での注意点 データの受け渡しに使用する`ExecutionContext`は`JobRepository`によりRDBMSの `BATCH_JOB_EXECUTION_CONTEXT`、`BATCH_JOB_STEP_EXECUTION_CONTEXT`に シリアライズされた状態で保存されるため、以下3点に注意すること。 受け渡しデータはシリアライズ可能な形式のオブジェクトであること。 `java.io.Serializable`を実装している必要がある。 受け渡しデータは必要最小限に留めること。 `ExecutionContext`はSpring Batchによる実行制御情報の保存でも利用しており、 受け渡しデータが大きくなればそれだけシリアライズコストが増大する。 データ受け渡しに直接ジョブ実行コンテキストに保存させず、上述の`ExecutionContextPromotionListener`を使用すること。 ジョブ実行コンテキストはステップ実行コンテキストよりスコープが広いため、無用なシリアライズデータが蓄積しやすいため。 また、実行コンテキストを経由せず、SingletonやJobスコープのBeanを共有することでも情報のやり取りは可能だが、 この方法もサイズが大きすぎるとメモリリソースを圧迫する可能性があるので注意すること。

以下、タスクレットモデルとチャンクモデルについて、それぞれステップ間のデータ受け渡しについて説明する。

###### 8.1.2.2.1. タスクレットモデルを用いたステップ間のデータ受け渡し

受け渡しデータの保存・取得に、`ChunkContext`から`ExecutionContext`を取得し、ステップ間のデータ受け渡しを行う。

データ受け渡し元タスクレットの実装例

    // package, imports are omitted.

    @Component
    public class SavePromotionalTasklet implements Tasklet {

        // omitted.

        @Override
        public RepeatStatus execute(StepContribution contribution,
                ChunkContext chunkContext) throws Exception {

            // (1)
            chunkContext.getStepContext().getStepExecution().getExecutionContext()
                    .put("promotion", "value1");

            // omitted.

            return RepeatStatus.FINISHED;
        }
    }

データ受け渡し先のタスクレット実装例

    // package and imports are omitted.

    @Component
    public class ConfirmPromotionalTasklet implements Tasklet {

        @Override
        public RepeatStatus execute(StepContribution contribution,
                ChunkContext chunkContext) {
            // (2)
            Object promotion = chunkContext.getStepContext().getJobExecutionContext()
                    .get("promotion");

            // omitted.

            return RepeatStatus.FINISHED;
        }
    }

JavaConfig

    // import,annotation,component-scan definitions are omitted

    // (3)
    @Bean
    ExecutionContextPromotionListener executionContextPromotionListener() {
        ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener();
        listener.setKeys(new String[] { "promotion" });
        listener.setStrict(true);
        return listener;
    }

    @Bean
    public Step step1(JobRepository jobRepository,
                      @Qualifier("savePromotionalTasklet") SavePromotionalTasklet tasklet,
                      @Qualifier("executionContextPromotionListener") ExecutionContextPromotionListener listener,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return new StepBuilder("jobPromotionalFlow.step1", jobRepository)
                .tasklet(tasklet, transactionManager)
                .listener(listener)
                .build();
    }

    @Bean
    public Step step2(JobRepository jobRepository,
                      @Qualifier("confirmPromotionalTasklet") ConfirmPromotionalTasklet tasklet,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return new StepBuilder("jobPromotionalFlow.step2", jobRepository)
                .tasklet(tasklet, transactionManager)
                .build();
    }

    @Bean
    public Job jobPromotionalFlow(JobRepository jobRepository,
                                  Step step1, Step step2) {
        return new JobBuilder("jobPromotionalFlow", jobRepository)
                .start(step1)
                .next(step2)
                .build();
    }

XMLConfig

    <!-- import,annotation,component-scan definitions are omitted -->

    <batch:job id="jobPromotionalFlow" job-repository="jobRepository">
        <batch:step id="jobPromotionalFlow.step1" next="jobPromotionalFlow.step2">
            <batch:tasklet ref="savePromotionalTasklet"
                           transaction-manager="jobTransactionManager"/>
            <batch:listeners>
                <batch:listener>
                    <!-- (3) -->
                    <bean class="org.springframework.batch.core.listener.ExecutionContextPromotionListener"
                          p:keys="promotion"
                          p:strict="true"/>
                </batch:listener>
            </batch:listeners>
        </batch:step>
        <batch:step id="jobPromotionalFlow.step2">
            <batch:tasklet ref="confirmPromotionalTasklet"
                           transaction-manager="jobTransactionManager"/>
        </batch:step>
    </batch:job>
    <!-- omitted -->

表 189. 実装内容の説明
項番 | 説明
--- | ---
(1) | ステップ実行コンテキストの`ExecutionContext`に後続ステップに受け渡す値を設定する。 ここでは一連のデータ受け渡しに必要なキーとして、`promotion`を指定している。
(2) | 先行ステップの(1)で設定された受け渡しデータを`ExecutionContext`から、 受け渡し元で指定されたキー`promotion`を用いて取得する。 ここで使用している`ExecutionContext`は(1)のステップ実行コンテキストではなく、 ジョブ実行コンテキストである点に注意する。
(3) | `ExecutionContextPromotionListener`を用い、 ステップ実行コンテキストからジョブ実行コンテキストに受け渡しデータを移す。 `setKeysメソッド/keys属性`には(1)で指定した受け渡しキーを指定する。 `setStrictメソッド/strict属性`に`true`を設定することにより、ステップ実行コンテキストに存在しない場合は`IllegalArgumentException`がスローされる。 `false`の場合は受け渡しデータがなくても処理が継続する。

| ExecutionContextPromotionListenerとステップ終了コードについて `ExecutionContextPromotionListener`はデータ受け渡し元のステップ終了コードが正常終了時(`COMPLETED`)の場合のみ、 ステップ実行コンテキストからジョブ実行コンテキストへデータを移す。 後続ステップが継続して実行される終了コードのカスタマイズを行う場合、 `status`プロパティに終了コードを配列形式で指定すること。

###### 8.1.2.2.2. チャンクモデルを用いたステップ間のデータ受け渡し

`ItemProcessor`に`@AfterStep`、`@BeforeStep`アノテーションを付与したメソッドを使用する。 データ受け渡しに使用するリスナーと、`ExecutionContext`の使用方法はタスクレットと同様である。

データ受け渡し元ItemProcessorの実装例

    // package and imports are omitted.

    @Component
    @Scope("step")
    public class PromotionSourceItemProcessor implements ItemProcessor<String, String> {

        @Override
        public String process(String item) {
            // omitted.
        }

        @AfterStep
        public ExitStatus afterStep(StepExecution stepExecution) {
            // (1)
            stepExecution.getExecutionContext().put("promotion", "value2");

            return null;
        }
    }

データ受け渡し先ItemProcessorの実装例

    // package and imports are omitted.

    @Component
    @Scope("step")
    public class PromotionTargetItemProcessor implements ItemProcessor<String, String> {

        @Override
        public String process(String item) {
            // omitted.
        }

        @BeforeStep
        public void beforeStep(StepExecution stepExecution) {
            // (2)
            Object promotion = stepExecution.getJobExecution().getExecutionContext()
                    .get("promotion");
            // omitted.
        }
    }

JavaConfig

    // (3)
    @Bean
    ExecutionContextPromotionListener executionContextPromotionListener() {
        ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener();
        listener.setKeys(new String[] { "promotion" });
        listener.setStrict(true);
        return listener;
    }

    @Bean
    public Step step1(JobRepository jobRepository,
                      @Qualifier("executionContextPromotionListener") ExecutionContextPromotionListener listener,
                      @Qualifier("listItemReader") ListItemReader<String> reader,
                      @Qualifier("promotionSourceItemProcessor") ItemProcessor<String, String> processor,
                      @Qualifier("promotionLogItemWriter") ItemWriter<String> writer,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        SimpleStepBuilder<String, String> builder
                = parentStepBuilder("jobChunkPromotionalFlow.step1",
                jobRepository, reader, processor, writer, transactionManager);
        builder.listener(listener);
        return builder.build();
    }

    @Bean
    public Step step2(JobRepository jobRepository,
                      @Qualifier("listItemReader") ListItemReader<String> reader,
                      @Qualifier("promotionTargetItemProcessor") ItemProcessor<String, String> processor,
                      @Qualifier("promotionLogItemWriter") ItemWriter<String> writer,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        SimpleStepBuilder<String, String> builder
                = parentStepBuilder("jobChunkPromotionalFlow.step2",
                jobRepository, reader, processor, writer, transactionManager);
        return builder.build();
    }

    @Bean
    public Job jobChunkPromotionalFlow(JobRepository jobRepository,
                                       Step step1, Step step2) {
        return new JobBuilder("jobChunkPromotionalFlow", jobRepository)
                .start(step1)
                .next(step2)
                .build();
    }

    SimpleStepBuilder<String, String> parentStepBuilder(String stepName,
                                                        JobRepository jobRepository,
                                                        ListItemReader<String> reader,
                                                        ItemProcessor<String, String> processor,
                                                        ItemWriter<String> writer,
                                                        PlatformTransactionManager transactionManager) {
        return new StepBuilder(stepName, jobRepository)
                .<String, String> chunk(1, transactionManager)
                .reader(reader)
                .processor(processor)
                .writer(writer);
    }

XMLConfig

    <!-- import,annotation,component-scan definitions are omitted -->
    <batch:job id="jobChunkPromotionalFlow" job-repository="jobRepository">
        <batch:step id="jobChunkPromotionalFlow.step1" parent="sourceStep"
                    next="jobChunkPromotionalFlow.step2">
            <batch:listeners>
                <batch:listener>
                    <!-- (3) -->
                    <bean class="org.springframework.batch.core.listener.ExecutionContextPromotionListener"
                          p:keys="promotion"
                          p:strict="true" />
                </batch:listener>
            </batch:listeners>
        </batch:step>
        <batch:step id="jobChunkPromotionalFlow.step2" parent="targetStep"/>
    </batch:job>

    <!-- step definitions are omitted. -->

表 190. 実装内容の説明
項番 | 説明
--- | ---
(1) | ステップ実行コンテキストの`ExecutionContext`に後続ステップに受け渡す値を設定する。 ここでは一連のデータ受け渡しに必要なキーとして、`promotion`を指定している。
(2) | 先行ステップの(1)で設定された受け渡しデータを`ExecutionContext`から、 受け渡し元で指定されたキー`promotion`を用いて取得する。 ここで使用している`ExecutionContext`は(1)のステップ実行コンテキストではなく、 ジョブ実行コンテキストである点に注意する。
(3) | `ExecutionContextPromotionListener`を用い、 ステップ実行コンテキストからジョブ実行コンテキストに受け渡しデータを移す。 プロパティの指定はタスクレットと同様である。

#### 8.1.3. How to extend

ここでは後続ステップの条件分岐と、条件により後続ステップ実行前にジョブを停止させる停止条件について説明する。

| ジョブ・ステップの終了コードとステータスの違い。 以降の説明では「ステータス」と「終了コード」という言葉が頻繁に登場する。 これらの判別がつかない場合混乱を招く恐れがあるため、 まず"終了コードのカスタマイズ"を参照。

##### 8.1.3.1. 条件分岐

条件分岐は先行ステップの実行結果となる終了コードを受けて、複数の後続ステップから1つを選択して継続実行させることを言う。
いずれの後続ステップを実行させずにジョブを停止させる場合は後述の"停止条件"を参照。

JavaConfig

    @Bean
    public Step stepA(JobRepository jobRepository,
                      SequentialFlowTasklet tasklet,
                      ChangeExitCodeReturnListener listener,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return parentStepBuilder("jobConditionalFlow.stepA", jobRepository,
                tasklet, listener, transactionManager)
                .build();
    }

    @Bean
    public Step stepB(JobRepository jobRepository,
                      SequentialFlowTasklet tasklet,
                      ChangeExitCodeReturnListener listener,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return parentStepBuilder("jobConditionalFlow.stepB", jobRepository,
                tasklet, listener, transactionManager)
                .build();
    }

    @Bean
    public Step stepC(JobRepository jobRepository,
                      SequentialFlowTasklet tasklet,
                      ChangeExitCodeReturnListener listener,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return parentStepBuilder("jobConditionalFlow.stepC", jobRepository,
                tasklet, listener, transactionManager)
                .build();
    }

    @Bean
    public Job jobConditionalFlow(JobRepository jobRepository, Step stepA,
                                  Step stepB, Step stepC) {
        return new JobBuilder("jobConditionalFlow", jobRepository)
                .start(stepA)
                .on(FlowExecutionStatus.COMPLETED.getName()).to(stepB) // (1) (2)
                .from(stepA)
                .on(FlowExecutionStatus.FAILED.getName()).to(stepC) // (1) (3)
                .end()
                .build();
    }

XMLConfig

    <batch:job id="jobConditionalFlow" job-repository="jobRepository">
        <batch:step id="jobConditionalFlow.stepA" parent="conditionalFlow.parentStep">
            <!-- (1) -->
            <batch:next on="COMPLETED" to="jobConditionalFlow.stepB" />
            <batch:next on="FAILED" to="jobConditionalFlow.stepC"/>
        </batch:step>
        <!-- (2) -->
        <batch:step id="jobConditionalFlow.stepB" parent="conditionalFlow.parentStep"/>
        <!-- (3) -->
        <batch:step id="jobConditionalFlow.stepC" parent="conditionalFlow.parentStep"/>
    </batch:job>

表 191. 実装内容の説明
項番 | 説明
--- | ---
(1) | シーケンシャルフローのように`nextメソッド/next属性`を指定させず、 `nextメソッド/<batch:next>要素`を複数置くことで、`toメソッド/to属性`で指定される後続ステップに振り分けることができる。 `onメソッド/on属性`には遷移条件となるステップの終了コードを指定する。
(2) | (1)のステップ終了コードが`COMPLETED`の場合のみに実行される後続ステップとなる。
(3) | (1)のステップ終了コードが`FAILED`の場合のみに実行される後続ステップとなる。 この指定が行われることで先行ステップ処理失敗時にジョブが停止せず、回復処理などの後続ステップが実行される。

| 後続ステップによる回復処理の注意点 先行ステップの処理失敗(終了コードが`FAILED`)により後続ステップの回復処理が行われた場合、 回復処理の成否を問わず先行ステップのステータスは`ABANDONED`となり、リスタート不能となる。 後続ステップの回復処理が失敗した場合にジョブをリスタートすると、回復処理のみが再実行される。 このため、先行ステップを含めて処理をやり直す場合は別のジョブ実行としてリランさせる必要がある。

##### 8.1.3.2. 停止条件

先行ステップの終了コードに応じ、ジョブを停止させる方法を説明する。
停止の手段として、以下の3つの要素を指定する方法がある。

1. `end`

2. `fail`

3. `stop`

これらの終了コードが先行ステップに該当する場合は後続ステップが実行されない。
また、同一ステップ内にそれぞれ複数指定が可能である。

JavaConfig

    TaskletStepBuilder parentStepBuilder(String stepName,
                                         JobRepository jobRepository,
                                         SequentialFlowTasklet tasklet,
                                         ChangeExitCodeReturnListener listener,
                                         PlatformTransactionManager transactionManager) {
        return new StepBuilder(stepName, jobRepository)
                .listener(listener)
                .tasklet(tasklet, transactionManager);
    }

    @Bean
    public Step step1(JobRepository jobRepository,
                      @Qualifier("stopFlowTasklet") SequentialFlowTasklet tasklet,
                      @Qualifier("changeExitCodeReturnListener") ChangeExitCodeReturnListener listener,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return parentStepBuilder("jobStopFlow.step1", jobRepository, tasklet,
                listener, transactionManager)
                .build();
    }

    @Bean
    public Step step2(JobRepository jobRepository,
                      @Qualifier("stopFlowTasklet") SequentialFlowTasklet tasklet,
                      @Qualifier("changeExitCodeReturnListener") ChangeExitCodeReturnListener listener,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return parentStepBuilder("jobStopFlow.step2", jobRepository, tasklet,
                listener, transactionManager)
                .build();
    }

    @Bean
    public Step step3(JobRepository jobRepository,
                      @Qualifier("stopFlowTasklet") SequentialFlowTasklet tasklet,
                      @Qualifier("changeExitCodeReturnListener") ChangeExitCodeReturnListener listener,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return parentStepBuilder("jobStopFlow.step3", jobRepository, tasklet,
                listener, transactionManager)
                .build();
    }

    @Bean
    public Step step4(JobRepository jobRepository,
                      @Qualifier("stopFlowTasklet") SequentialFlowTasklet tasklet,
                      @Qualifier("changeExitCodeReturnListener") ChangeExitCodeReturnListener listener,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return parentStepBuilder("jobStopFlow.step4", jobRepository, tasklet,
                listener, transactionManager)
                .build();
    }

    @Bean
    public Job jobStopFlow(JobRepository jobRepository,
                           @Qualifier("jobExitCodeChangeListener") JobExitCodeChangeListener listener,
                           Step step1, Step step2, Step step3, Step step4) {
        return new JobBuilder("jobStopFlow", jobRepository)
                .start(step1)
                .listener(listener)
                .on("END_WITH_NO_EXIT_CODE").end()
                .from(step1).on("END_WITH_EXIT_CODE").end("COMPLETED_CUSTOM")
                .from(step1).on("*").to(step2)
                .from(step2).on("FORCE_FAIL_WITH_NO_EXIT_CODE").fail()
                .from(step2).on("FORCE_FAIL_WITH_EXIT_CODE").fail()
                .from(step2).on("*").to(step3)
                .from(step3).on("FORCE_STOP").stopAndRestart(step4)
                .from(step3).on("FORCE_STOP_WITH_EXIT_CODE")
                .stopAndRestart(step4)
                .from(step3).on("*").to(step4)
                .end()
                .build();
    }

XMLConfig

    <batch:job id="jobStopFlow" job-repository="jobRepository">
        <batch:step id="jobStopFlow.step1" parent="stopFlow.parentStep">
            <!-- (1) -->
            <batch:end on="END_WITH_NO_EXIT_CODE"/>
            <batch:end on="END_WITH_EXIT_CODE" exit-code="COMPLETED_CUSTOM"/>
            <!-- (2) -->
            <batch:next on="*" to="jobStopFlow.step2"/>
        </batch:step>
        <batch:step id="jobStopFlow.step2" parent="stopFlow.parentStep">
            <!-- (3) -->
            <batch:fail on="FORCE_FAIL_WITH_NO_EXIT_CODE"/>
            <batch:fail on="FORCE_FAIL_WITH_EXIT_CODE" exit-code="FAILED_CUSTOM"/>
            <!-- (2) -->
            <batch:next on="*" to="jobStopFlow.step3"/>
        </batch:step>
        <batch:step id="jobStopFlow.step3" parent="stopFlow.parentStep">
            <!-- (4) -->
            <batch:stop on="FORCE_STOP" restart="jobStopFlow.step4"/>
            <!-- (2) -->
            <batch:next on="*" to="jobStopFlow.step4"/>
        </batch:step>
        <batch:step id="jobStopFlow.step4" parent="stopFlow.parentStep"/>
    </batch:job>

表 192. ジョブの停止の設定内容説明
項番 | 説明
--- | ---
(1) | `onメソッドの引数pattern/<batch:end>要素のon属性`とステップ終了コードが一致した場合、ジョブは正常終了 (ステータス：`COMPLETED`)として`JobRepository`に記録される。 `endメソッドの引数status/exit-code属性`を付与した場合、ジョブの終了コードをデフォルトの`COMPLETED`からカスタマイズすることができる。
(2) | `onメソッドの引数pattern/<batch:next>要素のon属性`にワイルドカード(`*`)を指定することで、`end`、`fail`、 `stop`いずれの終了コードにも該当しない場合に後続ジョブを継続させることができる。 ここではステップ要素内の最後に記述しているが、終了コードの一致条件が先に評価されるため、 要素の並び順はステップ要素内であれば任意である。
(3) | `failメソッド/<batch:fail>要素`を使用した場合、ジョブは異常終了(ステータス：`FAILED`)として`JobRepository`に記録される。 `<batch:end>`と同様、`exit-code`属性を付与することで、ジョブの終了コードをデフォルトの`FAILED`からカスタマイズすることができる。
(4) | `stopAndRestartメソッド/<batch:stop>要素`を使用した場合、ステップの正常終了時にジョブは中断(ステータス：`STOPPED`)として`JobRepository`に記録される。 `stopAndRestartメソッドの引数restart/restart属性`はリスタート時に中断状態からジョブが再開されるステップを指定する。

| exit-code属性による終了コードのカスタマイズ時は漏れなくプロセス終了コードにマッピングさせること。 詳細は"終了コードのカスタマイズ"を参照。

#### 8.1.4. Appendix

###### 8.1.4.1. 外部でのフロー定義方法

`Bean定義したFlow/<batch:flow>`を利用して、フロー定義を外部に切り出すことができる。
以下に`Bean定義したFlow/<batch:flow>`を利用して、3つのステップからなるジョブのシーケンシャルフローを設定する例を示す。

JavaConfig

    @Bean
    public Step step01outer(JobRepository jobRepository,
                            SequentialFlowTasklet tasklet,
                            @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return parentStepBuilder("jobSequentialOuterFlow.step1", jobRepository,
                tasklet, transactionManager)
                .build();
    }

    @Bean
    public Step step02outer(JobRepository jobRepository,
                            SequentialFlowTasklet tasklet,
                            @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return parentStepBuilder("jobSequentialOuterFlow.step2", jobRepository,
                tasklet, transactionManager)
                .build();
    }

    @Bean
    public Step step03outer(JobRepository jobRepository,
                            SequentialFlowTasklet tasklet,
                            @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return parentStepBuilder("jobSequentialOuterFlow.step3", jobRepository,
                tasklet, transactionManager)
                .build();
    }

    // (2)
    @Bean
    public Flow outerFlow(Step step01outer, Step step02outer,
                          Step step03outer) {
        return new FlowBuilder<Flow>("outerFlow")
                .from(step01outer)
                .next(step02outer)
                .next(step03outer)
                .build();
    }

    @Bean
    public Job jobSequentialOuterFlow(JobRepository jobRepository,
                                      Flow outerFlow) {
        return new JobBuilder("jobSequentialOuterFlow", jobRepository)
                .start(outerFlow) // (1)
                .end()
                .build();
    }

XMLConfig

    <batch:job id="jobSequentialOuterFlow" job-repository="jobRepository">
        <batch:flow id="innerFlow" parent="outerFlow"/> <!-- (1) -->
    </batch:job>

    <!-- (2) -->
    <batch:flow id="outerFlow">
        <batch:step id="jobSequentialOuterFlow.step1"
                    next="jobSequentialOuterFlow.step2"
                    parent="parentStep"/>
        <batch:step id="jobSequentialOuterFlow.step2"
                    next="jobSequentialOuterFlow.step3"
                    parent="parentStep"/>
        <batch:step id="jobSequentialOuterFlow.step3"
                    parent="parentStep"/>
    </batch:flow>

項番 | 説明
--- | ---
(1) | `JobBuilderのstartメソッド/parent属性`に(2)で定義したフローのidを設定する。
(2) | シーケンシャルフローを定義する。

これにより、 以下の順でステップが直列に起動する。
`jobSequentialFlow.step1` → `jobSequentialFlow.step2` → `jobSequentialFlow.step3`

### 8.2. 並列処理と多重処理

#### 8.2.1. Overview

一般的に、バッチウィンドウ(バッチ処理のために使用できる時間)がシビアなバッチシステムでは、 複数ジョブを並列に動作させる(以降、並列処理と呼ぶ)ことで全体の処理時間を可能な限り短くするように設計する。
しかし、1ジョブの処理データが大量であるために処理時間がバッチウィンドウに収まらない場合がある。
その際は、1ジョブの処理データを分割して多重走行させる(以降、多重処理と呼ぶ)ことで処理時間を短縮させる手法が用いられる。
この、並列処理と多重処理は同じような意味合いで扱われることもあるが、ここでは以下の定義とする。

並列処理

  複数の異なるジョブを、同時に実行する。

図 52. 並列処理の概略図

多重処理

  1ジョブの処理対象を分割して、同時に実行する。

図 53. 多重処理の概略図

並列処理と多重処理ともにジョブスケジューラで行う方法とMacchinetta Batch 2.xで行う方法がある。
なお、Macchinetta Batch 2.xでの並列処理および多重処理は フロー制御の上に成り立っている。

表 193. 並列処理および多重処理の実現方法
実現方法 | 並列処理 | 多重処理
--- | --- | ---
ジョブスケジューラ | 依存関係がない複数の異なるジョブを同時に実行するように定義する。 | 複数の同じジョブを異なるデータ範囲で実行するように定義する。各ジョブに処理対象のデータを絞るための情報を引数などで渡す。 たとえば、1年間のデータを月ごとに分割する、エリアや支店などの単位で分割する、など
Macchinetta Batch 2.x | Parallel Step (並列処理) ステップ単位で並列処理を行う。 各ステップは同じ処理である必要はなく、データベースとファイルというような種類が異なるリソースに対して並列で処理を行う事も可能である。 | Partitioning Step (多重処理) Managerステップでは対象データを分割するためのキーを取得し、 Workerステップではこのキーにもとづいて分割したデータを処理する。 Parallel Stepとは異なりWorkerステップの処理は同一処理となる。

ジョブスケジューラを使用する場合

  1ジョブに1プロセスが割り当てられるため複数プロセスで起動される。 そのため、1つのジョブを設計・実装する難易度は低い。
  しかし、複数プロセスで起動するため、同時実行数が増えるとマシンリソースへの負荷が高くなる。
  よって、同時実行数が3、4程度であれば、ジョブスケジューラを利用するとよい。
  もちろん、この数値は絶対的なものではない。実行環境やジョブの実装に依存するため目安としてほしい。

Macchinetta Batch 2.xを使用する場合

  各ステップがスレッドに割り当てられるため、1プロセス複数スレッドで動作する。そのため、1つのジョブへの設計・実装の難易度はジョブスケジューラを使用する場合より高くなる。
  しかし、複数スレッドで起動するため、同時実行数が増えてもマシンリソースへの負荷がジョブスケジューラを使用する場合ほど高くはならない。 よって、同時実行数が多い(5以上の)場合であれば、Macchinetta Batch 2.xを利用するのがよい。
  もちろん、この数値は絶対的なものではない。実行環境やシステム特性に依存するため目安としてほしい。

| Spring Batchで実行可能な並列処理方法の1つに`Multi Thread Step`があるが、以下の理由によりMacchinetta Batch 2.xでの利用は非推奨とする。 Multi Thread Stepとは チャンク単位で複数スレッドで並列処理を行う方法。 非推奨理由 Spring Batchが提供しているReaderやWriterのほとんどが、マルチスレッドでの利用を想定して設計されていない。 そのため、データロストや重複処理が発生する可能性があり、処理の信頼性が低い。また、複数スレッドで動作するため、一定の処理順序とならない。 ItemReader/ItemProcessor/ItemWriterを自作する場合でもスレッドセーフなど`Multi Thread Step`を使うためには考慮すべき点が多く実装および運用の難易度が高くなる。 これらの理由により、`Multi Thread Step`は非推奨としている。 代わりにPartitioning Step (多重処理)を利用することを推奨する。

| 並列処理・多重処理で1つのデータベースに対して更新する場合は、リソース競合とデッドロックが発生する可能性がある。 ジョブ設計の段階から潜在的な競合発生を排除すること。 マルチプロセスや複数筐体への分散処理は、Spring Batchに機能があるが、Macchinetta Batch 2.xとしては障害設計が困難になるため扱わないこととする。

本機能は、チャンクモデルとタスクレットモデルとで同じ使い方になる

##### 8.2.1.1. ジョブスケジューラによる並列処理と多重処理

ここでは、ジョブスケジューラによる並列処理と多重処理の概念について説明を行う。

ジョブ登録、スケジュール設定などについては、使用するジョブスケジューラのマニュアルを参照。

###### 8.2.1.1.1. ジョブスケジューラによるジョブの並列化

並列実行させたい処理をそれぞれジョブとして登録、それぞれのジョブが同時に開始するようにスケジュールを設定する。 各々のジョブは異なる処理として登録してよい。

###### 8.2.1.1.2. ジョブスケジューラによるジョブの多重化

多重実行させたい処理を複数登録し、パラメータにより対象データの抽出範囲を指定する。 その上で、それぞれのジョブが同時に開始するようにスケジュールを設定する。 各々のジョブは同じ処理ではあるが、処理対象データ範囲は独立していることが必要となる。

#### 8.2.2. How to use

Macchinetta Batch 2.xでの並列処理および多重処理を行う方法を説明する。

##### 8.2.2.1. Parallel Step (並列処理)

Parallel Step (並列処理)の方法を説明する。

図 54. Parallel Stepの概要図
概要図の説明

各ステップに別々な処理を定義することができ、それらを並列に実行することができる。 各ステップごとにスレッドが割り当てられる。

Parallel Stepの概要図を例にしたParallel Stepの定義方法を以下に示す。

JavaConfig

    // (1)
    @Bean
    public TaskExecutor parallelTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setQueueCapacity(200);
        return executor;
    }

    // (4)
    @Bean
    public Step stepChunkDb(JobRepository jobRepository,
                      ItemReader fileReader,
                      ItemWriter databaseWriter,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return new StepBuilder("parallelStepJob.step.chunk.db", jobRepository)
                .chunk(100, transactionManager)
                .reader(fileReader)
                .writer(databaseWriter)
                .build();
    }

    // (5)
    @Bean
    public Step stepTaskletChunk(JobRepository jobRepository,
                      Tasklet chunkTransactionTasklet,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return new StepBuilder("parallelStepJob.step.tasklet.chunk", jobRepository)
                .tasklet(chunkTransactionTasklet, transactionManager)
                .build();
    }

    // (6)
    @Bean
    public Step stepTaskletSingle(JobRepository jobRepository,
                      Tasklet singleTransactionTasklet,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return new StepBuilder("parallelStepJob.step.tasklet.single", jobRepository)
                .tasklet(singleTransactionTasklet, transactionManager)
                .build();
    }

    // (7)
    @Bean
    public Step stepChunkDb(JobRepository jobRepository,
                      ItemReader databaseReader,
                      ItemWriter fileWriter,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return new StepBuilder("parallelStepJob.step.chunk.file", jobRepository)
                .chunk(200, transactionManager)
                .reader(databaseReader)
                .writer(fileWriter)
                .build();
    }

    // (3)
    @Bean
    public Flow flow1(Step stepChunkDb) {
        return new FlowBuilder<SimpleFlow>("flow1")
                .start(stepChunkDb)
                .build();
    }

    // (3)
    @Bean
    public Flow flow2(Step stepTaskletChunk) {
        return new FlowBuilder<SimpleFlow>("flow2")
                .start(stepTaskletChunk)
                .build();
    }

    // (3)
    @Bean
    public Flow flow3(Step stepTaskletSingle) {
        return new FlowBuilder<SimpleFlow>("flow3")
                .start(stepTaskletSingle)
                .build();
    }

    // (3)
    @Bean
    public Flow flow4(Step stepChunkDb) {
        return new FlowBuilder<SimpleFlow>("flow4")
                .start(stepChunkDb)
                .build();
    }

    // (2)
    @Bean
    public Flow splitFlow(
            @Qualifier("parallelTaskExecutor") TaskExecutor taskExecutor,
            Flow flow1, Flow flow2, Flow flow3, Flow flow4) {
        return new FlowBuilder<SimpleFlow>("parallelStepJob.split")
                .split(taskExecutor)
                .add(flow1, flow2, flow3, flow4)
                .build();
    }

    @Bean
    public Job parallelSummarizeJob(JobRepository jobRepository,
                                    Flow splitFlow) {
        return new JobBuilder("parallelStepJob", jobRepository)
                .start(splitFlow)
                .end()
                .build();
    }

XMLConfig

    <!-- Task Executor -->
    <!-- (1) -->
    <task:executor id="parallelTaskExecutor" pool-size="10" queue-capacity="200"/>

    <!-- Job Definition -->
    <!-- (2) -->
    <batch:job id="parallelStepJob" job-repository="jobRepository">
      <batch:split id="parallelStepJob.split" task-executor="parallelTaskExecutor">
          <batch:flow>  <!-- (3)  -->
              <batch:step id="parallelStepJob.step.chunk.db">
                   <!-- (4) -->
                  <batch:tasklet transaction-manager="jobTransactionManager">
                      <batch:chunk reader="fileReader" writer="databaseWriter"
                              commit-interval="100"/>
                  </batch:tasklet>
              </batch:step>
          </batch:flow>

          <batch:flow>  <!-- (3)  -->
              <batch:step id="parallelStepJob.step.tasklet.chunk">
                   <!-- (5) -->
                  <batch:tasklet transaction-manager="jobTransactionManager"
                                 ref="chunkTransactionTasklet"/>
              </batch:step>
          </batch:flow>

          <batch:flow>  <!-- (3)  -->
              <batch:step id="parallelStepJob.step.tasklet.single">
                   <!-- (6) -->
                  <batch:tasklet transaction-manager="jobTransactionManager"
                                 ref="singleTransactionTasklet"/>
              </batch:step>
          </batch:flow>

          <batch:flow>  <!-- (3)  -->
              <batch:step id="parallelStepJob.step.chunk.file">
                  <batch:tasklet transaction-manager="jobTransactionManager">
                       <!-- (7) -->
                      <batch:chunk reader="databaseReader" writer="fileWriter"
                              commit-interval="200"/>
                  </batch:tasklet>
              </batch:step>
          </batch:flow>

      </batch:split>
    </batch:job>

表 194. 説明
項番 | 説明
--- | ---
(1) | 並列処理のために、各スレッドに割り当てるためのスレッドプールを定義する。
(2) | `FlowBuilderのaddメソッドの引数flows/<batch:split>要素内に`並列実行するステップを`Bean定義したFlowクラス/<batch:flow>要素`を使用した形式で定義をする。 `splitメソッドの引数executor/task-executor属性`に(1)で定義したスレッドプールのBeanを設定する
(3) | `Bean定義したFlow/<batch:flow>`ごとに並列位処理したい`Bean定義したStep/<batch:step>`を定義する。
(4) | 概要図のStep1：チャンクモデルの中間コミット方式処理を定義する。
(5) | 概要図のStep2：タスクレットモデルの中間コミット方式処理を定義する。
(6) | 概要図のStep3：タスクレットモデルの一括コミット方式処理を定義する。
(7) | 概要図のStep4：チャンクモデルの非トランザクショナルなリソースに対する中間コミット方式処理を定義する。

| 並列処理したために処理性能が低下するケース 並列処理では多重処理同様にデータ範囲を変えて同じ処理を並列走行させることが可能である。この場合、データ範囲はパラメータなどで与える。 この際に、個々の処理ごとに対象となるデータ量が小さい場合、 稼働時に占有するリソース量や処理時間などのフットプリントが並列処理では不利に働き、 かえって処理性能が低下することがある。 フットプリントの例 入力リソースに対するオープンから最初のデータ範囲を取得するまでの処理 リソースオープンは、データ取得に比べて処理時間がかかる データ範囲のメモリ領域を初期化する処理も同様に時間がかかる

また、Parallel Stepの前後に共通処理のステップを定義することも可能である。

JavaConfig

    @Bean
    public Step stepPreprocess(JobRepository jobRepository,
                            StepExecutionLoggingListener listener,
                            DeleteDetailTasklet deleteDetailTasklet,
                            @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return new StepBuilder("parallelRegisterJob.step.preprocess", jobRepository)
                .tasklet(deleteDetailTasklet, transactionManager)
                .listener(listener)
                .build();
    }

    @Bean
    public Step stepPlan(JobRepository jobRepository,
                      StepExecutionLoggingListener listener,
                      @Qualifier("planReader") ItemReader<SalesPlanDetail> reader,
                      @Qualifier("planWriter") ItemWriter<SalesPlanDetail> writer,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return new StepBuilder("parallelRegisterJob.step.plan", jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(1,
                        transactionManager)
                .listener(listener)
                .reader(reader)
                .writer(writer)
                .build();
    }

    @Bean
    public Step stepPerformance(JobRepository jobRepository,
                      StepExecutionLoggingListener listener,
                      @Qualifier("performanceReader") ItemReader<SalesPerformanceDetail> reader,
                      @Qualifier("performanceWriter") ItemWriter<SalesPerformanceDetail> writer,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return new StepBuilder("parallelRegisterJob.step.performance",
                jobRepository)
                .<SalesPerformanceDetail, SalesPerformanceDetail> chunk(1,
                        transactionManager)
                .listener(listener)
                .reader(reader)
                .writer(writer)
                .build();
    }

    @Bean
    public Flow flow1(Step stepPlan) {
        return new FlowBuilder<SimpleFlow>("flow1")
                .start(stepPlan)
                .build();
    }

    @Bean
    public Flow flow2(Step stepPerformance) {
        return new FlowBuilder<SimpleFlow>("flow2")
                .start(stepPerformance)
                .build();
    }

    // (2)
    @Bean
    public Flow splitFlow(
            @Qualifier("parallelTaskExecutor") TaskExecutor taskExecutor,
            Flow flow1, Flow flow2) {
        return new FlowBuilder<SimpleFlow>("parallelRegisterJob.split")
                .split(taskExecutor) // (1)
                .add(flow1, flow2)
                .build();
    }

    @Bean
    public Job parallelRegisterJob(JobRepository jobRepository,
                                    Flow splitFlow,
                                    Step stepPreprocess) {
        return new JobBuilder("parallelRegisterJob", jobRepository)
                .start(stepPreprocess)
                .on("*")
                .to(splitFlow)
                .end()
                .build();
    }

XMLConfig

    <batch:job id="parallelRegisterJob" job-repository="jobRepository">
        <!-- (1) -->
        <batch:step id="parallelRegisterJob.step.preprocess"
                    next="parallelRegisterJob.split">
            <batch:tasklet transaction-manager="jobTransactionManager"
                           ref="deleteDetailTasklet" />
        </batch:step>

        <!--(2) -->
        <batch:split id="parallelRegisterJob.split" task-executor="parallelTaskExecutor">
            <batch:flow>
                <batch:step id="parallelRegisterJob.step.plan">
                    <batch:tasklet transaction-manager="jobTransactionManager">
                        <batch:chunk reader="planReader" writer="planWriter"
                                commit-interval="1000" />
                    </batch:tasklet>
                </batch:step>
            </batch:flow>
            <batch:flow>
                <batch:step id="parallelRegisterJob.step.performance">
                    <batch:tasklet transaction-manager="jobTransactionManager">
                        <batch:chunk reader="performanceReader" writer="performanceWriter"
                                commit-interval="1000" />
                    </batch:tasklet>
                </batch:step>
            </batch:flow>
        </batch:split>
    </batch:job>

表 195. 説明
項番 | 説明
--- | ---
(1) | 前処理として処理するステップを定義する。`FlowBuilderクラスのsplitメソッドの引数executor/next属性`に`Bean定義したTaskExecutorクラス/<batch:split>`に設定したidを指定する。 `addメソッド/next属性`による後続ステップの指定に関する詳細は"シーケンシャルフロー"を参照。
(2) | Parallel Stepを定義する。 `Bean定義したFlow/<batch:flow>`ごとに並列処理したい`Bean定義したStep/<batch:step>`を定義する。

##### 8.2.2.2. Partitioning Step (多重処理)

Partitioning Step(多重処理)の方法を説明する

図 55. Partitioning Stepの概要図
概要図の説明

Partitioning Stepでは、ManagerステップとWorkerステップの処理フェーズに分割される。

1. Managerステップでは、`Partitioner`により各Workerステップが処理するデータ範囲を特定するための`Parition Key`を生成する。 `Parition Key`はステップコンテキストに格納される。

2. Workerステップでは、ステップコンテキストから自身に割り当てられた`Parition Key`を取得し、それを使い処理対象データを特定する。 特定した処理対象データに対して定義したステップの処理を実行する。

Partitioning Stepでは処理データを分割必要があるが、分割数については可変数と固定数のどちらにも対応できる。

分割数

可変数の場合

  部門別で分割や、特定のディレクトリに存在するファイル単位での処理

固定数の場合

  全データを個定数で分割してデータを処理

Spring Batchでは、固定数のことを`grid-size`といい、`Partitioner`で`grid-size`になるようにデータ分割範囲を決定する。

Partitioning Stepでは、分割数をスレッドサイズより大きくすることができる。 この場合、スレッド数分で多重実行され、スレッドに空きが出るまで、処理が未実行のまま保留となるステップが発生する。

以下にPartitioning Stepのユースケースを示す。

表 196. Partitioning Stepのユースケース
ユースケース | Manager(Patitioner) | Worker | 分割数
--- | --- | --- | ---
マスタ情報からトランザクション情報を分割・多重化するケース 部門別や月別の集計処理など | DB(マスタ情報) | DB(トランザクション情報) | 可変
複数ファイルのリストから1ファイル単位に多重化するケース 各支店からの転送データを支店別に多重処理(支店別集計処理など) | 複数ファイル | 単一ファイル | 可変
大量データを一定数で分割・多重化するケース 障害発生時にリラン以外のリカバリ設計が難しくなるため、実運用では利用されることはあまりないケース。 リランする場合は、全件やり直しなので分割したメリットが薄れてしまう。 | `grid-size`とトランザクション情報件数からデータ範囲を特定 | DB(トランザクション情報) | 固定

###### 8.2.2.2.1. 分割数が可変の場合

Partitioning Stepで分割数を可変とする方法を説明する。
下記に処理イメージ図を示す。

図 56. 処理イメージ図

処理イメージを例とした実装方法を示す。

Repository(SQLMapper)の定義 (PostgreSQL)

    <!-- (1) -->
    <select id="findAll" resultType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.mst.Branch">
        <![CDATA[
        SELECT
            branch_id AS branchId,
            branch_name AS branchName,
            branch_address AS branchAddrss,
            branch_tel AS branchTel,
            create_date AS createDate,
            update_date AS updateDate
        FROM
            branch_mst
        ]]>
    </select>

    <!-- (2) -->
    <select id="summarizeInvoice"
            resultType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.performance.SalesPerformanceDetail">
        <![CDATA[
        SELECT
            branchId, year, month, customerId, SUM(amount) AS amount
        FROM (
            SELECT
                t2.charge_branch_id AS branchId,
                date_part('year', t1.invoice_date) AS year,
                date_part('month', t1.invoice_date) AS month,
                t1.customer_id AS customerId,
                t1.invoice_amount AS amount
            FROM invoice t1
            INNER JOIN customer_mst t2 ON t1.customer_id = t2.customer_id
            WHERE
                t2.charge_branch_id = #{branchId}
            ) t3
        GROUP BY branchId, year, month, customerId
        ORDER BY branchId ASC, year ASC, month ASC, customerId ASC
        ]]>
    </select>

    <!-- omitted -->

Partitionerの実装例

    @Component
    public class BranchPartitioner implements Partitioner {

        @Inject
        BranchRepository branchRepository; // (3)

        @Override
        public Map<String, ExecutionContext> partition(int gridSize) {

            Map<String, ExecutionContext> map = new HashMap<>();
            List<Branch> branches = branchRepository.findAll();

            int index = 0;
            for (Branch branch : branches) {
                ExecutionContext context = new ExecutionContext();
                context.putString("branchId", branch.getBranchId()); // (4)
                map.put("partition" + index, context);  // (5)
                index++;
            }

            return map;
        }
    }

JavaConfig

    // (6)
    @Bean
    public TaskExecutor parallelTaskExecutor(
            @Value("${thread.size}") int threadSize) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(threadSize);
        executor.setQueueCapacity(10);
        return executor;
    }

    // (7)
    @Bean
    @StepScope
    public MyBatisCursorItemReader<SalesPerformanceDetail> reader(
            @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
            @Value("#{stepExecutionContext['branchId']}") String branchId) { // (8)
        final Map<String, Object> parameterValues = new LinkedHashMap<>();
        parameterValues.put("branchId", branchId);
        return new MyBatisCursorItemReaderBuilder<SalesPerformanceDetail>()
                .sqlSessionFactory(jobSqlSessionFactory)
                .queryId(
                        "jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.performance.InvoiceRepository.summarizeInvoice")
                .parameterValues(parameterValues)
                .build();
    }

    // omitted

    // (12)
    @Bean
    public Step step1(JobRepository jobRepository,
                      StepExecutionLoggingListener listener,
                      @Qualifier("reader") ItemReader<SalesPerformanceDetail> reader,
                      @Qualifier("writer") ItemWriter<SalesPerformanceDetail> writer,
                      @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return new StepBuilder("multipleInvoiceSummarizeJob.worker",
                jobRepository)
                .<SalesPerformanceDetail, SalesPerformanceDetail> chunk(1,
                        transactionManager)
                .listener(listener)
                .reader(reader)
                .writer(writer)
                .build();
    }

    @Bean
    public PartitionHandler partitionHandler(
            @Qualifier("parallelTaskExecutor") TaskExecutor taskExecutor,
            @Value("${grid.size}") int gridSize, Step step1) {
        TaskExecutorPartitionHandler handler = new TaskExecutorPartitionHandler();
        handler.setTaskExecutor(taskExecutor);
        handler.setStep(step1);
        handler.setGridSize(gridSize); // (11)
        return handler;
    }

    // (9)
    @Bean
    public Step step1Manager(JobRepository jobRepository,
                             BranchPartitioner partitioner,
                             PartitionHandler partitionHandlerandler) {
        return new StepBuilder("multipleInvoiceSummarizeJob.manager",
                jobRepository)
                .partitioner("multipleInvoiceSummarizeJob.worker", partitioner) // (10)
                .partitionHandler(partitionHandlerandler)
                .build();
    }

    @Bean
    public Job multipleInvoiceSummarizeJob(JobRepository jobRepository,
                                           JobExecutionLoggingListener listener,
                                           Step step1Manager) {
        return new JobBuilder("multipleInvoiceSummarizeJob", jobRepository)
                .start(step1Manager)
                .listener(listener)
                .build();
    }

XMLConfig

    <!-- (6) -->
    <task:executor id="parallelTaskExecutor"
                   pool-size="${thread.size}" queue-capacity="10"/>

    <!-- (7) -->
    <bean id="reader" class="org.mybatis.spring.batch.MyBatisCursorItemReader" scope="step"
          p:queryId="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.repository.performance.InvoiceRepository.summarizeInvoice"
          p:sqlSessionFactory-ref="jobSqlSessionFactory">
        <property name="parameterValues">
            <map>
                <!-- (8) -->
                <entry key="branchId" value="#{stepExecutionContext['branchId']}" />
            </map>
        </property>
    </bean>

    <!-- omitted -->

    <batch:job id="multipleInvoiceSummarizeJob" job-repository="jobRepository">
        <!-- (9) -->
        <batch:step id="multipleInvoiceSummarizeJob.manager">
            <!-- (10) -->
            <batch:partition partitioner="branchPartitioner"
                             step="multipleInvoiceSummarizeJob.worker">
                <!-- (11) -->
                <batch:handler grid-size="0" task-executor="parallelTaskExecutor" />
            </batch:partition>
        </batch:step>
    </batch:job>

    <!-- (12) -->
    <batch:step id="multipleInvoiceSummarizeJob.worker">
        <batch:tasklet transaction-manager="jobTransactionManager">
            <batch:chunk reader="reader" writer="writer" commit-interval="10"/>
        </batch:tasklet>
    </batch:step>

表 197. 説明
項番 | 説明
--- | ---
(1) | マスタデータから処理対象を取得するSQLを定義する。
(2) | マスタデータからの取得値を検索条件とするSQLを定義する。
(3) | 定義したRepository(SQLMapper)をInjectする。
(4) | 1つのWorkerステップが処理するマスタ値をステップコンテキストに格納する。
(5) | 各Workerが該当するコンテキストを取得できるようMapに格納する。
(6) | 多重処理でWorkerステップの各スレッドに割り当てるためのスレッドプールを定義する。 Managerステップはメインスレッドで処理される。
(7) | マスタ値によるデータ取得のItemReaderを定義する。
(8) | (4)で設定したマスタ値をステップコンテキストから取得し、検索条件に追加する。
(9) | Managerステップを定義する。
(10) | データの分割条件を生成する処理を定義する。 `StepBuilderのpartitionerメソッドの引数partitioner/partitioner属性`には、`Partitioner`インタフェース実装を設定する。 `partitionerメソッドの引数stepName/step属性`には、(12)で定義するWorkerステップのBeanIDを設定する。
(11) | `partitioner`では`grid-size`を使用しないため、`TaskExecutorPartitionHandlerのsetGridSizeメソッドの引数gridSize/grid-size属性`には任意の値を設定する。 `setTaskExecutorメソッドの引数taskExecutor/task-executor`属性に(6)で定義したスレッドプールのBeanIDを設定する。
(12) | Workerステップを定義する。 `StepBuilderのreaderメソッド/reader属性`に(7)で定義したItemReaderを設定する。

複数ファイルのリストから1ファイル単位に多重化する場合は、Spring Batchが提供している以下の`Partitioner`を利用することができる。

- `org.springframework.batch.core.partition.support.MultiResourcePartitioner`

`MultiResourcePartitioner`の利用例を以下に示す。

JavaConfig

    // (1)
    @Bean
    public TaskExecutor parallelTaskExecutor(
            @Value("${thread.size}") int threadSize) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(threadSize);
        executor.setQueueCapacity(200);
        return executor;
    }

    // (2)
    @Bean
    @StepScope
    public FlatFileItemReader<SalesPlanDetail> reader(
            @Value("#{stepExecutionContext['fileName']}") File fileName) {
        final DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
        tokenizer.setNames("branchId", "year", "month", "customerId", "amount");
        final BeanWrapperFieldSetMapper<SalesPlanDetail> fieldSetMapper = new BeanWrapperFieldSetMapper<>();
        fieldSetMapper.setTargetType(SalesPlanDetail.class);
        final DefaultLineMapper<SalesPlanDetail> lineMapper = new DefaultLineMapper<>();
        lineMapper.setLineTokenizer(tokenizer);
        lineMapper.setFieldSetMapper(fieldSetMapper);
        return new FlatFileItemReaderBuilder<SalesPlanDetail>()
                .name(ClassUtils.getShortName(FlatFileItemReader.class))
                .resource(new FileSystemResource(fileName)) // (3)
                .lineMapper(lineMapper)
                .build();
    }

    // (4)
    @Bean
    @StepScope
    public MultiResourcePartitioner partitioner(
            @Value("#{jobParameters['inputdir']}") File inputdir) throws Exception { // (5)
        MultiResourcePartitioner partitioner = new MultiResourcePartitioner();
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources(
                new FileSystemResource(inputdir).getURL() + File.separator + "salesPlanDetail_*.csv");
        partitioner.setResources(resources);
        return partitioner;
    }

    // (7)
    @Bean
    public Step stepWorker(JobRepository jobRepository,
                            ItemReader<SalesPlanDetail> reader,
                            LoggingItemWriter writer,
                            @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return new StepBuilder("jobEvaluationReport.worker", jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(20,
                        transactionManager)
                .reader(reader)
                .writer(writer)
                .build();
    }

    @Bean
    public PartitionHandler partitionHandler(
            TaskExecutor parallelTaskExecutor,
            Step stepWorker) {
        TaskExecutorPartitionHandler handler = new TaskExecutorPartitionHandler();
        handler.setTaskExecutor(parallelTaskExecutor);
        handler.setStep(stepWorker);
        handler.setGridSize(0);
        return handler;
    }

    // (6)
    @Bean
    public Step stepManager(JobRepository jobRepository,
                            MultiResourcePartitioner partitioner,
                            PartitionHandler partitionHandler) {
        return new StepBuilder("multiplePartitioninglStepFileJob.manager", jobRepository)
                .partitioner("multiplePartitioninglStepFileJob.worker", partitioner)
                .partitionHandler(partitionHandler)
                .build();
    }

    @Bean
    public Job multiplePartitioninglStepFileJob(JobRepository jobRepository,
                                           Step stepManager) {
        return new JobBuilder("multiplePartitioninglStepFileJob", jobRepository)
                .start(stepManager)
                .build();
    }

XMLConfig

    <!-- (1) -->
    <task:executor id="parallelTaskExecutor" pool-size="10" queue-capacity="200"/>

    <!-- (2) -->
    <bean id="reader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="#{stepExecutionContext['fileName']}"> <!-- (3) -->
        <property name="lineMapper">
            <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
                <property name="lineTokenizer">
                    <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
                          p:names="branchId,year,month,customerId,amount"/>
                </property>
                <property name="fieldSetMapper">
                    <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
                          p:targetType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.plan.SalesPlanDetail"/>
                </property>
            </bean>
        </property>
    </bean>

    <!-- (4) -->
    <bean id="partitioner"
          class="org.springframework.batch.core.partition.support.MultiResourcePartitioner"
          scope="step"
          p:resources="file:#{jobParameters['inputdir']}/salesPlanDetail_*.csv"/> <!-- (5) -->

    <!--(6) -->
    <batch:job id="multiplePartitioninglStepFileJob" job-repository="jobRepository">
        <batch:step id="multiplePartitioninglStepFileJob.step.manager">
            <batch:partition partitioner="partitioner"
                             step="multiplePartitioninglStepFileJob.step.worker">
                <batch:handler grid-size="0" task-executor="parallelTaskExecutor"/>
            </batch:partition>
        </batch:step>
    </batch:job>

    <!-- (7) -->
    <batch:step id="multiplePartitioninglStepFileJob.step.worker">
        <batch:tasklet transaction-manager="jobTransactionManager">
            <batch:chunk reader="reader" writer="writer" commit-interval="20"/>
        </batch:tasklet>
    </batch:step>

表 198. 説明
項番 | 説明
--- | ---
(1) | 多重処理でWorkerステップの各スレッドに割り当てるためのスレッドプールを定義する。 Managerステップはメインスレッドで処理される。
(2) | 1つのファイルを読み込むためのItemReaderを定義する。
(3) | `FlatFileItemReaderBuilderのresourceメソッドの引数resource/resouce属性`に、`MultiResourcePartitioner`で分割されたファイルを入力ファイルに指定する。 `MultiResourcePartitioner`は、"fileName"というキーでステップコンテキストにファイルパスを格納している。
(4) | `MultiResourcePartitioner`を`partitioner`として定義する。
(5) | `*`を用いたパターンを使用することで、複数ファイルを対象にすることができる。
(6) | Managerステップを定義する。 定義内容は上記で説明したPartitioning Stepの内容と同じ。
(7) | Workerステップを定義する。 `StepBuilderのreaderメソッド/reader属性`に(2)で定義したItemReaderを設定する。

###### 8.2.2.2.2. 分割数が固定の場合

Partitioning Stepで分割数を固定する方法を説明する。
下記に処理イメージ図を示す。

図 57. 処理イメージ図

処理イメージを例とした実装方法を示す。

Repository(SQLMapper)の定義 (PostgreSQL)

    <!-- (1) -->
    <select id="findByYearAndMonth"
        resultType="jp.co.ntt.fw.macchinetta.batch.functionaltest.app.model.performance.SalesPerformanceSummary">
        <![CDATA[
        SELECT
            branch_id AS branchId, year, month, amount
        FROM
            sales_performance_summary
        WHERE
            year = #{year} AND month = #{month}
        ORDER BY
            branch_id ASC
        LIMIT
            #{dataSize}
        OFFSET
            #{offset}
        ]]>
    </select>

    <!-- (2) -->
    <select id="countByYearAndMonth" resultType="_int">
        <![CDATA[
        SELECT
            count(*)
        FROM
            sales_performance_summary
        WHERE
            year = #{year} AND month = #{month}
        ]]>
    </select>

    <!-- omitted -->

Partitionerの実装例

    @Component
    public class SalesDataPartitioner implements Partitioner {

        @Inject
        SalesSummaryRepository repository;  // (3)

        // omitted.

        @Override
        public Map<String, ExecutionContext> partition(int gridSize) {

            Map<String, ExecutionContext> map = new HashMap<>();
            int count = repository.countByYearAndMonth(year, month);
            int dataSize = (count / gridSize) + 1;        // (4)
            int offset = 0;

            for (int i = 0; i < gridSize; i++) {
                ExecutionContext context = new ExecutionContext();
                context.putInt("dataSize", dataSize);     // (5)
                context.putInt("offset", offset);         // (6)
                offset += dataSize;
                map.put("partition:" + i, context);       // (7)
            }

            return map;
        }
    }

JavaConfig

    // (8)
    @Bean
    public TaskExecutor parallelTaskExecutor(
            @Value("${thread.size}") int threadSize) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(threadSize);
        executor.setQueueCapacity(10);
        return executor;
    }

    // (9)
    @Bean
    @StepScope
    public MyBatisCursorItemReader<SalesPerformanceSummary> reader(
            @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
            @Value("#{jobParameters['year']}") Integer year,
            @Value("#{jobParameters['month']}") Integer month,
            @Value("#{stepExecutionContext['dataSize']}") Integer dataSize, // (10)
            @Value("#{stepExecutionContext['offset']}") Integer offset) { // (11)
        Map<String, Object> parameterValues = new HashMap<>();
        parameterValues.put("year", year);
        parameterValues.put("month", month);
        parameterValues.put("dataSize", dataSize);
        parameterValues.put("offset", offset);
        return new MyBatisCursorItemReaderBuilder<SalesPerformanceSummary>()
                .sqlSessionFactory(jobSqlSessionFactory)
                .queryId(
                        "jp.co.ntt.fw.macchinetta.batch.functionaltest.ch08.parallelandmultiple.repository.SalesSummaryRepository.findByYearAndMonth")
                .parameterValues(parameterValues)
                .build();
    }

    // omitted

    // (15)
    @Bean
    public Step stepWorker(JobRepository jobRepository,
                            StepExecutionLoggingListener listener,
                            ItemReader<SalesPerformanceSummary> reader,
                            ItemWriter<SalesPlanSummary> writer,
                            AddProfitsItemProcessor addProfitsItemProcessor,
                            @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager) {
        return new StepBuilder("multipleCreateSalesPlanSummaryJob.worker", jobRepository)
                .<SalesPerformanceSummary, SalesPlanSummary> chunk(10,
                        transactionManager)
                .listener(listener)
                .reader(reader)
                .processor(addProfitsItemProcessor)
                .writer(writer)
                .build();
    }

    @Bean
    public PartitionHandler partitionHandler(
            TaskExecutor parallelTaskExecutor,
            @Value("${grid.size}") int gridSize,
            Step stepWorker) {
        TaskExecutorPartitionHandler handler = new TaskExecutorPartitionHandler();
        handler.setTaskExecutor(parallelTaskExecutor); // (14)
        handler.setStep(stepWorker);
        handler.setGridSize(gridSize); // (14)
        return handler;
    }

    // (12)
    @Bean
    public Step stepManager(JobRepository jobRepository,
                            SalesDataPartitioner salesDataPartitioner,
                            PartitionHandler partitionHandler) {
        return new StepBuilder("multipleCreateSalesPlanSummaryJob.manager", jobRepository)
                .partitioner("multipleCreateSalesPlanSummaryJob.worker", salesDataPartitioner) // (13)
                .partitionHandler(partitionHandler)
                .build();
    }

    @Bean
    public Job multipleCreateSalesPlanSummaryJob(JobRepository jobRepository,
                                           JobExecutionLoggingListener listener,
                                           Step stepManager) {
        return new JobBuilder("multipleCreateSalesPlanSummaryJob", jobRepository)
                .start(stepManager)
                .listener(listener)
                .build();
    }

XMLConfig

    <!-- (8) -->
    <task:executor id="parallelTaskExecutor"
                   pool-size="${thread.size}" queue-capacity="10"/>

    <!-- (9) -->
    <bean id="reader"
          class="org.mybatis.spring.batch.MyBatisCursorItemReader" scope="step"
          p:queryId="jp.co.ntt.fw.macchinetta.batch.functionaltest.ch08.parallelandmultiple.repository.SalesSummaryRepository.findByYearAndMonth"
          p:sqlSessionFactory-ref="jobSqlSessionFactory">
        <property name="parameterValues">
            <map>
                <entry key="year" value="#{jobParameters['year']}" value-type="java.lang.Integer"/>
                <entry key="month" value="#{jobParameters['month']}" value-type="java.lang.Integer"/>
                <!-- (10) -->
                <entry key="dataSize" value="#{stepExecutionContext['dataSize']}" />
                <!-- (11) -->
                <entry key="offset" value="#{stepExecutionContext['offset']}" />
            </map>
        </property>
    </bean>

    <!-- omitted -->

    <batch:job id="multipleCreateSalesPlanSummaryJob" job-repository="jobRepository">
        <!-- (12) -->
        <batch:step id="multipleCreateSalesPlanSummaryJob.manager">
            <!-- (13) -->
            <batch:partition partitioner="salesDataPartitioner"
                  step="multipleCreateSalesPlanSummaryJob.worker">
                <!-- (14) -->
                <batch:handler grid-size="4" task-executor="parallelTaskExecutor" />
            </batch:partition>
        </batch:step>
    </batch:job>

    <!-- (15) -->
    <batch:step id="multipleCreateSalesPlanSummaryJob.worker">
        <batch:tasklet transaction-manager="jobTransactionManager">
            <batch:chunk reader="reader" processor="addProfitsItemProcessor"
                  writer="writer" commit-interval="10"/>
        </batch:tasklet>
    </batch:step>

表 199. 説明
項番 | 説明
--- | ---
(1) | 特定のデータ範囲を取得するためにページネーション検索(SQL絞り込み方式)を定義する。 ページネーション検索(SQL絞り込み方式)の詳細は、Macchinetta Server 1.x 開発ガイドラインの Entityのページネーション検索(SQL絞り込み方式)を参照。
(2) | 処理対象の全件数を取得するSQLを定義する。
(3) | 定義したRepository(SQLMapper)をInjectする。
(4) | 1つのWorkerステップが処理するデータ件数を算出する。
(5) | (4)のデータ件数をステップコンテキストに格納する。
(6) | 各Workerステップの検索開始位置をステップコンテキストに格納する。
(7) | 各Workerが該当するコンテキストを取得できるようMapに格納する。
(8) | 多重処理でWorkerステップの各スレッドに割り当てるためのスレッドプールを定義する。 Managerステップはメインスレッドで処理される。
(9) | ページネーション検索(SQL絞り込み方式)によるデータ取得のItemReaderを定義する。
(10) | (5)で設定したデータ件数をステップコンテキストから取得し、検索条件に追加する。
(11) | (6)で設定した検索開始位置をステップコンテキストから取得し、検索条件に追加する。
(12) | Managerステップを定義する。
(13) | データの分割条件を生成する処理を定義する。 `StepBuilderのpartitionerメソッドの引数partitioner/partitioner属性`には、`Partitioner`インタフェース実装を設定する。 `partitionerメソッドの引数stepName/step属性`には、(15)で定義するWorkerステップのBeanIDを設定する。
(14) | `TaskExecutorPartitionHandlerのsetGridSizeメソッドの引数gridSize/grid-size属性`に分割数(固定値)を設定する。 `setTaskExecutorメソッドの引数taskExecutor/task-executor属性`に(8)で定義したスレッドプールのBeanIDを設定する。
(15) | Workerステップを定義する。 `StepBuilderのreaderメソッド/reader属性`に(9)で定義したItemReaderを設定する。

## 9. チュートリアル

### 9.1. はじめに

#### 9.1.1. チュートリアルの目的

本チュートリアルは、Macchinetta Batch 2.x 開発ガイドラインに記載されている内容について、実際にアプリケーションの開発を体験することで、 Macchinetta Batch 2.xの基本的な知識を習得することを目的としている。

#### 9.1.2. 対象読者

本チュートリアルはソフトウェア開発経験のあるアーキテクトやプログラマ向けに書かれており、 以下の知識があることを前提としている。

- Spring FrameworkのDIやAOPに関する基礎的な知識がある

- SQLに関する基礎的な知識がある

- Javaを主体としたアプリケーションの開発経験がある

#### 9.1.3. 検証環境

本チュートリアルの検証を行った環境条件を以下に示す。

表 200. 環境条件
ソフトウェア分類 | 製品名
--- | ---
OS | Microsoft Windows 11 Enterprise (64bit)
JDK | RedHat OpenJDK 21.0.9
IDE | Spring Tool Suite 5.0.0 released
Build Tool | Apache Maven 3.9.9
RDBMS | H2 Database 2.2.232

#### 9.1.4. フレームワークの概要

ここでは、フレームワークの概要として処理モデルの概要およびアーキテクチャの違いについて説明する。
Spring Batchについて、詳細はMacchinetta Batch 2.x 開発ガイドラインのSpring Batchのアーキテクチャを参照。

Macchinetta Batch 2.xが提供する処理モデルには、チャンクモデルとタスクレットモデルがある。
それぞれの特徴について以下に示す。

チャンクモデル

  一定件数のデータごとにまとめて入力／加工／出力する方式。このデータのまとまりをチャンクと呼ぶ。 データの入力／加工／出力といった処理の流れを定型化し、一部を実装するだけでジョブが実装できる。 大量データを効率よく処理する場合に用いる。
  詳細はチャンクモデルを参照。

タスクレットモデル

  自由に処理を記述する方式。SQLを1回発行するだけ、コマンドを発行するだけ、といった簡素なケースや 複数のデータベースやファイルにアクセスしながら処理するような複雑で定型化しにくい場合に用いる。
  詳細はタスクレットモデルを参照。

処理モデルについて、構成要素や機能的な差異を下表に示す。

表 201. 処理モデルの対比表
項目 | チャンクモデル | タスクレットモデル
--- | --- | ---
構成要素 | ItemReader、ItemProcessor、ItemWriter、ChunkOrientedTaskletで構成される。 | Taskletのみで構成される。
トランザクション制御 | チャンク単位にトランザクションが発生する。トランザクション制御は一定件数ごとにトランザクションを確定する中間コミット方式のみ。 | 1トランザクションで処理する。トランザクション制御は、全件を1トランザクションで確定する一括コミット方式と中間コミット方式のいずれかを利用可能。 前者はSpring Batchが持つトランザクション制御の仕組みを利用するが、後者はユーザにてトランザクションを直接操作する。
処理の再実行 | リランおよび、ステートレスリスタート(件数ベースリスタート)、ステートフルリスタート(処理状態を判断したリスタート)が利用できる。 | リランのみ利用することを原則とする。処理状態を判断したリスタートが利用できる。
例外ハンドリング | Spring Batch提供の各種`Listener`インタフェースを使うことでハンドリング処理が容易になっている。try-catchによる独自実装も可能。 | タスクレット実装内にて独自にtry-catchを実装することが基本。`ChunkListener`インタフェースの利用も可能。

本チュートリアルでは、基本的な機能を利用したアプリケーションについてチャンクモデル、タスクレットモデルそれぞれの実装方法を説明している。 チャンクモデル、タスクレットモデルのアーキテクチャの違いによって実装方法も異なってくるため、 ここでそれぞれの特徴をしっかり理解してから進めることを推奨する。

#### 9.1.5. チュートリアルの進め方

本チュートリアルで作成するアプリケーション(ジョブ)においては、 作成済みのジョブに実装を追加して作成するジョブがあるため、作成する順序を考慮しなければならない。

本チュートリアルの読み進め方を作成するジョブの順序関係も含めて図を以下に示す。

図 58. チュートリアルの進め方
非同期実行方式のジョブの実施タイミング

非同期実行方式のジョブは、本チュートリアルの進め方の順序では最後のジョブとしているが、 チャンクモデルまたはタスクレットモデルで1つでもジョブを作成済みであれば、非同期実行方式のジョブを実施してもよい。

| ファイルアクセスでデータ入出力を行うジョブへの追加実装について ファイルアクセスでデータ入出力を行うジョブの説明以外は、 データベースアクセスでデータ入出力を行うジョブをもとに実装を追加したり、実行例を表示させている。 ファイルアクセスでデータ入出力を行うジョブをもとに実装を追加する場合は、読み替える必要があるため留意すること。

### 9.2. 作成するアプリケーションの説明

#### 9.2.1. 背景

とある量販店では会員に対してポイントカードを発行している。
会員には「ゴールド会員」「一般会員」の会員種別が存在し、会員種別に応じたサービスを提供している。
今回そのサービスの一環として、月内に商品を購入した会員のうち、 会員種別が「ゴールド会員」の場合は100ポイント、「一般会員」の場合は10ポイントを月末に加算することにした。

#### 9.2.2. 処理概要

会員種別に応じてポイント加算を行うアプリケーションを月次バッチ処理としてMacchinetta Batch 2.xを使用して実装する。

#### 9.2.3. 業務仕様

業務仕様を以下に示す。

- 「月内に商品を購入した会員」は商品購入フラグで示す

  - 商品購入フラグは、"0"の場合に初期状態、"1"の場合に処理対象を表す

- 商品購入フラグが"1"(処理対象)の場合に、会員種別に応じてポイントを加算する

  - 会員種別が"G"(ゴールド会員)の場合は100ポイント、"N"(一般会員)の場合は10ポイントを加算する

- 商品購入フラグはポイント加算後に、"0"(初期状態)に更新する

- ポイントの上限値は1,000,000ポイントとする

- ポイント加算後に1,000,000ポイントを超えた場合は、1,000,000ポイントに補正する

#### 9.2.4. 学習コンテンツ

簡単な業務仕様のアプリケーション(ジョブ)の作成を通して、ジョブに関する様々な機能や処理方式を学習する。
なお、ジョブはタスクレットモデルとチャンクモデルをそれぞれ実装する。
各ジョブで主に学習することとそのジョブで利用する機能や処理方式を以下に示す。

表 202. チュートリアルで作成するジョブ
項番 | ジョブ | 略称 | 学習内容
--- | --- | --- | ---
1 | データベースアクセスでデータ入出力を行うジョブ | DB | MyBatis用のItemReaderおよびItemWriterを利用したデータベースアクセスの手法を学ぶ。
2 | ファイルアクセスでデータ入出力を行うジョブ | FL | フラットファイルの入出力用のItemReaderおよびItemWriterを利用したファイルアクセスの手法を学ぶ。
3 | 入力データの妥当性検証を行うジョブ | VL | Bean Validationを利用した入力チェックの手法を学ぶ。
4 | ChunkListenerで例外ハンドリングを行うジョブ | CL | リスナーとしてChunkListenerを利用した例外ハンドリングの手法を学ぶ。
5 | try-catchで例外ハンドリングを行うジョブ | TC | try-catchを利用した例外ハンドリングとスキップ、およびカスタマイズした終了コードを出力する手法を学ぶ。
6 | 非同期実行方式のジョブ | AS | Macchinetta Batch 2.xが提供するDBポーリング機能を利用した非同期実行の手法を学ぶ。

チュートリアルで作成するジョブで利用している機能や処理方式とMacchinetta Batch 2.x 開発ガイドラインの説明の対応表を以下に示す。

なお、以下の対応表ではチャンクモデルをC、タスクレットモデルをTとしている。

表 203. チュートリアルで作成するジョブとMacchinetta Batch 2.x 開発ガイドラインの説明の対応表
項番 | 機能 | DB | FL | VL | CL | TC | AS
--- | --- | --- | --- | --- | --- | --- | ---
1 | ジョブの起動 > 起動方式 > 同期実行 | C/T | C/T | C/T | C/T | C/T | -
2 | ジョブの起動 > 起動方式 > 非同期実行(DBポーリング) | - | - | - | - | - | C/T
3 | ジョブの起動 > ジョブの起動パラメータ > コマンドライン引数から与える | - | C/T | - | - | - | -
4 | ジョブの起動 > リスナー | - | - | - | C/T | C/T | -
5 | データの入出力 > トランザクション制御 > Spring Batchにおけるトランザクション制御 | C/T | - | - | - | - | -
6 | データの入出力 > トランザクション制御 > 単一データソースの場合 > トランザクション制御の実施 | C/T | - | - | - | - | -
7 | データの入出力 > データベースアクセス > 入力 | C/T | - | - | - | - | -
8 | データの入出力 > データベースアクセス > 出力 | C/T | - | - | - | - | -
9 | データの入出力 > ファイルアクセス > 可変長レコード > 入力 | - | C/T | - | - | - |
10 | データの入出力 > ファイルアクセス > 可変長レコード > 出力 | - | C/T | - | - | - | -
11 | 異常系への対応 > 入力チェック | - | - | C/T | - | C/T | -
12 | 異常系への対応 > 例外ハンドリング > ステップ単位の例外ハンドリング > `ChunkListener`インタフェースによる例外ハンドリング | - | - | - | C/T | - | -
13 | 異常系への対応 > 例外ハンドリング > ステップ単位の例外ハンドリング > チャンクモデルにおける例外ハンドリング | - | - | - | - | C | -
14 | 異常系への対応 > 例外ハンドリング > ステップ単位の例外ハンドリング > タスクレットモデルにおける例外ハンドリング | - | - | - | - | T | -
15 | 異常系への対応 > 例外ハンドリング > 処理継続可否の決定 > スキップ | - | - | - | - | C/T | -
16 | ジョブの管理 > ジョブの状態管理 > ジョブの状態・実行結果の確認 | - | - | - | - | - | C/T
17 | ジョブの管理 > 終了コードのカスタマイズ | - | - | - | - | C/T | -
18 | ジョブの管理 > ロギング | - | - | - | C/T | C/T | -
19 | ジョブの管理 > メッセージ管理 | - | - | - | C/T | C/T | -

### 9.3. 環境構築

#### 9.3.1. プロジェクトの作成

まず、`Maven Archetype Plugin`の`mvn archetype:generate`を利用して、プロジェクトを作成する。
ここでは、Windowsのコマンドプロンプトを使用してプロジェクトを作成する手順となっている。

`mvn archetype:generate`を利用してプロジェクトを作成する方法の詳細については、プロジェクトの作成を参照。

| プロキシサーバの経由について インターネット接続するためにプロキシサーバを経由する必要がある場合、STSのProxy設定と MavenのProxy設定をする。

プロジェクトを作成するディレクトリにて、以下のコマンドを実行する。

JavaConfig
コマンドプロンプト(Windows)

    C:\xxx>mvn archetype:generate ^
      -DarchetypeGroupId=com.github.macchinetta.blank ^
      -DarchetypeArtifactId=macchinetta-batch-archetype ^
      -DarchetypeVersion=2.6.1.RELEASE

XMLConfig
コマンドプロンプト(Windows)

    C:\xxx>mvn archetype:generate ^
      -DarchetypeGroupId=com.github.macchinetta.blank ^
      -DarchetypeArtifactId=macchinetta-batch-xmlconfig-archetype ^
      -DarchetypeVersion=2.6.1.RELEASE

その後、以下を対話式に設定する。

表 204. プロジェクト作成時に設定する値
項目名 | 設定例
--- | ---
groupId | com.example.batch
artifactId | macchinetta-batch-tutorial
version | 1.0.0-SNAPSHOT
package | com.example.batch.tutorial

以下のとおり、mvnコマンドに対して「BUILD SUCCESS」が表示されることを確認する。

JavaConfig
実行例

    C:\xxx>mvn archetype:generate ^
    More?   -DarchetypeGroupId=com.github.macchinetta.blank ^
    More?   -DarchetypeArtifactId=macchinetta-batch-archetype ^
    More?   -DarchetypeVersion=2.6.1.RELEASE
    [INFO] Scanning for projects…​
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building Maven Stub Project (No POM) 1
    [INFO] ------------------------------------------------------------------------

    (.. omitted)

    Define value for property 'groupId': com.example.batch
    Define value for property 'artifactId': macchinetta-batch-tutorial
    Define value for property 'version' 1.0-SNAPSHOT: : 1.0.0-SNAPSHOT
    Define value for property 'package' com.example.batch: : com.example.batch.tutorial
    Confirm properties configuration:
    groupId: com.example.batch
    artifactId: macchinetta-batch-tutorial
    version: 1.0.0-SNAPSHOT
    package: com.example.batch.tutorial
     Y: : y
    [INFO] ----------------------------------------------------------------------------
    [INFO] Using following parameters for creating project from Archetype: macchinetta-batch-archetype:2.6.1.RELEASE
    [INFO] ----------------------------------------------------------------------------
    [INFO] Parameter: groupId, Value: com.example.batch
    [INFO] Parameter: artifactId, Value: macchinetta-batch-tutorial
    [INFO] Parameter: version, Value: 1.0.0-SNAPSHOT
    [INFO] Parameter: package, Value: org.macchinetta.batch.tutorial
    [INFO] Parameter: packageInPathFormat, Value: com/example/batch/tutorial
    [INFO] Parameter: package, Value: org.macchinetta.batch.tutorial
    [INFO] Parameter: groupId, Value: org.macchinetta.batch
    [INFO] Parameter: artifactId, Value: macchinetta-batch-tutorial
    [INFO] Parameter: version, Value: 1.0.0-SNAPSHOT
    [INFO] Project created from Archetype in dir: C:\xxx\macchinetta-batch-tutorial
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 01:20 min
    [INFO] Finished at: 2020-03-26T16:33:34+09:00
    [INFO] Final Memory: 15M/57M
    [INFO] ------------------------------------------------------------------------

XMLConfig
実行例

    C:\xxx>mvn archetype:generate ^
    More?   -DarchetypeGroupId=com.github.macchinetta.blank ^
    More?   -DarchetypeArtifactId=macchinetta-batch-xmlconfig-archetype ^
    More?   -DarchetypeVersion=2.6.1.RELEASE
    [INFO] Scanning for projects…​
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building Maven Stub Project (No POM) 1
    [INFO] ------------------------------------------------------------------------

    (.. omitted)

    Define value for property 'groupId': com.example.batch
    Define value for property 'artifactId': macchinetta-batch-tutorial
    Define value for property 'version' 1.0-SNAPSHOT: : 1.0.0-SNAPSHOT
    Define value for property 'package' com.example.batch: : com.example.batch.tutorial
    Confirm properties configuration:
    groupId: com.example.batch
    artifactId: macchinetta-batch-tutorial
    version: 1.0.0-SNAPSHOT
    package: com.example.batch.tutorial
     Y: : y
    [INFO] ----------------------------------------------------------------------------
    [INFO] Using following parameters for creating project from Archetype: macchinetta-batch-archetype:2.6.1.RELEASE
    [INFO] ----------------------------------------------------------------------------
    [INFO] Parameter: groupId, Value: com.example.batch
    [INFO] Parameter: artifactId, Value: macchinetta-batch-tutorial
    [INFO] Parameter: version, Value: 1.0.0-SNAPSHOT
    [INFO] Parameter: package, Value: com.example.batch.tutorial
    [INFO] Parameter: packageInPathFormat, Value: com/example/batch/tutorial
    [INFO] Parameter: package, Value: com.example.batch.tutorial
    [INFO] Parameter: groupId, Value: com.example.batch
    [INFO] Parameter: artifactId, Value: macchinetta-batch-tutorial
    [INFO] Parameter: version, Value: 1.0.0-SNAPSHOT
    [INFO] Project created from Archetype in dir: C:\xxx\macchinetta-batch-tutorial
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 45.293 s
    [INFO] Finished at: 2020-03-04T16:48:55+09:00
    [INFO] Final Memory: 16M/197M
    [INFO] ------------------------------------------------------------------------

サンプルジョブを実行し、プロジェクトが正しく作成できたことを確認する。

JavaConfig

    C:\xxx>cd macchinetta-batch-tutorial
    C:\xxx>mvn clean dependency:copy-dependencies -DoutputDirectory=lib package
    C:\xxx>java -cp "lib/*;target/*" ^
    org.springframework.batch.core.launch.support.CommandLineJobRunner ^
    com.example.batch.tutorial.jobs.Job01Config job01

以下のとおり、mvnコマンドに対して「BUILD SUCCESS」、javaコマンドに対して「COMPLETED」が表示されることを確認する。

出力例

    C:\xxx>cd macchinetta-batch-tutorial

    C:\xxx\macchinetta-batch-tutorial>mvn clean dependency:copy-dependencies -Doutput
    Directory=lib package
    [INFO] Scanning for projects...
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building TERASOLUNA Batch Framework for Java (5.x) Blank Project 1.0.0-SN
    APSHOT
    [INFO] ------------------------------------------------------------------------

    (.. omitted)

    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 30.139 s
    [INFO] Finished at: 2020-03-26T16:41:55+09:00
    [INFO] Final Memory: 20M/74M
    [INFO] ------------------------------------------------------------------------

    C:\xxx\macchinetta-batch-tutorial>java -cp "lib/*;target/*" ^
    More? org.springframework.batch.core.launch.support.CommandLineJobRunner ^
    More? com.example.batch.tutorial.jobs.Job01Config job01
    [2020/03/26 16:42:29] [main] [o.h.v.i.util.Version  ] [INFO ] HV000001: Hibernate Validator 8.0.1.Final
    [2020/03/04 16:58:50] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [SimpleJob: [name=job01]] launched with the following parameters: [{'jsr_batch_run_id':'{value=1, type=class java.lang.Long, identifying=true}'}]
    [2020/03/04 16:58:50] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [job01.step01]
    [2020/03/04 16:58:50] [main] [o.s.b.c.s.AbstractStep] [INFO ] Step: [job01.step01] executed in 30ms
    [2020/03/04 16:58:50] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [SimpleJob: [name=job01]] completed with the following parameters: [{'jsr_batch_run_id':'{value=1, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 55ms

XMLConfig

    C:\xxx>cd macchinetta-batch-tutorial
    C:\xxx>mvn clean dependency:copy-dependencies -DoutputDirectory=lib package
    C:\xxx>java -cp "lib/*;target/*" ^
    org.springframework.batch.core.launch.support.CommandLineJobRunner ^
    META-INF/jobs/job01.xml job01

以下のとおり、mvnコマンドに対して「BUILD SUCCESS」、javaコマンドに対して「COMPLETED」が表示されることを確認する。

出力例

    C:\xxx>cd macchinetta-batch-tutorial

    C:\xxx\macchinetta-batch-tutorial>mvn clean dependency:copy-dependencies -Doutput
    Directory=lib package
    [INFO] Scanning for projects...
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building Maccinetta Batch Framework for Java (2.x) Blank Project 1.0.0-SN
    APSHOT
    [INFO] ------------------------------------------------------------------------

    (.. omitted)

    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 9.462 s
    [INFO] Finished at: 2020-03-04T16:53:06+09:00
    [INFO] Final Memory: 26M/211M
    [INFO] ------------------------------------------------------------------------

    C:\xxx\macchinetta-batch-tutorial>java -cp "lib/*;target/*" ^
    More? org.springframework.batch.core.launch.support.CommandLineJobRunner ^
    More? META-INF/jobs/job01.xml job01
    [2020/03/04 16:58:46] [main] [o.h.v.i.util.Version  ] [INFO ] HV000001: Hibernate Validator 8.0.1.Final
    [2020/03/04 16:58:50] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [SimpleJob: [name=job01]] launched with the following parameters: [{'jsr_batch_run_id':'{value=1, type=class java.lang.Long, identifying=true}'}]
    [2020/03/04 16:58:50] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [job01.step01]
    [2020/03/04 16:58:50] [main] [o.s.b.c.s.AbstractStep] [INFO ] Step: [job01.step01] executed in 30ms
    [2020/03/04 16:58:50] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [SimpleJob: [name=job01]] completed with the following parameters: [{'jsr_batch_run_id':'{value=1, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 55ms

#### 9.3.2. プロジェクトのインポート

作成したプロジェクトをSTSへインポートする。
STSのメニューから、[File] → [Import] → [Maven] → [Existing Maven Projects] → [Next]を選択し、archetypeで作成したプロジェクトを選択する。

図 59. Import Existing Maven Projects

Root Directoryに`C:\xxx\macchinetta-batch-tutorial`を設定し、Projectsにcom.example.batchのpom.xmlが選択された状態で、 [Finish]を押下する。

図 60. Select Maven Projects

インポートが完了すると、Package Explorerに次のようなプロジェクトが表示される。

JavaConfig
図 61. Package Explorer
XMLConfig
図 62. Package Explorer

| インポート後にビルドエラーが発生する場合 インポート後にビルドエラーが発生する場合は、プロジェクト名を右クリックし、「Maven」→「Update Project…​」をクリックし、 「OK」ボタンをクリックすることでエラーが解消されるケースがある。

| パッケージの表示形式の設定 パッケージの表示形式は、デフォルトは「Flat」だが、「Hierarchical」にしたほうが見通しがよい。 Package Explorerの「View Menu」 (右端の下矢印)をクリックし、「Package Presentation」→「Hierarchical」を選択する。

#### 9.3.3. プロジェクトの構成

プロジェクトの構成については、Macchinetta Batch 2.x 開発ガイドライン本編のプロジェクトの構成を参照。

#### 9.3.4. 設定ファイルの確認・編集

##### 9.3.4.1. 設定ファイルの確認

作成したプロジェクトにはSpring BatchやMyBatisなどの設定のほとんどが既に設定済みである。
作成したプロジェクトの設定ファイルについてはプロジェクトの構成を参照。

| 設定値のカスタマイズについて チュートリアルを実施する場合にユーザの状況に応じてカスタマイズが必要な設定値について理解する必要はないが、チュートリアルを実施する前または後に一読するとよい。 詳細についてはアプリケーション全体の設定を参照。

##### 9.3.4.2. 設定ファイルの編集

チュートリアルを実施するため、H2 Databaseの設定を変更する。設定の変更点を以下に示す。

- 手動でサーバを起動することなく複数のバッチアプリケーションのプロセスからデータベースへ接続可能とする。

- バッチアプリケーションのプロセスの終了後でもデータを保持した状態のデータベースへ接続可能とする。

なお、H2 Databaseの設定の詳細については H2の公式ドキュメントのFeatures を参照。

具体的な編集内容を以下に示す。

batch-application.propertiesを開き、`admin.jdbc.url`及び`jdbc.url`を以下のように編集する。
下記の例は分かりやすさのために編集対象行のみ記載し、上書きではなくコメントアウトをした上で新たに行を追加している。

src/main/resources/batch-application.properties

    ##  Application settings.

    # Admin DataSource settings.
    #admin.jdbc.url=jdbc:h2:mem:batch-admin;DB_CLOSE_DELAY=-1
    admin.jdbc.url=jdbc:h2:~/batch-admin;AUTO_SERVER=TRUE

    # Job DataSource settings.
    #jdbc.url=jdbc:h2:mem:batch;DB_CLOSE_DELAY=-1
    jdbc.url=jdbc:h2:~/batch-admin;AUTO_SERVER=TRUE

| admin.jdbc.urlとjdbc.urlにおいて同じdatabaseName(batch-admin)を指定している理由 チュートリアルを実施する際のJDBCドライバの接続設定では、`admin.jdbc.url`と`jdbc.url`において同じdatabaseNameを指定している。 アプリケーション全体の設定に記載してあるとおり、`admin.jdbc.url`はFW(Spring BatchやMacchinetta Batch 2.x)が利用するURLであり、`jdbc.url`はジョブ個別が利用するURLである。 本来はFWとジョブ個別が使用するデータベースは分けるのが好ましい。 しかし、チュートリアルではデータベースを切替る手間をなくし、より簡単にFWとチュートリアルで使用するテーブルを参照するため、このような設定にしている。

#### 9.3.5. 入力データの準備

##### 9.3.5.1. データベースアクセスでデータ入出力を行うジョブの入力データ

データベースアクセスでデータ入出力を行うジョブで使用する入力データの準備を行う。
なお、データベースアクセスでデータ入出力を行うジョブを作成しない場合は実施する必要はない。

入力データの準備は以下の流れで行う。

1. テーブル作成・初期データ挿入スクリプト作成

2. ジョブ実行時にスクリプトを自動実行する設定の追加

これらの設定を行うことにより、ジョブ実行時(ApplicationContext生成時)にスクリプトを実行し、データベースの初期化を行う。

###### 9.3.5.1.1. テーブル作成・初期データ挿入スクリプト作成

テーブル作成・初期データ挿入スクリプトの作成を行う。

プロジェクトルートディレクトリに`sqls`ディレクトリを作成し、下記の3つのスクリプトを格納する。

- テーブル作成スクリプト(`create-member-info-table.sql`)

- 初期データ(正常)挿入スクリプト(`insert-member-info-data.sql`)

- 初期データ(異常)挿入スクリプト(`insert-member-info-error-data.sql`)

作成するファイルの内容を以下に示す。

sqls/create-member-info-table.sql

    CREATE TABLE IF NOT EXISTS member_info (
        id CHAR(8),
        type CHAR(1),
        status CHAR(1),
        point INT,
        PRIMARY KEY(id)
    );

sqls/insert-member-info-data.sql

    TRUNCATE TABLE member_info;
    INSERT INTO member_info (id, type, status, point) VALUES ('00000001', 'G', '1', 0);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000002', 'N', '1', 0);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000003', 'G', '0', 10);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000004', 'N', '0', 10);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000005', 'G', '1', 100);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000006', 'N', '1', 100);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000007', 'G', '0', 1000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000008', 'N', '0', 1000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000009', 'G', '1', 10000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000010', 'N', '1', 10000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000011', 'G', '0', 100000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000012', 'N', '0', 100000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000013', 'G', '1', 999901);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000014', 'N', '1', 999991);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000015', 'G', '0', 999900);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000016', 'N', '0', 999990);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000017', 'G', '1', 10);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000018', 'N', '1', 10);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000019', 'G', '0', 100);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000020', 'N', '0', 100);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000021', 'G', '1', 1000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000022', 'N', '1', 1000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000023', 'G', '0', 10000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000024', 'N', '0', 10000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000025', 'G', '1', 100000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000026', 'N', '1', 100000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000027', 'G', '0', 1000000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000028', 'N', '0', 1000000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000029', 'G', '1', 999899);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000030', 'N', '1', 999989);
    COMMIT;

sqls/insert-member-info-error-data.sql

    TRUNCATE TABLE member_info;
    INSERT INTO member_info (id, type, status, point) VALUES ('00000001', 'G', '0', 0);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000002', 'N', '0', 0);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000003', 'G', '1', 10);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000004', 'N', '1', 10);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000005', 'G', '0', 100);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000006', 'N', '0', 100);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000007', 'G', '1', 1000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000008', 'N', '1', 1000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000009', 'G', '0', 10000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000010', 'N', '0', 10000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000011', 'G', '1', 100000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000012', 'N', '1', 100000);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000013', 'G', '1', 1000001);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000014', 'N', '1', 999991);
    INSERT INTO member_info (id, type, status, point) VALUES ('00000015', 'G', '1', 999901);
    COMMIT;

###### 9.3.5.1.2. ジョブ実行時にスクリプトを自動実行する設定の追加

ジョブ実行時(ApplicationContext生成時)にスクリプトを実行し、データベースの初期化を行うため、`DataSourceInitializer/<jdbc:initialize-database>`の定義を追加する。

以下の2つのファイルの編集を実施する。

- batch-application.propertiesに実行対象スクリプトのパスの設定を追加する

- LaunchContextConfig.java/launch-context.xmlに`DataSourceInitializer/<jdbc:initialize-database>`の定義を追加する

具体的な設定内容を以下に示す。

batch-application.propertiesを開き、末尾に以下の実行対象スクリプトのパスの設定を追加する。

- tutorial.create-table.script(テーブル作成スクリプトのパス)

- tutorial.insert-data.script(初期データ挿入スクリプトのパス)

初期データ挿入スクリプトのパスは、実行するスクリプトの切替を容易にするために正常データと異常データを同じプロパティ名で定義し、コメントアウトしている。

src/main/resources/batch-application.properties

    # Database Initialize
    tutorial.create-table.script=file:sqls/create-member-info-table.sql
    tutorial.insert-data.script=file:sqls/insert-member-info-data.sql
    #tutorial.insert-data.script=file:sqls/insert-member-info-error-data.sql

`LaunchContextConfig.java/launch-context.xml`を開き、Bean定義で`DataSourceInitializer/<jdbc:initialize-database>`を追加する。

JavaConfig
com.example.batch.tutorial.config.LaunchContextConfig.java

    // database initialize definition
    @Bean
    public DataSourceInitializer jobDataSourceInitializer(@Qualifier("jobDataSource") DataSource jobDataSource,
                                                       @Value("${data-source.initialize.enabled:true}") boolean enabled,
                                                       @Value("${tutorial.create-table.script}") Resource script,
                                                       @Value("${tutorial.insert-data.script}") Resource commitScript) {
        final DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
        dataSourceInitializer.setDataSource(jobDataSource);
        dataSourceInitializer.setEnabled(true);
        ResourceDatabasePopulator resourceDatabasePopulator = new ResourceDatabasePopulator(script, commitScript);
        resourceDatabasePopulator.setContinueOnError(true);
        dataSourceInitializer.setDatabasePopulator(resourceDatabasePopulator);
        return dataSourceInitializer;
    }

XMLConfig
META-INF/spring/launch-context.xml

    <!-- database initialize definition -->
    <jdbc:initialize-database data-source="jobDataSource" enabled="${data-source.initialize.enabled:true}" ignore-failures="ALL">
        <jdbc:script location="${tutorial.create-table.script}" />
        <jdbc:script location="${tutorial.insert-data.script}" />
    </jdbc:initialize-database>

##### 9.3.5.2. ファイルアクセスでデータ入出力を行うジョブの入力データ

ファイルアクセスでデータ入出力を行うジョブで使用する入力データの準備を行う。
なお、ファイルアクセスでデータ入出力を行うジョブを作成しない場合は実施する必要はない。

入力データの準備として、入出力ファイルを格納するディレクトリの作成と入力ファイルの作成を行う。

プロジェクトルートディレクトリに入出力ファイル格納用として以下の2ディレクトリを作成する。

- `files/input`

- `files/output`

`files/input`配下に以下の2ファイルを作成する。

- 正常データ入力ファイル(`input-member-info-data.csv`)

- 異常データ入力ファイル(`input-member-info-error-data.csv`)

作成した入力ファイル格納ディレクトリに以下の内容で入力ファイルを格納する。

作成するファイルの内容を以下に示す。

files/input/input-member-info-data.csv

    00000001,G,1,0
    00000002,N,1,0
    00000003,G,0,10
    00000004,N,0,10
    00000005,G,1,100
    00000006,N,1,100
    00000007,G,0,1000
    00000008,N,0,1000
    00000009,G,1,10000
    00000010,N,1,10000
    00000011,G,0,100000
    00000012,N,0,100000
    00000013,G,1,999901
    00000014,N,1,999991
    00000015,G,0,999900
    00000016,N,0,999990
    00000017,G,1,10
    00000018,N,1,10
    00000019,G,0,100
    00000020,N,0,100
    00000021,G,1,1000
    00000022,N,1,1000
    00000023,G,0,10000
    00000024,N,0,10000
    00000025,G,1,100000
    00000026,N,1,100000
    00000027,G,0,1000000
    00000028,N,0,1000000
    00000029,G,1,999899
    00000030,N,1,999989

files/input/input-member-info-error-data.csv

    00000001,G,0,0
    00000002,N,0,0
    00000003,G,1,10
    00000004,N,1,10
    00000005,G,0,100
    00000006,N,0,100
    00000007,G,1,1000
    00000008,N,1,1000
    00000009,G,0,10000
    00000010,N,0,10000
    00000011,G,1,100000
    00000012,N,1,100000
    00000013,G,1,1000001
    00000014,N,1,999991
    00000015,G,1,999901

#### 9.3.6. データベースを参照する準備

チュートリアルでは、データベースの参照やSQL実行を行うためにH2 Consoleを使用する。
H2 Consoleでデータベースへ接続するまでの手順は以下のとおり。

1. H2のインストール

2. H2 Consoleのデータベース接続設定

チュートリアルで参照するデータベースの対象は以下のとおり。

- JobRepositoryに永続化されているバッチアプリケーション実行結果や状態を管理するためのデータ

- データベースアクセスでデータ入出力を行うジョブが使用するデータ

##### 9.3.6.1. H2のインストール

ここでは、H2のインストールを説明する。

1. `https://www.h2database.com/html/main.html`を開く。

2. Windows Installerを押下し、インストーラを適当な場所に保存する。

図 63. H2 Database Engine

3. インストーラを実行し、画面に従ってインストールする。

図 64. Installer

##### 9.3.6.2. H2 Consoleのデータベース接続設定

ここでは、H2 Consoleでデータベースに接続するための設定を説明する。

1. スタートメニューに作成された[H2]から、H2 Consoleを実行する。

図 65. Start

2. 下表のとおりフォームを入力する。

表 205. H2 Embedded接続設定
フォーム | 値
--- | ---
JDBC URL | jdbc:h2:~/batch-admin;AUTO_SERVER=TRUE
user | sa

図 66. Login

3. [Test Connection]を押下する。

4. 入力フォーム下部にTest successfulが表示され、接続できたことを確認し、[Connect]を押下する。

図 67. Connection Test

以上でSTSからデータベースを参照する準備が完了した。

#### 9.3.7. プロジェクトの動作確認

プロジェクトの動作確認の手順を以下に示す。

1. STSでジョブを実行する

2. H2 Consoleを使用してデータベースを参照する

##### 9.3.7.1. STSでジョブを実行する

STSでジョブを実行する手順を以下に示す。

1. Run Configuration(実行構成)の作成

2. ジョブの実行と結果の確認

| ジョブの実行方法について 本来であればジョブはシェルスクリプトなどから実行するが、本チュートリアルでは説明のしやすさからSTSでジョブを実行する手順としている。

###### 9.3.7.1.1. Run Configuration(実行構成)の作成

Run Configuration(実行構成)を作成する方法についてサンプルジョブの実行を例にして説明する。

1. STSのメニューから、[Run] → [Run Configurations…​]を押下し、[Run Configurations]を開く。

2. サイドバーの[Java Application]を右クリックで[New]を選択し、Run Configuration作成画面を表示して下表のとおり値を入力する。

表 206. Run ConfigurationsのMainタブで入力する値
項目名 | 値
--- | ---
Name | Execute Job01 (任意の値を設定する)
Project | macchinetta-batch-tutorial
Main class | org.springframework.batch.core.launch.support.CommandLineJobRunner

図 68. Run Configurations Main tab

3. [Arguments]タブを開き、下表のとおり値を入力する。

JavaConfig

項目名 | 値
--- | ---
Program arguments | com.example.batch.tutorial.jobs.Job01Config job01

図 69. Run Configurations Arguments tab
XMLConfig

項目名 | 値
--- | ---
Program arguments | META-INF/jobs/job01.xml job01

図 70. Run Configurations Arguments tab

4. [Apply]を押下する。

| Run Configurationの作成で設定する値について Run Configurationにサンプルジョブの実行(正しく作成できたことの確認)のコマンドと同様のパラメータを設定する。 ただし、クラスパスはMainタブのProjectへプロジェクトを設定するとSTSによって自動的に解決される。 Run Configurationに設定するパラメータは、実行するジョブによってパラメータを変更してほしい。 なお、ファイルアクセスでデータ入出力を行うジョブでは、パラメータで入出力ファイルを指定する必要があるため留意すること。

###### 9.3.7.1.2. ジョブの実行と結果の確認

ジョブの実行及び結果の確認方法について説明する。
ここで説明するジョブの実行結果の確認とはコンソールログの確認及びジョブ実行の終了コードの確認である。

チュートリアルではジョブ実行の終了コードを確認するため、Debug Viewを使用する。Debug Viewの表示方法は後述する。

| Debug Viewを表示する理由 STSでDebug Viewを表示しないとジョブの実行時の終了コードを確認することはできない。 try-catchで例外ハンドリングを行うジョブではリスナーにてジョブの終了コードの変換するため、Debug Viewを表示して結果を確認する必要がある。

1. STSのメニューから、[Run] → [Run Configurations…​]を押下し、[Run Configurations]を開く。

2. サイドバーの[Java Application]配下にあるRun Configuration(実行構成)の作成にて作成した[Execute Job01]を選択して[Run]を押下する。

図 71. Run Job01

3. ジョブの実行結果を[Console]で確認する。
   以下ようなコンソールログが[Console]に表示されていれば正常にジョブが実行されている。

コンソールログ出力例

    [2020/03/06 09:41:13] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=job01]] completed with the following parameters: [{jsr_batch_run_id=48}] and the following status: [COMPLETED] in 125ms

図 72. Run Job01 console

4. STSのメニューから、[Window] → [Show View] → [Other…​]を選択し、[Debug]配下の[Debug]を選択した状態で[OK]を押下する。

図 73. Show View

5. [Debug]でジョブ実行の終了コードを確認する。
   `<terminated, exit value: 0>`という表示のジョブ実行の終了コードが`0`であることから、ジョブが正常に終了したことを確認できる。

図 74. Debug View

| STSでジョブの実行が失敗する場合について 正しいソースコードにもかかわらずSTSでジョブの実行が失敗する場合、不完全なビルド状態を解消することによりジョブの実行が成功する可能性がある。手順を以下に示す。 STSのメニューから、[project] → [clean]を選択する。

##### 9.3.7.2. H2 Consoleを使用してデータベースを参照する

ここでは、H2 Consoleを使用してデータベースを参照する方法について説明する。

1. データベースに接続する。

2. テーブル一覧を確認する。
   Spring Batchメタデータテーブル(詳細はJobRepositoryのメタデータスキーマを参照)及び、データベースアクセスでデータ入出力を行うジョブの入力データの準備を実施した場合には`MEMBER_INFO`テーブルが作成されていることが確認できる。

図 75. Show Tables

3. 参照したいテーブルをクリックし、[Run]を押下する。

図 76. Sql

以下は`BATCH_JOB_EXECUTION`テーブルを参照した例である。

図 77. Show BATCH_JOB_EXECUTION

`job01`という名前のジョブが実行されたことがわかる。

以上でチュートリアルの環境構築は完了である。

### 9.4. バッチジョブの実装

#### 9.4.1. データベースアクセスでデータ入出力を行うジョブ

##### 9.4.1.1. 概要

データベースアクセスを行うジョブを作成する。

なお、詳細についてはMacchinetta Batch 2.x 開発ガイドラインのデータベースアクセスを参照。

作成するアプリケーションの説明の 背景、処理概要、業務仕様を以下に再掲する。

###### 9.4.1.1.1. 背景

とある量販店では、会員に対してポイントカードを発行している。
会員には「ゴールド会員」「一般会員」の会員種別が存在し、会員種別に応じたサービスを提供している。
今回そのサービスの一環として、月内に商品を購入した会員のうち、 会員種別が「ゴールド会員」の場合は100ポイント、「一般会員」の場合は10ポイントを月末に加算することにした。

###### 9.4.1.1.2. 処理概要

会員種別に応じてポイント加算を行うアプリケーションを 月次バッチ処理としてMacchinetta Batch 2.xを使用して実装する。

###### 9.4.1.1.3. 業務仕様

業務仕様は以下のとおり。

- 商品購入フラグが"1"(処理対象)の場合に、会員種別に応じてポイントを加算する

  - 会員種別が"G"(ゴールド会員)の場合は100ポイント、"N"(一般会員)の場合は10ポイント加算する

- 商品購入フラグはポイント加算後に"0"(初期状態)に更新する

- ポイントの上限値は1,000,000ポイントとする

- ポイント加算後に1,000,000ポイントを超えた場合は、1,000,000ポイントに補正する

###### 9.4.1.1.4. テーブル仕様

入出力リソースとなる会員情報テーブルの仕様は以下のとおり。

表 207. 会員情報テーブル(member_info)
No | 属性名 | カラム名 | PK | データ型 | 桁数 | 説明
--- | --- | --- | --- | --- | --- | ---
1 | 会員番号 | id |  | CHAR | 8 | 会員を一意に示す8桁固定の番号を表す。
2 | 会員種別 | type | - | CHAR | 1 | 会員の種別を以下のとおり表す。 "G"(ゴールド会員)、"N"(一般会員)
3 | 商品購入フラグ | status | - | CHAR | 1 | 月内に商品を買ったかどうかを表す。 商品購入で"1"(処理対象)、月次バッチ処理で"0"(初期状態)に更新される。
4 | ポイント | point | - | INT | 7 | 会員の保有するポイントを表す。 初期値は0。

| テーブル仕様について チュートリアルを実施するうえでの便宜を図り、 実際の業務に即したテーブル設計は行っていないため留意すること。

###### 9.4.1.1.5. ジョブの概要

ここで作成するデータベースアクセスするジョブの概要を把握するために、 処理フローおよび処理シーケンスを以下に示す。

処理シーケンスではトランザクション制御の範囲について触れている。 ジョブのトランザクション制御はSpring Batchがもつ仕組みを利用しており、これをフレームワークトランザクションと定義して説明する。 トランザクション制御の詳細はトランザクション制御を参照。

処理フロー概要

  処理フローの概要を以下に示す。

図 78. データベースアクセスジョブの処理フロー

チャンクモデルの場合の処理シーケンス

  チャンクモデルの場合の処理シーケンスを説明する。

橙色のオブジェクトは今回実装するクラスを表す。

図 79. チャンクモデルのシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

2. ステップは、リソースをオープンする。

3. `MyBatisCursorItemReader`は、member_infoテーブルから会員情報を取得するためのselect文を発行する。

   - 入力データがなくなるまで、以降の処理を繰り返す。

   - チャンク単位で、フレームワークトランザクションを開始する。

   - チャンクサイズに達するまで4から10までの処理を繰り返す。

4. ステップは、`MyBatisCursorItemReader`から入力データを1件取得する処理を行う。

5. `MyBatisCursorItemReader`は、member_infoテーブルから入力データを1件取得する。

6. member_infoテーブルは、`MyBatisCursorItemReader`に入力データを返却する。

7. `MyBatisCursorItemReader`は、ステップに入力データを返却する。

8. ステップは、`PointAddItemProcessor`で入力データに対して処理を行う。

9. `PointAddItemProcessor`は、入力データを読み込んでポイント加算処理を行う。

10. `PointAddItemProcessor`は、ステップに処理結果を返却する。

11. ステップは、チャンクサイズ分のデータを`MyBatisBatchItemWriter`で出力する。

12. `MyBatisBatchItemWriter`は、member_infoテーブルに対して会員情報の更新(update文の発行)を行う。

13. ステップはフレームワークトランザクションをコミットする。

14. ステップはジョブに終了コード(ここでは正常終了:0)を返却する。

| Cursorについての説明 上記のシーケンス図を読み進める上で必要なCursorについての説明を行う。Cursorとは、検索結果がマッピングされたBeanの代わりにMyBatisCursorItemReaderにより1件ずつデータが返却される仕組みである。以下にCursorを用いた処理の流れについて説明する。 SELECT文を発行し、Cursorを取得する。 Cursor自身にはSELECT文結果のレコードが直接含まれておらず、Cursorはレコードを指す位置情報を保持している。 2から3までの処理をレコードがなくなるまで繰り返す。 SELECT文結果を取得するためにCursorが指すレコードに対してfetchを行う。 Cursorの初回fetchはSELECT文結果の先頭行のレコードを指している。 Cursorを次のレコードへと進める。 Cursorを閉じる。

タスクレットモデルの場合の処理シーケンス

  タスクレットモデルの場合の処理シーケンスについて説明する。

このチュートリアルでは、タスクレットモデルでもチャンクモデルのように一定件数のデータをまとめて処理する方式としている。 大量データを効率的に処理できるなどのメリットがある。 詳細はチャンクモデルのコンポーネントを利用するTasklet実装を参照。

橙色のオブジェクトは今回実装するクラスを表す。

図 80. タスクレットモデルのシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

   - ステップはフレームワークトランザクションを開始する。

2. ステップは`PointAddTasklet`を実行する。

3. `PointAddTasklet`は、リソースをオープンする。

4. `MyBatisCursorItemReader`は、member_infoテーブルから会員情報を取得するためのselect文を発行する。

   - 入力データがなくなるまで5から9までの処理を繰り返す。

   - 一定件数に達するまで5から11までの処理を繰り返す。

5. `PointAddTasklet`は、`MyBatisCursorItemReader`から入力データを1件取得する処理を行う。

6. `MyBatisCursorItemReader`は、member_infoテーブルから入力データを1件取得する。

7. member_infoテーブルは、`MyBatisCursorItemReader`に入力データを返却する。

8. `MyBatisCursorItemReader`は、タスクレットに入力データを返却する。

9. `PointAddTasklet`は、入力データを読み込んでポイント加算処理を行う。

10. `PointAddTasklet`は、一定件数分のデータを`MyBatisBatchItemWriter`で出力する。

11. `MyBatisBatchItemWriter`は、member_infoテーブルに対して会員情報の更新(update文の発行)を行う。

12. `PointAddTasklet`はステップへ処理終了を返却する。

13. ステップはフレームワークトランザクションをコミットする。

14. ステップはジョブに終了コード(ここでは正常終了:0)を返却する。

以降で、チャンクモデル、タスクレットモデルそれぞれの実装方法を説明する。

- チャンクモデルでの実装

- タスクレットモデルでの実装

##### 9.4.1.2. チャンクモデルでの実装

チャンクモデルでデータベースアクセスするジョブの作成から実行までを以下の手順で実施する。

1. ジョブBean定義ファイルの作成

2. DTOの実装

3. MyBatisによるデータベースアクセスの定義

4. ロジックの実装

5. ジョブの実行と結果の確認

###### 9.4.1.2.1. ジョブBean定義ファイルの作成

Bean定義ファイルにて、チャンクモデルでデータベースアクセスを行うジョブを構成する要素の組み合わせ方を設定する。
ここでは、Bean定義ファイルの枠および共通的な設定のみ記述し、以降の項で各構成要素の設定を行う。

JavaConfig
com.example.batch.tutorial.config.dbaccess.JobPointAddChunkConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class) // (1)
    @ComponentScan("com.example.batch.tutorial.dbaccess.chunk") // (2)
    public class JobPointAddChunkConfig {

    }

XMLConfig
src/main/resources/META-INF/jobs/dbaccess/jobPointAddChunk.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd
                 http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

        <!-- (1) -->
        <import resource="classpath:META-INF/spring/job-base-context.xml"/>

        <!-- (2) -->
        <context:component-scan base-package="com.example.batch.tutorial.dbaccess.chunk"/>

    </beans>

表 208. 説明
項番 | 説明
--- | ---
(1) | Macchinetta Batch 2.xを利用する際に、常に必要なBean定義を読み込む設定をインポートする。
(2) | コンポーネントスキャンの設定を行う。 `value属性(省略可能)/base-package属性`に、使用するコンポーネント(ItemProcessorの実装クラスなど)が格納されているパッケージを指定する。

###### 9.4.1.2.2. DTOの実装

業務データを保持するためのクラスとしてDTOクラスを実装する。
DTOクラスはテーブルごとに作成する。

チャンクモデル/タスクレットモデルで共通して利用するため、既に作成している場合は読み飛ばしてよい。

com.example.batch.tutorial.common.dto.MemberInfoDto

    package com.example.batch.tutorial.common.dto;

    public class MemberInfoDto {
        private String id; // (1)

        private String type; // (2)

        private String status; // (3)

        private int point; // (4)

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }

        public String getStatus() {
            return status;
        }

        public void setStatus(String status) {
            this.status = status;
        }

        public int getPoint() {
            return point;
        }

        public void setPoint(int point) {
            this.point = point;
        }
    }

表 209. 説明
項番 | 説明
--- | ---
(1) | 会員番号に対応するフィールドとして`id`を定義する。
(2) | 会員種別に対応するフィールドとして`type`を定義する。
(3) | 商品購入フラグに対応するフィールドとして`status`を定義する。
(4) | ポイントに対応するフィールドとして`point`を定義する。

###### 9.4.1.2.3. MyBatisによるデータベースアクセスの定義

MyBatisを利用してデータベースアクセスするための実装・設定を行う。

以下の作業を実施する。

1. `Repository`インタフェースの実装

2. MapperXMLファイルの作成

3. ジョブBean定義ファイルの設定

チャンクモデル/タスクレットモデルで共通して利用するため、既に作成している場合は読み飛ばしてよい。

`Repository`インタフェースの実装

MapperXMLファイルに定義したSQLを呼び出すためのインタフェースを実装する。
このインタフェースに対する実装クラスは、MyBatisが自動で生成するため、開発者はインタフェースのみ作成すればよい。

com.example.batch.tutorial.common.repository.MemberInfoRepository

    package com.example.batch.tutorial.common.repository;

    import com.example.batch.tutorial.common.dto.MemberInfoDto;
    import org.apache.ibatis.cursor.Cursor;

    public interface MemberInfoRepository {
      Cursor<MemberInfoDto> cursor(); // (1)

      int updatePointAndStatus(MemberInfoDto memberInfo); // (2)
    }

表 210. 説明
項番 | 説明
--- | ---
(1) | MapperXMLファイルに定義するSQLのIDに対応するメソッドを定義する。 ここでは、member_infoテーブルからすべてのレコードを取得するためのメソッドを定義する。
(2) | ここでは、member_infoテーブルのpointカラムとstatusカラムを更新するためのメソッドを定義する。

MapperXMLファイルの作成

SQLとO/Rマッピングの設定を記載するMapperXMLファイルを作成する。
MapperXMLファイルは、`Repository`インタフェースごとに作成する。

MyBatisが定めたルールに則ったディレクトリに格納することで、自動的にMapperXMLファイルを読み込むことができる。 MapperXMLファイルを自動的に読み込ませるために、`Repository`インタフェースのパッケージ階層と同じ階層のディレクトリにMapperXMLファイルを格納する。

src/main/resources/com/example/batch/tutorial/common/repository/MemberInfoRepository.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

    <!-- (1) -->
    <mapper namespace="com.example.batch.tutorial.common.repository.MemberInfoRepository">

        <!-- (2) -->
        <select id="cursor" resultType="com.example.batch.tutorial.common.dto.MemberInfoDto">
            SELECT
                id,
                type,
                status,
                point
            FROM
                member_info
            ORDER BY
                id ASC
        </select>

        <!-- (3) -->
        <update id="updatePointAndStatus" parameterType="com.example.batch.tutorial.common.dto.MemberInfoDto">
            UPDATE
                member_info
            SET
                status = #{status},
                point = #{point}
            WHERE
                id = #{id}
        </update>
    </mapper>

表 211. 説明
項番 | 説明
--- | ---
(1) | `<mapper>`要素の`namespace`属性に、`Repository`インタフェースの完全修飾クラス名(FQCN)を指定する。
(2) | 参照系のSQLの設定を行う。 ここでは、member_infoテーブルからすべてのレコードを取得するSQLを設定する。
(3) | 更新系のSQLの設定を行う。 ここでは、member_infoテーブルの指定したidに一致するレコードについて、 ステータスとポイントを更新するSQLを設定する。

ジョブBean定義ファイルの設定

MyBatisによるデータベースアクセスするための設定として、ジョブBean定義ファイルに以下の(1)～(3)を追記する。

JavaConfig
com.example.batch.tutorial.config.dbaccess.JobPointAddChunkConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan("com.example.batch.tutorial.dbaccess.chunk")
    @MapperScan(basePackages = "com.example.batch.tutorial.common.repository", sqlSessionFactoryRef = "jobSqlSessionFactory") // (1)
    public class JobPointAddChunkConfig {

        // (2)
        @Bean
        public MyBatisCursorItemReader<MemberInfoDto> reader(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory) {
            return new MyBatisCursorItemReaderBuilder<MemberInfoDto>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .queryId(
                            "com.example.batch.tutorial.common.repository.MemberInfoRepository.cursor")
                    .build();
        }

        // (3)
        @Bean
        public MyBatisBatchItemWriter<MemberInfoDto> writer(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
                SqlSessionTemplate batchModeSqlSessionTemplate) {
            return new MyBatisBatchItemWriterBuilder<MemberInfoDto>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .statementId(
                            "com.example.batch.tutorial.common.repository.MemberInfoRepository.updatePointAndStatus")
                    .sqlSessionTemplate(batchModeSqlSessionTemplate)
                    .build();
        }

    }

XMLConfig
src/main/resources/META-INF/jobs/dbaccess/jobPointAddChunk.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd
                 http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

        <import resource="classpath:META-INF/spring/job-base-context.xml"/>

        <context:component-scan base-package="com.example.batch.tutorial.dbaccess.chunk"/>

        <!-- (1) -->
        <mybatis:scan base-package="com.example.batch.tutorial.common.repository" factory-ref="jobSqlSessionFactory"/>

        <!-- (2) -->
        <bean id="reader"
              class="org.mybatis.spring.batch.MyBatisCursorItemReader"
              p:queryId="com.example.batch.tutorial.common.repository.MemberInfoRepository.cursor"
              p:sqlSessionFactory-ref="jobSqlSessionFactory"/>

        <!-- (3) -->
        <bean id="writer" class="org.mybatis.spring.batch.MyBatisBatchItemWriter"
              p:statementId="com.example.batch.tutorial.common.repository.MemberInfoRepository.updatePointAndStatus"
              p:sqlSessionTemplate-ref="batchModeSqlSessionTemplate"/>

    </beans>

表 212. 説明
項番 | 説明
--- | ---
(1) | `Repository`インタフェースをスキャンするための設定を行う。 `basePackages属性/base-package属性`に、`Repository`インタフェースが格納されている基底パッケージを指定する。
(2) | ItemReaderの設定を行う。 `戻り値の型/class属性`に、MyBatisが提供しているSpring連携ライブラリであるMyBatis-Springから提供されている`org.mybatis.spring.batch.MyBatisCursorItemReader`を指定する。 データベースから大量データを取得する際は、この`MyBatisCursorItemReader`を使用する。 詳細は入力を参照。 `MyBatisCursorItemReaderBuilderのqueryIdメソッド/queryId属性`に、MapperXMLファイルに設定するSQLの`namespace`+`id`を指定する。
(3) | ItemWriterの設定を行う。 `戻り値の型/class属性`に、MyBatisが提供しているSpring連携ライブラリであるMyBatis-Springから提供されている`org.mybatis.spring.batch.MyBatisBatchItemWriter`を指定する。 `MyBatisBatchItemWriterBuilderのstatementIdメソッド/statementId属性`に、MapperXMLファイルに設定するSQLの`namespace`+`id`を指定する。

| ItemReader・ItemWriter以外のデータベースアクセス `ItemReader`・`ItemWriter`以外でデータベースアクセスする方法として、`Mapper`インタフェースを利用する方法がある。 `Mapper`インタフェースを利用するにあたっては、Macchinetta Batch 2.xとして制約を設けているため、 `Mapper`インタフェース(入力)、`Mapper`インタフェース(出力)を参照してほしい。 ItemProcessorの実装例は、チャンクモデルにおける利用方法(入力)を参照。

###### 9.4.1.2.4. ロジックの実装

ポイント加算処理を行うビジネスロジッククラスを実装する。

以下の作業を実施する。

1. PointAddItemProcessorクラスの実装

2. ジョブBean定義ファイルの設定

PointAddItemProcessorクラスの実装

`ItemProcessor`インタフェースを実装したPointAddItemProcessorクラスを実装する。

com.example.batch.tutorial.dbaccess.chunk.PointAddItemProcessor

    package com.example.batch.tutorial.dbaccess.chunk;

    import org.springframework.batch.item.ItemProcessor;
    import org.springframework.stereotype.Component;
    import com.example.batch.tutorial.common.dto.MemberInfoDto;

    @Component // (1)
    public class PointAddItemProcessor implements ItemProcessor<MemberInfoDto, MemberInfoDto> { // (2)

        private static final String TARGET_STATUS = "1"; // (3)

        private static final String INITIAL_STATUS = "0"; // (4)

        private static final String GOLD_MEMBER = "G"; // (5)

        private static final String NORMAL_MEMBER = "N"; // (6)

        private static final int MAX_POINT = 1000000; // (7)

        @Override
        public MemberInfoDto process(MemberInfoDto item) throws Exception { // (8) (9) (10)
            if (TARGET_STATUS.equals(item.getStatus())) {
                if (GOLD_MEMBER.equals(item.getType())) {
                    item.setPoint(item.getPoint() + 100);
                } else if (NORMAL_MEMBER.equals(item.getType())) {
                    item.setPoint(item.getPoint() + 10);
                }

                if (item.getPoint() > MAX_POINT) {
                    item.setPoint(MAX_POINT);
                }

                item.setStatus(INITIAL_STATUS);
            }

            return item;
        }
    }

表 213. 説明
項番 | 説明
--- | ---
(1) | コンポーネントスキャンの対象とするため、`@Component`アノテーションを付与してBean定義を行う。
(2) | 入出力で使用するオブジェクトの型をそれぞれ型引数に指定した`ItemProcessor`インタフェースを実装する。 ここでは、入出力で使用するオブジェクトは共にDTOの実装で作成した`MemberInfoDto`を指定する。
(3) | 定数として、ポイント加算対象とする商品購入フラグ:1を定義する。 本来、このようなフィールド定数は定数クラスなどに定義し、ロジックに定義することはあまりない。 このチュートリアルでは、便宜上、定数として定義していることを留意すること。(以降の定数も同様)
(4) | 定数として、商品購入フラグの初期値:0を定義する。
(5) | 定数として、会員種別:G(ゴールド会員)を定義する。
(6) | 定数として、会員種別:N(一般会員)を定義する。
(7) | 定数として、ポイントの上限値:1000000を定義する。
(8) | 商品購入フラグおよび、会員種別に応じてポイント加算するビジネスロジックを実装する。
(9) | 返り値の型は、このクラスで実装している`ItemProcessor`インタフェースの型引数で指定した 出力オブジェクトの型である`MemberInfoDto`とする。
(10) | 引数として受け取る`item`の型は、 このクラスで実装している`ItemProcessor`インタフェースの型引数で指定した入力オブジェクトの型である`MemberInfoDto`とする。

ジョブBean定義ファイルの設定

作成したビジネスロジックをジョブとして設定するため、ジョブBean定義ファイルに以下の(1)以降を追記する。

JavaConfig
com.example.batch.tutorial.config.dbaccess.JobPointAddChunkConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan("com.example.batch.tutorial.dbaccess.chunk")
    @MapperScan(basePackages = "com.example.batch.tutorial.common.repository", sqlSessionFactoryRef = "jobSqlSessionFactory")
    public class JobPointAddChunkConfig {

        @Bean
        public MyBatisCursorItemReader<MemberInfoDto> reader(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory) {
            return new MyBatisCursorItemReaderBuilder<MemberInfoDto>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .queryId(
                            "com.example.batch.tutorial.common.repository.MemberInfoRepository.cursor")
                    .build();
        }

        @Bean
        public MyBatisBatchItemWriter<MemberInfoDto> writer(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
                SqlSessionTemplate batchModeSqlSessionTemplate) {
            return new MyBatisBatchItemWriterBuilder<MemberInfoDto>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .statementId(
                            "com.example.batch.tutorial.common.repository.MemberInfoRepository.updatePointAndStatus")
                    .sqlSessionTemplate(batchModeSqlSessionTemplate)
                    .build();
        }

        // (2)
        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           ItemReader<MemberInfoDto> reader,
                           PointAddItemProcessor processor,
                           ItemWriter<MemberInfoDto> writer) {
            return new StepBuilder("jobPointAddChunk.step01",
                    jobRepository)
                    .<MemberInfoDto, MemberInfoDto>chunk(10, // (3)
                            transactionManager)
                    .reader(reader)
                    .processor(processor)
                    .writer(writer)
                    .build();
        }

        @Bean
        public Job jobPointAddChunk(JobRepository jobRepository,
                                                 Step step01) {
            return new JobBuilder("jobPointAddChunk", jobRepository) // (1)
                    .start(step01)
                    .build();
        }
    }

XMLConfig
src/main/resources/META-INF/jobs/dbaccess/jobPointAddChunk.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd
                 http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

        <import resource="classpath:META-INF/spring/job-base-context.xml"/>

        <context:component-scan base-package="com.example.batch.tutorial.dbaccess.chunk"/>

        <mybatis:scan base-package="com.example.batch.tutorial.common.repository" factory-ref="jobSqlSessionFactory"/>

        <bean id="reader"
              class="org.mybatis.spring.batch.MyBatisCursorItemReader"
              p:queryId="com.example.batch.tutorial.common.repository.MemberInfoRepository.cursor"
              p:sqlSessionFactory-ref="jobSqlSessionFactory"/>

        <bean id="writer" class="org.mybatis.spring.batch.MyBatisBatchItemWriter"
              p:statementId="com.example.batch.tutorial.common.repository.MemberInfoRepository.updatePointAndStatus"
              p:sqlSessionTemplate-ref="batchModeSqlSessionTemplate"/>

        <!-- (1) -->
        <batch:job id="jobPointAddChunk" job-repository="jobRepository">
            <batch:step id="jobPointAddChunk.step01"> <!-- (2) -->
                <batch:tasklet transaction-manager="jobTransactionManager">
                    <batch:chunk reader="reader"
                                 processor="pointAddItemProcessor"
                                 writer="writer" commit-interval="10"/> <!-- (3) -->
                </batch:tasklet>
            </batch:step>
        </batch:job>
    </beans>

表 214. 説明
項番 | 説明
--- | ---
(1) | ジョブの設定を行う。 `JobBuilderのコンストラクタの引数name/id属性`は、1つのバッチアプリケーションに含まれる全ジョブの範囲内で一意とする必要がある。 ここでは、チャンクモデルのジョブ名として`jobPointAddChunk`を指定する。
(2) | ステップの設定を行う。 `StepBuilderのコンストラクタの引数name/id属性`は、1つのバッチアプリケーションに含まれる全ジョブの範囲内で一意とする必要はないが、障害発生時に追跡しやすくなる等の様々なメリットがあるため一意とする。 特別な理由がない限り、(1)で指定した`JobBuilderのコンストラクタの引数name/id属性`に[step+連番]を付加する形式とする。 ここでは、チャンクモデルのジョブのステップ名として`jobPointAddChunk.step01`を指定する。
(3) | チャンクモデルジョブの設定を行う。 `StepBuilderのreaderメソッド、writerメソッド/reader、writerそれぞれの属性`に、前項までに定義した`ItemReader`、`ItemWriter`のBeanIDを指定する。 `StepBuilderのprocessorメソッド/processor属性`に、ItemProcessorの実装クラスのBeanIDである`pointAddItemProcessor`を指定する。 `chunkメソッドの引数chunkSize/commit-interval属性`に、1チャンクあたりの入力データ件数を10件として設定する。

| commit-intervalのチューニング commit-intervalはチャンクモデルジョブにおける、性能上のチューニングポイントである。 このチュートリアルでは10件としているが、利用できるマシンリソースやジョブの特性によって適切な件数は異なる。 複数のリソースにアクセスしてデータを加工するジョブであれば10件から100件程度で処理スループットが頭打ちになることもある。 一方、入出力リソースが1:1対応しておりデータを移し替える程度のジョブであれば5,000件や10,000件でも処理スループットが伸びることがある。 ジョブ実装時のcommit-intervalは100件程度で仮置きしておき、 その後に実施した性能測定の結果に応じてジョブごとにチューニングするとよい。

###### 9.4.1.2.5. ジョブの実行と結果の確認

作成したジョブをSTS上で実行し、結果を確認する。

実行構成からジョブを実行

以下のとおり実行構成を作成し、ジョブを実行する。
実行構成の作成手順はプロジェクトの動作確認を参照。

実行構成の設定値

JavaConfig

- Name: 任意の名称(例: Run DBAccessJob for ChunkModel)

- Mainタブ

  - Project: `macchinetta-batch-tutorial`

  - Main class: `org.springframework.batch.core.launch.support.CommandLineJobRunner`

- Argumentsタブ

  - Program arguments: `com.example.batch.tutorial.config.dbaccess.JobPointAddChunkConfig jobPointAddChunk`

XMLConfig

- Name: 任意の名称(例: Run DBAccessJob for ChunkModel)

- Mainタブ

  - Project: `macchinetta-batch-tutorial`

  - Main class: `org.springframework.batch.core.launch.support.CommandLineJobRunner`

- Argumentsタブ

  - Program arguments: `META-INF/jobs/dbaccess/jobPointAddChunk.xml jobPointAddChunk`

コンソールログの確認

Console Viewを開き、以下の内容のログが出力されていることを確認する。

- 処理が完了(COMPLETED)し、例外が発生していないこと。

コンソールログ出力例

    (.. omitted)

    [2020/03/10 13:07:50] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddChunk]] launched with the following parameters: [{jsr_batch_run_id=112}]
    [2020/03/10 13:07:50] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [jobPointAddChunk.step01]
    [2020/03/10 13:07:50] [main] [o.s.b.c.s.AbstractStep] [INFO ] Step: [jobPointAddChunk.step01] executed in 173ms
    [2020/03/10 13:07:50] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddChunk]] completed with the following parameters: [{jsr_batch_run_id=112}] and the following status: [COMPLETED] in 330ms

終了コードの確認

終了コードにより、正常終了したことを確認する。
確認手順はジョブの実行と結果の確認を参照。 終了コード(exit value)が0(正常終了)となっていることを確認する。

図 81. 終了コードの確認
会員情報テーブルの確認

更新前後の会員情報テーブルの内容を比較し、確認内容のとおりとなっていることを確認する。
確認手順はH2 Consoleを使用してデータベースを参照するを参照。

確認内容

  - statusカラム

    - "1"(処理対象)から"0"(初期状態)に更新されていること

  - pointカラム

    - ポイント加算対象について、会員種別に応じたポイントが加算されていること

      - typeカラムが"G"(ゴールド会員)の場合は100ポイント

      - typeカラムが"N"(一般会員)の場合は10ポイント

    - 1,000,000(上限値)を超えたレコードが存在しないこと

更新前後の会員情報テーブルの内容は以下のとおり。

図 82. 更新前後の会員情報テーブルの内容

##### 9.4.1.3. タスクレットモデルでの実装

タスクレットモデルでデータベースアクセスするジョブの作成から実行までを以下の手順で実施する。

1. ジョブBean定義ファイルの作成

2. DTOの実装

3. MyBatisによるデータベースアクセスの定義

4. ロジックの実装

5. ジョブの実行と結果の確認

###### 9.4.1.3.1. ジョブBean定義ファイルの作成

Bean定義ファイルにて、タスクレットモデルでデータベースアクセスを行うジョブを構成する要素の組み合わせ方を設定する。
ここでは、Bean定義ファイルの枠および共通的な設定のみ記述し、以降の項で各構成要素の設定を行う。

JavaConfig
com.example.batch.tutorial.config.dbaccess.JobPointAddTaskletConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class) // (1)
    @ComponentScan("com.example.batch.tutorial.dbaccess.tasklet") // (2)
    public class JobPointAddTaskletConfig {

    }

XMLConfig
src/main/resources/META-INF/jobs/dbaccess/jobPointAddTasklet.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd
                 http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

        <!-- (1) -->
        <import resource="classpath:META-INF/spring/job-base-context.xml"/>

        <!-- (2) -->
        <context:component-scan base-package="com.example.batch.tutorial.dbaccess.tasklet"/>

    </beans>

表 215. 説明
項番 | 説明
--- | ---
(1) | Macchinetta Batch 2.xを利用する際に、常に必要なBean定義を読み込む設定をインポートする。
(2) | コンポーネントスキャンの設定を行う。 `basePackages属性/base-package属性`に、使用するコンポーネント(Taskletの実装クラスなど)が格納されているパッケージを指定する。

###### 9.4.1.3.2. DTOの実装

業務データを保持するためのクラスとしてDTOクラスを作成する。
DTOクラスはテーブルごとに作成する。

チャンクモデル/タスクレットモデルで共通して利用するため、既に作成している場合は読み飛ばしてよい。

com.example.batch.tutorial.common.dto.MemberInfoDto

    package com.example.batch.tutorial.common.dto;

    public class MemberInfoDto {
        private String id; // (1)

        private String type; // (2)

        private String status; // (3)

        private int point; // (4)

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }

        public String getStatus() {
            return status;
        }

        public void setStatus(String status) {
            this.status = status;
        }

        public int getPoint() {
            return point;
        }

        public void setPoint(int point) {
            this.point = point;
        }
    }

表 216. 説明
項番 | 説明
--- | ---
(1) | 会員番号に対応するフィールドとして`id`を定義する。
(2) | 会員種別に対応するフィールドとして`type`を定義する。
(3) | 商品購入フラグに対応するフィールドとして`status`を定義する。
(4) | ポイントに対応するフィールドとして`point`を定義する。

###### 9.4.1.3.3. MyBatisによるデータベースアクセスの定義

MyBatisを利用してデータベースアクセスするための実装・設定を行う。

以下の作業を実施する。

1. `Repository`インタフェースの実装

2. MapperXMLファイルの作成

3. ジョブBean定義ファイルの設定

チャンクモデル/タスクレットモデルで共通して利用するため、既に作成している場合は読み飛ばしてよい。

`Repository`インタフェースの実装

MapperXMLファイルに定義したSQLを呼び出すためのインタフェースを作成する。
このインタフェースに対する実装クラスは、MyBatisが自動で生成するため、開発者はインタフェースのみ作成すればよい。

com.example.batch.tutorial.common.repository.MemberInfoRepository

    package com.example.batch.tutorial.common.repository;

    import com.example.batch.tutorial.common.dto.MemberInfoDto;
    import org.apache.ibatis.cursor.Cursor;

    public interface MemberInfoRepository {
        Cursor<MemberInfoDto> cursor(); // (1)

        int updatePointAndStatus(MemberInfoDto memberInfo); // (2)
    }

表 217. 説明
項番 | 説明
--- | ---
(1) | MapperXMLファイルに定義するSQLのIDに対応するメソッドを定義する。 ここでは、member_infoテーブルからすべてのレコードを取得するためのメソッドを定義する。
(2) | ここでは、member_infoテーブルのpointカラムとstatusカラムを更新するためのメソッドを定義する。

MapperXMLファイルの作成

SQLとO/Rマッピングの設定を記載するMapperXMLファイルを作成する。
MapperXMLファイルは、`Repository`インタフェースごとに作成する。

MyBatisが定めたルールに則ったディレクトリに格納することで、自動的にMapperXMLファイルを読み込むことができる。 MapperXMLファイルを自動的に読み込ませるために、`Repository`インタフェースのパッケージ階層と同じ階層のディレクトリにMapperXMLファイルを格納する。

src/main/resources/com/example/batch/tutorial/common/repository/MemberInfoRepository.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

    <!-- (1) -->
    <mapper namespace="com.example.batch.tutorial.common.repository.MemberInfoRepository">

        <!-- (2) -->
        <select id="cursor" resultType="com.example.batch.tutorial.common.dto.MemberInfoDto">
            SELECT
                id,
                type,
                status,
                point
            FROM
                member_info
            ORDER BY
                id ASC
        </select>

        <!-- (3) -->
        <update id="updatePointAndStatus" parameterType="com.example.batch.tutorial.common.dto.MemberInfoDto">
            UPDATE
                member_info
            SET
                status = #{status},
                point = #{point}
            WHERE
                id = #{id}
        </update>
    </mapper>

表 218. 説明
項番 | 説明
--- | ---
(1) | `<mapper>`要素の`namespace`属性に、`Repository`インタフェースの完全修飾クラス名(FQCN)を指定する。
(2) | 参照系のSQLの定義を行う。 ここでは、member_infoテーブルからすべてのレコードを取得するSQLを設定する。
(3) | 更新系のSQLの定義を行う。 ここでは、member_infoテーブルの指定したidに一致するレコードについて、 statusとpointを更新するSQLを設定する。

ジョブBean定義ファイルの設定

MyBatisによるデータベースアクセスするための設定として、ジョブBean定義ファイルに以下の(1)～(3)を追記する。

JavaConfig
com.example.batch.tutorial.config.dbaccess.JobPointAddTaskletConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan("com.example.batch.tutorial.dbaccess.tasklet")
    @MapperScan(basePackages = "com.example.batch.tutorial.common.repository", sqlSessionFactoryRef = "jobSqlSessionFactory") // (1)
    public class JobPointAddTaskletConfig {

        // (2)
        @Bean
        public MyBatisCursorItemReader<MemberInfoDto> reader(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory) {
            return new MyBatisCursorItemReaderBuilder<MemberInfoDto>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .queryId(
                            "com.example.batch.tutorial.common.repository.MemberInfoRepository.cursor")
                    .build();
        }

        // (3)
        @Bean
        public MyBatisBatchItemWriter<MemberInfoDto> writer(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
                SqlSessionTemplate batchModeSqlSessionTemplate) {
            return new MyBatisBatchItemWriterBuilder<MemberInfoDto>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .statementId(
                            "com.example.batch.tutorial.common.repository.MemberInfoRepository.updatePointAndStatus")
                    .sqlSessionTemplate(batchModeSqlSessionTemplate)
                    .build();
        }
    }

XMLConfig
src/main/resources/META-INF/jobs/dbaccess/jobPointAddTasklet.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd
                 http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

        <import resource="classpath:META-INF/spring/job-base-context.xml"/>

        <context:component-scan base-package="com.example.batch.tutorial.dbaccess.tasklet"/>

        <!-- (1) -->
        <mybatis:scan base-package="com.example.batch.tutorial.common.repository" factory-ref="jobSqlSessionFactory"/>

        <!-- (2) -->
        <bean id="reader"
              class="org.mybatis.spring.batch.MyBatisCursorItemReader"
              p:queryId="com.example.batch.tutorial.common.repository.MemberInfoRepository.cursor"
              p:sqlSessionFactory-ref="jobSqlSessionFactory"/>

        <!-- (3) -->
        <bean id="writer" class="org.mybatis.spring.batch.MyBatisBatchItemWriter"
              p:statementId="com.example.batch.tutorial.common.repository.MemberInfoRepository.updatePointAndStatus"
              p:sqlSessionTemplate-ref="batchModeSqlSessionTemplate"/>

    </beans>

表 219. 説明
項番 | 説明
--- | ---
(1) | `Repository`インタフェースをスキャンするための設定を行う。 `basePackages属性/base-package属性`に、`Repository`インタフェースが格納されている基底パッケージを指定する。
(2) | ItemReaderの設定を行う。 `戻り値の型/class属性`に、MyBatisが提供しているSpring連携ライブラリMyBatis-Springから提供されている`org.mybatis.spring.batch.MyBatisCursorItemReader`を指定する。 `MyBatisCursorItemReaderBuilderのqueryIdメソッド/queryId属性`に、MapperXMLファイルに設定するSQLの`namespace`+`id`を指定する。
(3) | ItemWriterの設定を行う。 `戻り値の型/class属性`に、MyBatisが提供しているSpring連携ライブラリMyBatis-Springから提供されている`org.mybatis.spring.batch.MyBatisBatchItemWriter`を指定する。 `MyBatisBatchItemWriterBuilderのstatementIdメソッド/statementId属性`に、MapperXMLファイルに設定するSQLの`namespace`+`id`を指定する。

| チャンクモデルのコンポーネントを利用するTasklet実装 このチュートリアルでは、タスクレットモデルでデータベースアクセスするジョブの作成を容易に実現するために、 チャンクモデルのコンポーネントである`ItemReader`・`ItemWriter`を利用している。 Tasklet実装の中でチャンクモデルの各種コンポーネントを利用するかどうかは、 チャンクモデルのコンポーネントを利用するTasklet実装を参照して適宜判断してほしい。

| ItemReader・ItemWriter以外のデータベースアクセス `ItemReader`・`ItemWriter`以外でデータベースアクセスする方法として、`Mapper`インタフェースを利用する方法がある。 `Mapper`インタフェースを利用するにあたっては、Macchinetta Batch 2.xとして制約を設けているため、 `Mapper`インタフェース(入力)、`Mapper`インタフェース(出力)を参照してほしい。 Taskletの実装例は、タスクレットモデルにおける利用方法(入力)、 タスクレットモデルにおける利用方法(出力)を参照。

###### 9.4.1.3.4. ロジックの実装

ポイント加算処理を行うビジネスロジッククラスを実装する。

以下の作業を実施する。

1. PointAddTaskletクラスの実装

2. ジョブBean定義ファイルの設定

PointAddTaskletクラスの実装

`Tasklet`インタフェースを実装したPointAddTaskletクラスを実装する。

com.example.batch.tutorial.dbaccess.tasklet.PointAddTasklet

    package com.example.batch.tutorial.dbaccess.tasklet;

    import org.springframework.batch.core.StepContribution;
    import org.springframework.batch.core.scope.context.ChunkContext;
    import org.springframework.batch.core.step.tasklet.Tasklet;
    import org.springframework.batch.item.Chunk;
    import org.springframework.batch.item.ItemStreamReader;
    import org.springframework.batch.item.ItemWriter;
    import org.springframework.batch.repeat.RepeatStatus;
    import org.springframework.stereotype.Component;
    import com.example.batch.tutorial.common.dto.MemberInfoDto;

    import jakarta.inject.Inject;
    import java.util.ArrayList;
    import java.util.List;

    @Component // (1)
    public class PointAddTasklet implements Tasklet {

        private static final String TARGET_STATUS = "1"; // (2)

        private static final String INITIAL_STATUS = "0"; // (3)

        private static final String GOLD_MEMBER = "G"; // (4)

        private static final String NORMAL_MEMBER = "N"; // (5)

        private static final int MAX_POINT = 1000000; // (6)

        private static final int CHUNK_SIZE = 10; // (7)

        @Inject // (8)
        ItemStreamReader<MemberInfoDto> reader; // (9)

        @Inject // (8)
        ItemWriter<MemberInfoDto> writer; // (10)

        @Override
        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { // (11)
            MemberInfoDto item = null;

            List<MemberInfoDto> items = new ArrayList<>(CHUNK_SIZE); // (12)

            try {
                reader.open(chunkContext.getStepContext().getStepExecution().getExecutionContext()); // (13)

                while ((item = reader.read()) != null) { // (14)

                    if (TARGET_STATUS.equals(item.getStatus())) {
                        if (GOLD_MEMBER.equals(item.getType())) {
                            item.setPoint(item.getPoint() + 100);
                        } else if (NORMAL_MEMBER.equals(item.getType())) {
                            item.setPoint(item.getPoint() + 10);
                        }

                        if (item.getPoint() > MAX_POINT) {
                            item.setPoint(MAX_POINT);
                        }

                        item.setStatus(INITIAL_STATUS);
                    }

                    items.add(item);

                    if (items.size() == CHUNK_SIZE) { // (15)
                        writer.write(new Chunk(items)); // (16)
                        items.clear();
                    }
                }

                writer.write(new Chunk(items)); // (17)
            } finally {
                reader.close(); // (18)
            }

            return RepeatStatus.FINISHED; // (19)
        }
    }

表 220. 説明
項番 | 説明
--- | ---
(1) | コンポーネントスキャンの対象とするため、`@Component`アノテーションを付与してBean定義を行う。
(2) | 定数として、ポイント加算対象とする商品購入フラグ:1を定義する。 本来、このようなフィールド定数は定数クラスなどに定義し、ロジックに定義することはあまりない。 このチュートリアルでは、便宜上、定数として定義していることを留意すること。(以降の定数も同様)
(3) | 定数として、商品購入フラグの初期値:0を定義する。
(4) | 定数として、会員種別:G(ゴールド会員)を定義する。
(5) | 定数として、会員種別:N(一般会員)を定義する。
(6) | 定数として、ポイントの上限値:1000000を定義する。
(7) | 定数として、まとめて処理する単位(一定件数):10を定義する。
(8) | `@Inject`アノテーションを付与して、`ItemStreamReader/ItemWriter`の実装をインジェクションする。
(9) | データベースアクセスするために`ItemReader`のサブインタフェースである、`ItemStreamReader`として型を定義する。 `ItemStreamReader`はリソースのオープン/クローズを実行する必要がある。
(10) | `ItemWriter`を定義する。 `ItemStreamReader`とは異なり、リソースのオープン/クローズを実行する必要はない。
(11) | 商品購入フラグおよび、会員種別に応じてポイント加算するビジネスロジックを実装する。
(12) | 一定件数分の`item`を格納するためのリストを定義する。
(13) | 入力リソースをオープンする。 このタイミングでSQLが発行される。
(14) | 入力リソース全件を逐次ループ処理する。 `ItemReader#read`は、入力データがすべて読み取り末端に到達した場合、`null`を返却する。
(15) | リストに追加した`item`の数が一定件数に達したかどうかを判定する。 一定件数に達した場合は、(16)でデータベースへ出力し、リストを`clear`する。
(16) | データベースへ出力する。 このタイミングでコミットするわけではないため留意すること。
(17) | 全体の処理件数/一定件数の余り分をデータベースへ出力する。
(18) | リソースをクローズする。 なお、ここでは実装を簡易にするため例外処理を実装していない。例外処理は必要に応じて実装すること。 ここで例外が発生した場合、タスクレット全体のトランザクションがロールバックされ、例外のスタックトレースを出力し、ジョブが異常終了する。
(19) | Taskletの処理が完了したかどうかを返却する。 常に`return RepeatStatus.FINISHED;`と明示する。

ジョブBean定義ファイルの設定

作成したビジネスロジックをジョブとして設定するため、ジョブBean定義ファイルに以下の(1)以降を追記する。

JavaConfig
com.example.batch.tutorial.config.dbaccess.JobPointAddTaskletConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan("com.example.batch.tutorial.dbaccess.tasklet")
    @MapperScan(basePackages = "com.example.batch.tutorial.common.repository", sqlSessionFactoryRef = "jobSqlSessionFactory")
    public class JobPointAddTaskletConfig {

        @Bean
        public MyBatisCursorItemReader<MemberInfoDto> reader(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory) {
            return new MyBatisCursorItemReaderBuilder<MemberInfoDto>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .queryId(
                            "com.example.batch.tutorial.common.repository.MemberInfoRepository.cursor")
                    .build();
        }

        @Bean
        public MyBatisBatchItemWriter<MemberInfoDto> writer(
                @Qualifier("jobSqlSessionFactory") SqlSessionFactory jobSqlSessionFactory,
                SqlSessionTemplate batchModeSqlSessionTemplate) {
            return new MyBatisBatchItemWriterBuilder<MemberInfoDto>()
                    .sqlSessionFactory(jobSqlSessionFactory)
                    .statementId(
                            "com.example.batch.tutorial.common.repository.MemberInfoRepository.updatePointAndStatus")
                    .sqlSessionTemplate(batchModeSqlSessionTemplate)
                    .build();
        }

        // (2)
        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           PointAddTasklet tasklet) {
            return new StepBuilder("jobPointAddTasklet.step01",
                    jobRepository)
                    .tasklet(tasklet, transactionManager) // (3)
                    .build();
        }

        // (1)
        @Bean
        public Job jobPointAddTasklet(JobRepository jobRepository,
                                                 Step step01) {
            return new JobBuilder("jobPointAddTasklet", jobRepository)
                    .start(step01)
                    .build();
        }
    }

XMLConfig
src/main/resources/META-INF/jobs/dbaccess/jobPointAddTasklet.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd
                 http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

        <import resource="classpath:META-INF/spring/job-base-context.xml"/>

        <context:component-scan base-package="com.example.batch.tutorial.dbaccess.tasklet"/>

        <mybatis:scan base-package="com.example.batch.tutorial.common.repository" factory-ref="jobSqlSessionFactory"/>

        <bean id="reader"
              class="org.mybatis.spring.batch.MyBatisCursorItemReader"
              p:queryId="com.example.batch.tutorial.common.repository.MemberInfoRepository.cursor"
              p:sqlSessionFactory-ref="jobSqlSessionFactory"/>

        <bean id="writer" class="org.mybatis.spring.batch.MyBatisBatchItemWriter"
              p:statementId="com.example.batch.tutorial.common.repository.MemberInfoRepository.updatePointAndStatus"
             p:sqlSessionTemplate-ref="batchModeSqlSessionTemplate"/>

        <!-- (1) -->
        <batch:job id="jobPointAddTasklet" job-repository="jobRepository">
            <batch:step id="jobPointAddTasklet.step01"> <!-- (2) -->
                <batch:tasklet transaction-manager="jobTransactionManager"
                               ref="pointAddTasklet"/> <!-- (3) -->
            </batch:step>
        </batch:job>
    </beans>

表 221. 説明
項番 | 説明
--- | ---
(1) | ジョブの設定を行う。 `JobBuilderのコンストラクタの引数name/id属性`は、1つのバッチアプリケーションに含まれる全ジョブの範囲内で一意とする必要がある。 ここでは、タスクレットモデルのジョブ名として`jobPointAddTasklet`を指定する。
(2) | ステップの設定を行う。 `StepBuilderのコンストラクタの引数name/id属性`は、1つのバッチアプリケーションに含まれる全ジョブの範囲内で一意とする必要はないが、障害発生時に追跡しやすくなる等の様々なメリットがあるため一意とする。 特別な理由がない限り、(1)で指定した`JobBuilderのコンストラクタの引数name/id属性`に[step+連番]を付加する形式とする。 ここでは、タスクレットモデルのジョブのステップ名として`jobPointAddTasklet.step01`を指定する。
(3) | タスクレットの設定を行う。 `StepBuilderのtaskletメソッドの引数tasklet/ref属性`に、Taskletの実装クラスのBeanIDである`pointAddTasklet`を指定する。

###### 9.4.1.3.5. ジョブの実行と結果の確認

作成したジョブをSTS上で実行し、結果を確認する。

実行構成からジョブを実行

以下のとおり実行構成を作成し、ジョブを実行する。
実行構成の作成手順はプロジェクトの動作確認を参照。

実行構成の設定値

JavaConfig

- Name: 任意の名称(例: Run DBAccessJob for TaskletModel)

- Mainタブ

  - Project: `macchinetta-batch-tutorial`

  - Main class: `org.springframework.batch.core.launch.support.CommandLineJobRunner`

- Argumentsタブ

  - Program arguments: `com.example.batch.tutorial.config.dbaccess.JobPointAddTaskletConfig jobPointAddTasklet`

XMLConfig

- Name: 任意の名称(例: Run DBAccessJob for TaskletModel)

- Mainタブ

  - Project: `macchinetta-batch-tutorial`

  - Main class: `org.springframework.batch.core.launch.support.CommandLineJobRunner`

- Argumentsタブ

  - Program arguments: `META-INF/jobs/dbaccess/jobPointAddTasklet.xml jobPointAddTasklet`

コンソールログの確認

Console Viewを開き、以下の内容のログが出力されていることを確認する。

- 処理が完了(COMPLETED)し、例外が発生していないこと。

コンソールログ出力例

    (.. omitted)

    [2020/03/10 13:10:12] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddTasklet]] launched with the following parameters: [{jsr_batch_run_id=114}]
    [2020/03/10 13:10:12] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [jobPointAddTasklet.step01]
    [2020/03/10 13:10:12] [main] [o.s.b.c.s.AbstractStep] [INFO ] Step: [jobPointAddTasklet.step01] executed in 94ms
    [2020/03/10 13:10:12] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddTasklet]] completed with the following parameters: [{jsr_batch_run_id=114}] and the following status: [COMPLETED] in 168ms

終了コードの確認

終了コードにより、正常終了したことを確認する。
確認手順はジョブの実行と結果の確認を参照。 終了コード(exit value)が0(正常終了)となっていることを確認する。

図 83. 終了コードの確認
会員情報テーブルの確認

更新前後の会員情報テーブルの内容を比較し、確認内容のとおりとなっていることを確認する。
確認手順はH2 Consoleを使用してデータベースを参照するを参照。

確認内容

  - statusカラム

    - "1"(処理対象)から"0"(初期状態)に更新されていること

  - pointカラム

    - ポイント加算対象について、会員種別に応じたポイントが加算されていること

      - typeカラムが"G"(ゴールド会員)の場合は100ポイント

      - typeカラムが"N"(一般会員)の場合は10ポイント

    - 1,000,000(上限値)を超えたレコードが存在しないこと

更新前後の会員情報テーブルの内容は以下のとおり。

図 84. 更新前後の会員情報テーブルの内容

#### 9.4.2. ファイルアクセスでデータ入出力を行うジョブ

##### 9.4.2.1. 概要

ファイルアクセスでデータ入出力を行うジョブを作成する。

なお、詳細についてはMacchinetta Batch 2.x 開発ガイドラインのファイルアクセスを参照。

作成するアプリケーションの説明の 背景、処理概要、業務仕様を以下に再掲する。

###### 9.4.2.1.1. 背景

とある量販店では、会員に対してポイントカードを発行している。
会員には「ゴールド会員」「一般会員」の会員種別が存在し、会員種別に応じたサービスを提供している。
今回そのサービスの一環として、月内に商品を購入した会員のうち、 会員種別が「ゴールド会員」の場合は100ポイント、「一般会員」の場合は10ポイントを月末に加算することにした。

###### 9.4.2.1.2. 処理概要

会員種別に応じてポイント加算を行うアプリケーションを 月次バッチ処理としてMacchinetta Batch 2.xを使用して実装する。

###### 9.4.2.1.3. 業務仕様

業務仕様は以下のとおり。

- 商品購入フラグが"1"(処理対象)の場合に、会員種別に応じてポイントを加算する

  - 会員種別が"G"(ゴールド会員)の場合は100ポイント、"N"(一般会員)の場合は10ポイント加算する

- 商品購入フラグはポイント加算後に"0"(初期状態)に更新する

- ポイントの上限値は1,000,000ポイントとする

- ポイント加算後に1,000,000ポイントを超えた場合は、1,000,000ポイントに補正する

###### 9.4.2.1.4. ファイル仕様

入出力リソースとなる会員情報ファイルの仕様は以下のとおり。

表 222. 会員情報ファイル(可変長CSV形式)
No | フィールド名 | データ型 | 桁数 | 説明
--- | --- | --- | --- | ---
1 | 会員番号 | 文字列 | 8 | 会員を一意に示す8桁固定の番号を表す。
2 | 会員種別 | 文字列 | 1 | 会員の種別を以下のとおり表す。 "G"(ゴールド会員)、"N"(一般会員)
3 | 商品購入フラグ | 文字列 | 1 | 月内に商品を買ったかどうかを表す。 商品購入で"1"(処理対象)、月次バッチ処理で"0"(初期状態)に更新される。
4 | ポイント | 数値 | 7 | 会員の保有するポイントを表す。 初期値は0。

このチュートリアルではヘッダレコード、フッタレコードは扱わないこととしているため、 ヘッダレコード、フッタレコードの扱いやファイルフォーマットについては、ファイルアクセスを参照。

###### 9.4.2.1.5. ジョブの概要

ここで作成するファイルアクセスでデータ入出力を行うジョブの概要を把握するために、 処理フローおよび処理シーケンスを以下に示す。

処理シーケンスではトランザクション制御の範囲について触れているが、ファイルの場合は擬似的なトランザクション制御を行うことで実現している。 詳細は、非トランザクショナルなデータソースに対する補足を参照。

処理フロー概要

  処理フローの概要を以下に示す。

図 85. ファイルアクセスジョブの処理フロー

チャンクモデルの場合の処理シーケンス

  チャンクモデルの場合の処理シーケンスを説明する。

橙色のオブジェクトは今回実装するクラスを表す。

図 86. チャンクモデルのシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

2. ステップは、入力リソースをオープンする。

3. `FlatFileItemReader`は、member_info(input)ファイルをオープンする。

4. ステップは、出力リソースをオープンする。

5. `FlatFileItemWriter`は、member_info(output)ファイルをオープンする。

   - 入力データがなくなるまで6から16の処理を繰り返す。

   - チャンク単位で、フレームワークトランザクション(擬似的)を開始する。

   - チャンクサイズに達するまで6から12までの処理を繰り返す。

6. ステップは、`FlatFileItemReader`から入力データを1レコード取得する。

7. `FlatFileItemReader`は、member_info(input)ファイルから入力データを1レコード取得する。

8. member_info(input)ファイルは、`FlatFileItemReader`に入力データを返却する。

9. `FlatFileItemReader`は、ステップに入力データを返却する。

10. ステップは、`PointAddItemProcessor`で入力データに対して処理を行う。

11. `PointAddItemProcessor`は、入力データを読み込んでポイント加算処理を行う。

12. `PointAddItemProcessor`は、ステップに処理結果を返却する。

13. ステップは、チャンクサイズ分のデータを`FlatFileItemWriter`で出力する。

14. `FlatFileItemWriter`は、処理結果をバッファリングする。

15. ステップは、フレームワークトランザクション(擬似的)をコミットする。

16. `FlatFileItemWriter`は、フラッシュしてバッファ内のデータをmember_info(output)ファイルに書き込む。

17. ステップは、入力リソースをクローズする。

18. `FlatFileItemReader`は、member_info(input)ファイルをクローズする。

19. ステップは、出力リソースをクローズする。

20. `FlatFileItemWriter`は、member_info(output)ファイルをクローズする。

21. ステップはジョブに終了コード(ここでは正常終了:0)を返却する。

タスクレットモデルの場合の処理シーケンス

  タスクレットモデルの場合の処理シーケンスについて説明する。

橙色のオブジェクトは今回実装するクラスを表す。

図 87. タスクレットモデルのシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

   - ステップはフレームワークトランザクション(擬似的)を開始する。

2. ステップは`PointAddTasklet`を実行する。

3. `PointAddTasklet`は、入力リソースをオープンする。

4. `FlatFileItemReader`は、member_info(input)ファイルをオープンする。

5. `PointAddTasklet`は、出力リソースをオープンする。

6. `FlatFileItemWriter`は、member_info(output)ファイルをオープンする。

   - 入力データがなくなるまで7から13までの処理を繰り返す。

   - 一定件数に達するまで7から11までの処理を繰り返す。

7. `PointAddTasklet`は、`FlatFileItemReader`から入力データを1レコード取得する。

8. `FlatFileItemReader`は、member_info(input)ファイルから入力データを1レコード取得する。

9. member_info(input)ファイルは、`FlatFileItemReader`に入力データを返却する。

10. `FlatFileItemReader`は、タスクレットに入力データを返却する。

11. `PointAddTasklet`は、入力データを読み込んでポイント加算処理を行う。

12. `PointAddTasklet`は、一定件数分のデータを`FlatFileItemWriter`で出力する。

13. `FlatFileItemWriter`は、処理結果をバッファリングする。

14. `PointAddTasklet`は、入力リソースをクローズする。

15. `FlatFileItemReader`は、member_info(input)ファイルをクローズする。

16. `PointAddTasklet`は、出力リソースをクローズする。

17. `PointAddTasklet`は、ステップへ処理終了を返却する。

18. ステップは、フレームワークトランザクション(擬似的)をコミットする。

19. `FlatFileItemWriter`は、フラッシュしてバッファ内のデータをmember_info(output)ファイルに書き込む。

20. `FlatFileItemWriter`は、member_info(output)ファイルをクローズする。

21. ステップはジョブに終了コード(ここでは正常終了:0)を返却する。

以降で、チャンクモデル、タスクレットモデルそれぞれの実装方法を説明する。

- チャンクモデルでの実装

- タスクレットモデルでの実装

##### 9.4.2.2. チャンクモデルでの実装

チャンクモデルでのファイルアクセスでデータ入出力を行うジョブの作成から実行までを以下の手順で実施する。

1. ジョブBean定義ファイルの作成

2. DTOの実装

3. ファイルアクセスの定義

4. ロジックの実装

5. ジョブの実行と結果の確認

###### 9.4.2.2.1. ジョブBean定義ファイルの作成

Bean定義ファイルにて、チャンクモデルでのファイルアクセスでデータ入出力を行うジョブを構成する要素の組み合わせ方を設定する。
ここでは、Bean定義ファイルの枠および共通的な設定のみ記述し、以降の項で各構成要素の設定を行う。

JavaConfig
com.example.batch.tutorial.config.fileaccess.JobPointAddChunkConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class) // (1)
    @ComponentScan("com.example.batch.tutorial.fileaccess.chunk") // (2)
    public class JobPointAddChunkConfig {

    }

XMLConfig
src/main/resources/META-INF/jobs/fileaccess/jobPointAddChunk.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd">

        <!-- (1) -->
        <import resource="classpath:META-INF/spring/job-base-context.xml"/>

        <!-- (2) -->
        <context:component-scan base-package="com.example.batch.tutorial.fileaccess.chunk"/>

    </beans>

表 223. 説明
項番 | 説明
--- | ---
(1) | Macchinetta Batch 2.xを利用する際に、常に必要なBean定義を読み込む設定をインポートする。
(2) | コンポーネントスキャン対象とするベースパッケージの設定を行う。 `basePackages属性/base-package属性`に、使用するコンポーネント(ItemProcessorの実装クラスなど)が格納されているパッケージを指定する。

###### 9.4.2.2.2. DTOの実装

業務データを保持するためのクラスとしてDTOクラスを実装する。
DTOクラスはファイルごとに作成する。

チャンクモデル/タスクレットモデルで共通して利用するため、既に作成している場合は読み飛ばしてよい。

以下のとおり、変換対象クラスとしてDTOクラスを実装する。

com.example.batch.tutorial.common.dto.MemberInfoDto

    package com.example.batch.tutorial.common.dto;

    public class MemberInfoDto {
        private String id; // (1)

        private String type; // (2)

        private String status; // (3)

        private int point; // (4)

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }

        public String getStatus() {
            return status;
        }

        public void setStatus(String status) {
            this.status = status;
        }

        public int getPoint() {
            return point;
        }

        public void setPoint(int point) {
            this.point = point;
        }
    }

表 224. 説明
項番 | 説明
--- | ---
(1) | 会員番号に対応するフィールドとして`id`を定義する。
(2) | 会員種別に対応するフィールドとして`type`を定義する。
(3) | 商品購入フラグに対応するフィールドとして`status`を定義する。
(4) | ポイントに対応するフィールドとして`point`を定義する。

###### 9.4.2.2.3. ファイルアクセスの定義

ファイルアクセスでデータ入出力するためのジョブBean定義ファイルの設定を行う。

ItemReader、ItemWriterの設定として、ジョブBean定義ファイルに以下の(1)以降を追記する。
ここで触れていない設定内容については、可変長レコードの入力 および可変長レコードの出力を参照。

JavaConfig
com.example.batch.tutorial.config.fileaccess.JobPointAddChunkConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan("com.example.batch.tutorial.fileaccess.chunk")
    public class JobPointAddChunkConfig {

        // (1)
        @Bean
        @StepScope
        public FlatFileItemReader<MemberInfoDto> reader(
                @Value("#{jobParameters['inputFile']}") File inputFile) {
            DefaultLineMapper<MemberInfoDto> lineMapper = new DefaultLineMapper<>();
            DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer(); // (3)
            lineTokenizer.setNames("id", "type", "status", "point"); // (4)
            lineTokenizer.setDelimiter(","); // (5)
            lineTokenizer.setQuoteCharacter('"');
            BeanWrapperFieldSetMapper<MemberInfoDto> fieldSetMapper = new BeanWrapperFieldSetMapper<>(); // (6)
            fieldSetMapper.setTargetType(MemberInfoDto.class);
            lineMapper.setLineTokenizer(lineTokenizer);
            lineMapper.setFieldSetMapper(fieldSetMapper);
            return new FlatFileItemReaderBuilder<MemberInfoDto>()
                    .name(ClassUtils.getShortName(FlatFileItemReader.class))
                    .lineMapper(lineMapper)
                    .resource(new FileSystemResource(inputFile)) // (2)
                    .encoding("MS932")
                    .strict(true)
                    .build();
        }

        // (7)
        @Bean
        @StepScope
        public FlatFileItemWriter<MemberInfoDto> writer(
                @Value("#{jobParameters['outputFile']}") File outputFile) {
            BeanWrapperFieldExtractor<MemberInfoDto> fieldExtractor = new BeanWrapperFieldExtractor<>(); // (11)
            fieldExtractor.setNames(new String[] {"id", "type", "status", "point"}); // (12)
            DelimitedLineAggregator<MemberInfoDto> lineAggregator = new DelimitedLineAggregator<>(); // (9)
            lineAggregator.setDelimiter(","); // (10)
            lineAggregator.setFieldExtractor(fieldExtractor);
            return new FlatFileItemWriterBuilder<MemberInfoDto>()
                    .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                    .resource(new FileSystemResource(outputFile)) // (8)
                    .lineAggregator(lineAggregator)
                    .encoding("UTF-8")
                    .lineSeparator("\n")
                    .append(false)
                    .shouldDeleteIfExists(true)
                    .transactional(true)
                    .build();
        }
    }

XMLConfig
src/main/resources/META-INF/jobs/fileaccess/jobPointAddChunk.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd">

        <import resource="classpath:META-INF/spring/job-base-context.xml"/>

        <context:component-scan base-package="com.example.batch.tutorial.fileaccess.chunk"/>

        <!-- (1) (2) -->
        <bean id="reader"
              class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
              p:resource="file:#{jobParameters['inputFile']}"
              p:encoding="UTF-8"
              p:strict="true">
            <property name="lineMapper">
                <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
                    <property name="lineTokenizer"> <!-- (3) -->
                        <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
                              p:names="id,type,status,point"
                              p:delimiter=","
                              p:quoteCharacter='"'/> <!-- (4) (5) -->
                    </property>
                    <property name="fieldSetMapper"> <!-- (6) -->
                        <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
                              p:targetType="com.example.batch.tutorial.common.dto.MemberInfoDto"/>
                    </property>
                </bean>
            </property>
        </bean>

        <!-- (7) (8) -->
        <bean id="writer"
              class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
              p:resource="file:#{jobParameters['outputFile']}"
              p:encoding="UTF-8"
              p:lineSeparator="&#x0A;"
              p:appendAllowed="false"
              p:shouldDeleteIfExists="true"
              p:transactional="true">
            <property name="lineAggregator"> <!-- (9) -->
                <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator"
                      p:delimiter=","> <!-- (10) -->
                    <property name="fieldExtractor"> <!-- (11) -->
                        <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                              p:names="id,type,status,point"/> <!-- (12) -->
                    </property>
                </bean>
            </property>
        </bean>

    </beans>

表 225. 説明
項番 | 説明
--- | ---
(1) | ItemReaderの設定を行う。 `戻り値の型/class属性`に、Spring Batchが提供するフラットファイルを読み込むための`ItemReader`の実装クラスである `org.springframework.batch.item.file.FlatFileItemReader`を指定する。 `@Beanの後に@StepScope/scope属性に、stepスコープ`を指定する。
(2) | `FlatFileItemReaderBuilderのresourceメソッド/resource属性`に入力ファイルのパスを設定する。 パスは直接指定することも可能であるが、ここでは、ジョブ起動時にパラメータで渡すようにするため、 入力ファイルパスのパラメータ名を指定している。
(3) | lineTokenizerの設定を行う。 `インスタンス作成のクラス/class属性`に、Spring Batchが提供する区切り文字を指定してレコードを分割する`LineTokenizer`の実装クラスである `org.springframework.batch.item.file.transform.DelimitedLineTokenizer`を指定する。 CSV形式の一般的書式とされるRFC-4180の仕様に定義されている、エスケープされた改行、区切り文字、囲み文字の読み込みに対応している。
(4) | `DelimitedLineTokenizerのsetNamesメソッド/names属性`に、1レコードの各項目に付与する名前を設定する。 `FieldSetMapper`で使われる`FieldSet`で設定した名前を用いて各項目を取り出すことができるようになる。 レコードの先頭から各名前をカンマ区切りで指定する。
(5) | `DelimitedLineTokenizerのsetDelimiterメソッド/delimiter属性`に、区切り文字としてカンマを指定する。
(6) | `fieldSetMapper`の設定を行う。 今回は文字列や数字など特別な変換処理が不要なため、 `インスタンス作成のクラス/class属性`に、`org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper`を指定する。 `指定する型/targetType属性`には、変換対象クラスとしてDTOの実装で作成したDTOクラスを指定する。 これにより、(4)で設定した各項目の名前と一致するフィールドに値を自動的に設定したインスタンスを生成する。
(7) | ItemWriterの設定を行う。 `戻り値の型/class属性`に、Spring Batchが提供するフラットファイルへ書き込むための`ItemWriter`の実装クラスである `org.springframework.batch.item.file.FlatFileItemWriter`を指定する。 `@Beanの後に@StepScope/scope属性に、stepスコープ`を指定する。 このチュートリアルでは、`FlatFileItemWriterBuilderのappendメソッド/appendAllowed属性`は`false`(追記しない)、`shouldDeleteIfExistsメソッド/shouldDeleteIfExists`属性は`true`(既存ファイルを削除する)を指定して ジョブを何度実行しても新規にファイルが作成されるようにする。 `transactionalメソッド/transactional属性`に、`true`を指定して、擬似的トランザクション制御を有効にする。
(8) | `FlatFileItemWriterBuilderのresourceメソッド/resource属性`に出力ファイルのパスを設定する。 ジョブ起動時にパラメータで渡すようにするため、出力ファイルパスのパラメータ名を指定している。
(9) | `lineAggregator`の設定を行う。 `インスタンス作成のクラス/class属性`に、対象Beanを1レコードへマッピングするための`org.springframework.batch.item.file.transform.LineAggregator`を指定する。 Beanのプロパティとレコード内の各項目とのマッピングは`FieldExtractor`で行う。
(10) | `DelimitedLineAggregatorのsetDelimiterメソッド/delimiter属性`に、区切り文字としてカンマを指定する。
(11) | `fieldExtractor`の設定を行う。 (12)で指定する各項目の名前に(6)で指定したDTOクラスのフィールドと一致する値をマッピングする。 `インスタンス作成のクラス/class属性`に、`org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor`を指定する。
(12) | `BeanWrapperFieldExtractorのsetNamesメソッド/names属性`に、1レコードの各項目に付与する名前を指定する。

| 擬似的トランザクション制御の有効 擬似的トランザクション制御を有効にすると、リソースへの書き込みを遅延し、コミットタイミングで実際に書き出す。 そのため、ファイルへの書き出しまでメモリ内に出力分のデータを保持することになり、取り扱うデータ量が多い場合、メモリ不足でエラーとなる可能性が高くなる。 このチュートリアルで実装するジョブは、取り扱うデータ量が少ないことから擬似的トランザクション制御を有効にしている。 詳細は、非トランザクショナルなデータソースに対する補足を参照。

###### 9.4.2.2.4. ロジックの実装

ポイント加算処理を行うビジネスロジッククラスを実装する。

以下の作業を実施する。

1. PointAddItemProcessorクラスの実装

2. ジョブBean定義ファイルの設定

PointAddItemProcessorクラスの実装

`ItemProcessor`インタフェースを実装したPointAddItemProcessorクラスを実装する。

com.example.batch.tutorial.fileaccess.chunk.PointAddItemProcessor

    package com.example.batch.tutorial.fileaccess.chunk;

    import org.springframework.batch.item.ItemProcessor;
    import org.springframework.stereotype.Component;
    import com.example.batch.tutorial.common.dto.MemberInfoDto;

    @Component // (1)
    public class PointAddItemProcessor implements ItemProcessor<MemberInfoDto, MemberInfoDto> { // (2)

        private static final String TARGET_STATUS = "1"; // (3)

        private static final String INITIAL_STATUS = "0"; // (4)

        private static final String GOLD_MEMBER = "G"; // (5)

        private static final String NORMAL_MEMBER = "N"; // (6)

        private static final int MAX_POINT = 1000000; // (7)

        @Override
        public MemberInfoDto process(MemberInfoDto item) throws Exception { // (8) (9) (10)
            if (TARGET_STATUS.equals(item.getStatus())) {
                if (GOLD_MEMBER.equals(item.getType())) {
                    item.setPoint(item.getPoint() + 100);
                } else if (NORMAL_MEMBER.equals(item.getType())) {
                    item.setPoint(item.getPoint() + 10);
                }

                if (item.getPoint() > MAX_POINT) {
                    item.setPoint(MAX_POINT);
                }

                item.setStatus(INITIAL_STATUS);
            }

            return item;
        }
    }

表 226. 説明
項番 | 説明
--- | ---
(1) | コンポーネントスキャンの対象とするため、`@Component`アノテーションを付与してBean定義を行う。
(2) | 入出力で使用するオブジェクトの型をそれぞれ型引数に指定した`ItemProcessor`インタフェースを実装する。 ここでは、入出力で使用するオブジェクトは共にDTOの実装で作成した`MemberInfoDto`を指定する。
(3) | 定数として、ポイント加算対象とする商品購入フラグ:1を定義する。 本来、このようなフィールド定数は定数クラスなどに定義し、ロジックに定義することはあまりない。 このチュートリアルでは、便宜上、定数として定義していることを留意すること。(以降の定数も同様)
(4) | 定数として、商品購入フラグの初期値:0を定義する。
(5) | 定数として、会員区分:G(ゴールド会員)を定義する。
(6) | 定数として、会員区分:N(一般会員)を定義する。
(7) | 定数として、ポイントの上限値:1000000を定義する。
(8) | 商品購入フラグおよび、会員種別に応じてポイント加算するビジネスロジックを実装する。
(9) | 返り値の型は、このクラスで実装している`ItemProcessor`インタフェースの型引数で指定した 出力オブジェクトの型である`MemberInfoDto`とする。
(10) | 引数として受け取る`item`の型は、 このクラスで実装している`ItemProcessor`インタフェースの型引数で指定した入力オブジェクトの型である`MemberInfoDto`とする。

ジョブBean定義ファイルの設定

作成したビジネスロジックをジョブとして設定するため、ジョブBean定義ファイルに以下の(1)以降を追記する。

JavaConfig
com.example.batch.tutorial.config.fileaccess.JobPointAddChunkConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan("com.example.batch.tutorial.fileaccess.chunk")
    public class JobPointAddChunkConfig {

        @Bean
        @StepScope
        public FlatFileItemReader<MemberInfoDto> reader(
                @Value("#{jobParameters['inputFile']}") File inputFile) {
            DefaultLineMapper<MemberInfoDto> lineMapper = new DefaultLineMapper<>();
            DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
            lineTokenizer.setNames("id", "type", "status", "point");
            lineTokenizer.setDelimiter(",");
            lineTokenizer.setQuoteCharacter('"');
            BeanWrapperFieldSetMapper<MemberInfoDto> fieldSetMapper = new BeanWrapperFieldSetMapper<>();
            fieldSetMapper.setTargetType(MemberInfoDto.class);
            lineMapper.setLineTokenizer(lineTokenizer);
            lineMapper.setFieldSetMapper(fieldSetMapper);
            return new FlatFileItemReaderBuilder<MemberInfoDto>()
                    .name(ClassUtils.getShortName(FlatFileItemReader.class))
                    .lineMapper(lineMapper)
                    .resource(new FileSystemResource(inputFile))
                    .encoding("MS932")
                    .strict(true)
                    .build();
        }

        @Bean
        @StepScope
        public FlatFileItemWriter<MemberInfoDto> writer(
                @Value("#{jobParameters['outputFile']}") File outputFile) {
            DelimitedLineAggregator<MemberInfoDto> lineAggregator = new DelimitedLineAggregator<>();
            lineAggregator.setDelimiter(",");
            BeanWrapperFieldExtractor<MemberInfoDto> fieldExtractor = new BeanWrapperFieldExtractor<>();
            fieldExtractor.setNames(new String[] {"id", "type", "status", "point"});
            lineAggregator.setFieldExtractor(fieldExtractor);
            return new FlatFileItemWriterBuilder<MemberInfoDto>()
                    .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                    .resource(new FileSystemResource(outputFile))
                    .lineAggregator(lineAggregator)
                    .encoding("UTF-8")
                    .lineSeparator("\n")
                    .append(false)
                    .shouldDeleteIfExists(true)
                    .transactional(true)
                    .build();
        }

        // (1)
        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           ItemReader<MemberInfoDto> reader,
                           PointAddItemProcessor processor,
                           ItemWriter<MemberInfoDto> writer) {
            return new StepBuilder("jobPointAddChunk.step01",
                    jobRepository)
                    .<MemberInfoDto, MemberInfoDto>chunk(10, // (3)
                            transactionManager)
                    .reader(reader)
                    .processor(processor)
                    .writer(writer)
                    .build();
        }

        // (1)
        @Bean
        public Job jobPointAddChunk(JobRepository jobRepository,
                                                 Step step01) {
            return new JobBuilder("jobPointAddChunk", jobRepository)
                    .start(step01)
                    .build();
        }
    }

XMLConfig
src/main/resources/META-INF/jobs/fileaccess/jobPointAddChunk.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd">

        <import resource="classpath:META-INF/spring/job-base-context.xml"/>

        <context:component-scan base-package="com.example.batch.tutorial.fileaccess.chunk"/>

        <bean id="reader"
              class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
              p:resource="file:#{jobParameters['inputFile']}"
              p:encoding="UTF-8"
              p:strict="true">
            <property name="lineMapper">
                <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
                    <property name="lineTokenizer">
                        <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
                              p:names="id,type,status,point"
                              p:delimiter=","
                              p:quoteCharacter='"'/>
                    </property>
                    <property name="fieldSetMapper">
                        <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
                              p:targetType="com.example.batch.tutorial.common.dto.MemberInfoDto"/>
                    </property>
                </bean>
            </property>
        </bean>

        <bean id="writer"
              class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
              p:resource="file:#{jobParameters['outputFile']}"
              p:encoding="UTF-8"
              p:lineSeparator="&#x0A;"
              p:appendAllowed="false"
              p:shouldDeleteIfExists="true"
              p:transactional="true">
            <property name="lineAggregator">
                <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator"
                      p:delimiter=",">
                    <property name="fieldExtractor">
                        <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                              p:names="id,type,status,point"/>
                    </property>
                </bean>
            </property>
        </bean>

        <!-- (1) -->
        <batch:job id="jobPointAddChunk" job-repository="jobRepository">
            <batch:step id="jobPointAddChunk.step01"> <!-- (2) -->
                <batch:tasklet transaction-manager="jobTransactionManager">
                    <batch:chunk reader="reader"
                                 processor="pointAddItemProcessor"
                                 writer="writer" commit-interval="10"/> <!-- (3) -->
                </batch:tasklet>
            </batch:step>
        </batch:job>
    </beans>

表 227. 説明
項番 | 説明
--- | ---
(1) | ジョブの設定を行う。 `JobBuilderのコンストラクタの引数name/id属性`は、1つのバッチアプリケーションに含まれる全ジョブの範囲内で一意とする必要がある。 ここでは、チャンクモデルのジョブ名として`jobPointAddChunk`を指定する。
(2) | ステップの設定を行う。 `StepBuilderのコンストラクタの引数name/id属性`は、1つのバッチアプリケーションに含まれる全ジョブの範囲内で一意とする必要はないが、障害発生時に追跡しやすくなる等の様々なメリットがあるため一意とする。 特別な理由がない限り、(1)で指定した`JobBuilderのコンストラクタの引数name/id属性`に[step+連番]を付加する形式とする。 ここでは、チャンクモデルのジョブのステップ名として`jobPointAddChunk.step01`を指定する。
(3) | チャンクモデルジョブの設定を行う。 ``StepBuilderのreaderメソッド、writerメソッド/reader、writerそれぞれの属性`に、前項までに定義した`ItemReader`、`ItemWriter`のBeanIDを指定する。 `StepBuilderのprocessorメソッド/processor属性`に、ItemProcessorの実装クラスのBeanIDである`pointAddItemProcessor`を指定する。 `chunkメソッドの引数chunkSize/commit-interval属性`に、1チャンクあたりの入力データ件数を10件として設定する。

| `chunk`メソッドの引数`chunkSize/commit-interval`のチューニング `chunkメソッドの引数chunkSize/commit-interval`はチャンクモデルジョブにおける、性能上のチューニングポイントである。 このチュートリアルでは10件としているが、利用できるマシンリソースやジョブの特性によって適切な件数は異なる。 複数のリソースにアクセスしてデータを加工するジョブであれば10件から100件程度で処理スループットが頭打ちになることもある。 一方、入出力リソースが1:1対応しておりデータを移し替える程度のジョブであれば5,000件や10,000件でも処理スループットが伸びることがある。 ジョブ実装時の`chunkメソッドの引数chunkSize/commit-interval`は100件程度で仮置きしておき、 その後に実施した性能測定の結果に応じてジョブごとにチューニングするとよい。

###### 9.4.2.2.5. ジョブの実行と結果の確認

作成したジョブをSTS上で実行し、結果を確認する。

実行構成からジョブを実行

以下のとおり実行構成を作成し、ジョブを実行する。
実行構成の作成手順は動作確認を参照。

ここでは、正常系データを利用してジョブを実行する。
Argumentsタブに入出力ファイルのパラメータを引数として追加する。

実行構成の設定値

JavaConfig

- Name: 任意の名称(例: Run FileAccessJob for ChunkModel)

- Mainタブ

  - Project: `macchinetta-batch-tutorial`

  - Main class: `org.springframework.batch.core.launch.support.CommandLineJobRunner`

- Argumentsタブ

  - Program arguments: `com.example.batch.tutorial.config.fileaccess.JobPointAddChunkConfig jobPointAddChunk inputFile=files/input/input-member-info-data.csv outputFile=files/output/output-member-info-data.csv`

XMLConfig

- Name: 任意の名称(例: Run FileAccessJob for ChunkModel)

- Mainタブ

  - Project: `macchinetta-batch-tutorial`

  - Main class: `org.springframework.batch.core.launch.support.CommandLineJobRunner`

- Argumentsタブ

  - Program arguments: `META-INF/jobs/fileaccess/jobPointAddChunk.xml jobPointAddChunk inputFile=files/input/input-member-info-data.csv outputFile=files/output/output-member-info-data.csv`

コンソールログの確認

Console Viewを開き、以下の内容のログが出力されていることを確認する。

- 処理が完了(COMPLETED)し、例外が発生していないこと。

コンソールログ出力例

    (.. omitted)

    [2020/03/10 13:11:26] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddChunk]] launched with the following parameters: [{inputFile=files/input/input-member-info-data.csv, outputFile=files/output/output-member-info-data.csv, jsr_batch_run_id=116}]
    [2020/03/10 13:11:26] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [jobPointAddChunk.step01]
    [2020/03/10 13:11:26] [main] [o.s.b.c.s.AbstractStep] [INFO ] Step: [jobPointAddChunk.step01] executed in 388ms
    [2020/03/10 13:11:26] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddChunk]] completed with the following parameters: [{inputFile=files/input/input-member-info-data.csv, outputFile=files/output/output-member-info-data.csv, jsr_batch_run_id=116}] and the following status: [COMPLETED] in 461ms

終了コードの確認

終了コードにより、正常終了したことを確認する。
確認手順はジョブの実行と結果の確認を参照。 終了コード(exit value)が0(正常終了)となっていることを確認する。

図 88. 終了コードの確認
会員情報ファイルの確認

会員情報ファイルの入出力内容を比較し、確認内容のとおりとなっていることを確認する。

確認内容

  - 出力ディレクトリに会員情報ファイルが出力されていること

    - 出力ファイル: files/output/output-member-info-data.csv

  - statusフィールド

    - "1"(処理対象)から"0"(初期状態)に更新されていること

  - pointフィールド

    - ポイント加算対象について、会員種別に応じたポイントが加算されていること

      - typeフィールドが"G"(ゴールド会員)の場合は100ポイント

      - typeフィールドが"N"(一般会員)の場合は10ポイント

    - 1,000,000(上限値)を超えたレコードが存在しないこと

会員情報ファイルの入出力内容は以下のとおり。
ファイルのフィールドはid(会員番号)、type(会員種別)、status(商品購入フラグ)、point(ポイント)の順で出力される。

図 89. 会員情報ファイルの入出力内容

##### 9.4.2.3. タスクレットモデルでの実装

タスクレットモデルでのファイルアクセスでデータ入出力を行うジョブの作成から実行までを以下の手順で実施する。

1. ジョブBean定義ファイルの作成

2. DTOの実装

3. ファイルアクセスの定義

4. ロジックの実装

5. ジョブの実行と結果の確認

###### 9.4.2.3.1. ジョブBean定義ファイルの作成

Bean定義ファイルにて、タスクレットモデルでのファイルアクセスでデータ入出力を行うジョブを構成する要素の組み合わせ方を設定する。
ここでは、Bean定義ファイルの枠および共通的な設定のみ記述し、以降の項で各構成要素の設定を行う。

JavaConfig
com.example.batch.tutorial.config.fileaccess.JobPointAddTaskletConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class) // (1)
    @ComponentScan(value = "com.example.batch.tutorial.fileaccess.tasklet", scopedProxy = ScopedProxyMode.TARGET_CLASS) // (2)
    public class JobPointAddTaskletConfig {

    }

XMLConfig
src/main/resources/META-INF/jobs/fileaccess/jobPointAddTasklet.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd">

        <!-- (1) -->
        <import resource="classpath:META-INF/spring/job-base-context.xml"/>

        <!-- (2) -->
        <context:component-scan base-package="com.example.batch.tutorial.fileaccess.tasklet"/>

    </beans>

表 228. 説明
項番 | 説明
--- | ---
(1) | Macchinetta Batch 2.xを利用する際に、常に必要なBean定義を読み込む設定をインポートする。
(2) | `basePackages属性/base-package属性`に、使用するコンポーネント(Taskletの実装クラスなど)が格納されているパッケージを指定する。

###### 9.4.2.3.2. DTOの実装

業務データを保持するためのクラスとしてDTOクラスを実装する。
DTOクラスはファイルごとに作成する。

チャンクモデル/タスクレットモデルで共通して利用するため、既に作成している場合は読み飛ばしてよい。

以下のとおり、変換対象クラスとしてDTOクラスを実装する。

com.example.batch.tutorial.common.dto.MemberInfoDto

    package com.example.batch.tutorial.common.dto;

    public class MemberInfoDto {
        private String id; // (1)

        private String type; // (2)

        private String status; // (3)

        private int point; // (4)

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }

        public String getStatus() {
            return status;
        }

        public void setStatus(String status) {
            this.status = status;
        }

        public int getPoint() {
            return point;
        }

        public void setPoint(int point) {
            this.point = point;
        }
    }

表 229. 説明
項番 | 説明
--- | ---
(1) | 会員番号に対応するフィールドとして`id`を定義する。
(2) | 会員種別に対応するフィールドとして`type`を定義する。
(3) | 商品購入フラグに対応するフィールドとして`status`を定義する。
(4) | ポイントに対応するフィールドとして`point`を定義する。

###### 9.4.2.3.3. ファイルアクセスの定義

ファイルアクセスでデータ入出力するためのジョブBean定義ファイルの設定を行う。

ItemReader、ItemWriterの設定として、ジョブBean定義ファイルに以下の(1)以降を追記する。
ここで触れていない設定内容については、可変長レコードの入力 および可変長レコードの出力を参照。

JavaConfig
com.example.batch.tutorial.config.fileaccess.JobPointAddTaskletConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan(value = "com.example.batch.tutorial.fileaccess.tasklet", scopedProxy = ScopedProxyMode.TARGET_CLASS)
    public class JobPointAddTaskletConfig {

        // (1)
        @Bean
        @StepScope
        public FlatFileItemReader<MemberInfoDto> reader(
                @Value("#{jobParameters['inputFile']}") File inputFile) {  // (2)
            DefaultLineMapper<MemberInfoDto> lineMapper = new DefaultLineMapper<>();
            DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer(); // (3)
            lineTokenizer.setNames("id", "type", "status", "point"); // (4)
            lineTokenizer.setDelimiter(","); // (5)
            lineTokenizer.setQuoteCharacter('"');
            BeanWrapperFieldSetMapper<MemberInfoDto> fieldSetMapper = new BeanWrapperFieldSetMapper<>(); // (6)
            fieldSetMapper.setTargetType(MemberInfoDto.class);
            lineMapper.setLineTokenizer(lineTokenizer);
            lineMapper.setFieldSetMapper(fieldSetMapper);
            return new FlatFileItemReaderBuilder<MemberInfoDto>()
                    .name(ClassUtils.getShortName(FlatFileItemReader.class))
                    .lineMapper(lineMapper)
                    .resource(new FileSystemResource(inputFile))
                    .encoding("MS932")
                    .strict(true)
                    .build();
        }

        // (7)
        @Bean
        @StepScope
        public FlatFileItemWriter<MemberInfoDto> writer(
                @Value("#{jobParameters['outputFile']}") File outputFile) { // (8)
            DelimitedLineAggregator<MemberInfoDto> lineAggregator = new DelimitedLineAggregator<>(); // (9)
            lineAggregator.setDelimiter(","); // (10)
            BeanWrapperFieldExtractor<MemberInfoDto> fieldExtractor = new BeanWrapperFieldExtractor<>(); // (11)
            fieldExtractor.setNames(new String[] {"id", "type", "status", "point"}); // (12)
            lineAggregator.setFieldExtractor(fieldExtractor);
            return new FlatFileItemWriterBuilder<MemberInfoDto>()
                    .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                    .resource(new FileSystemResource(outputFile))
                    .lineAggregator(lineAggregator)
                    .encoding("UTF-8")
                    .lineSeparator("\n")
                    .append(false)
                    .shouldDeleteIfExists(true)
                    .transactional(true)
                    .build();
        }
    }

XMLConfig
src/main/resources/META-INF/jobs/fileaccess/jobPointAddTasklet.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd">

        <import resource="classpath:META-INF/spring/job-base-context.xml"/>

        <context:component-scan base-package="com.example.batch.tutorial.fileaccess.tasklet"/>

        <!-- (1) (2) -->
        <bean id="reader"
              class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
              p:resource="file:#{jobParameters['inputFile']}"
              p:encoding="UTF-8"
              p:strict="true">
            <property name="lineMapper">
                <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
                    <property name="lineTokenizer"> <!-- (3) -->
                        <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
                              p:names="id,type,status,point"
                              p:delimiter=","
                              p:quoteCharacter='"'/> <!-- (4) (5) -->
                    </property>
                    <property name="fieldSetMapper"> <!-- (6) -->
                        <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
                              p:targetType="com.example.batch.tutorial.common.dto.MemberInfoDto"/>
                    </property>
                </bean>
            </property>
        </bean>

        <!-- (7) (8) -->
        <bean id="writer"
              class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
              p:resource="file:#{jobParameters['outputFile']}"
              p:encoding="UTF-8"
              p:lineSeparator="&#x0A;"
              p:appendAllowed="false"
              p:shouldDeleteIfExists="true"
              p:transactional="true">
            <property name="lineAggregator"> <!-- (9) -->
                <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator"
                      p:delimiter=","> <!-- (10) -->
                    <property name="fieldExtractor"> <!-- (11) -->
                        <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                              p:names="id,type,status,point"/> <!-- (12) -->
                    </property>
                </bean>
            </property>
        </bean>

    </beans>

表 230. 説明
項番 | 説明
--- | ---
(1) | ItemReaderの設定を行う。 `戻り値の型/class属性`に、Spring Batchが提供するフラットファイルを読み込むための`ItemReader`の実装クラスである `org.springframework.batch.item.file.FlatFileItemReader`を指定する。 `@Beanの後に@StepScope/scope属性に、stepスコープ`を指定する。
(2) | `FlatFileItemReaderBuilderのresourceメソッド/resource属性`に入力ファイルのパスを設定する。 パスは直接指定することも可能であるが、ここでは、ジョブ起動時にパラメータで渡すようにするため、 入力ファイルパスのパラメータ名を指定している。
(3) | lineTokenizerの設定を行う。 `インスタンス作成のクラス/class属性`に、Spring Batchが提供する区切り文字を指定してレコードを分割する`LineTokenizer`の実装クラスである `org.springframework.batch.item.file.transform.DelimitedLineTokenizer`を指定する。 CSV形式の一般的書式とされるRFC-4180の仕様に定義されている、エスケープされた改行、区切り文字、囲み文字の読み込みに対応している。
(4) | `DelimitedLineTokenizerのsetNamesメソッド/names属性`に、1レコードの各項目に付与する名前を設定する。 `FieldSetMapper`で使われる`FieldSet`で設定した名前を用いて各項目を取り出すことができるようになる。 レコードの先頭から各名前をカンマ区切りで指定する。
(5) | `DelimitedLineTokenizerのsetDelimiterメソッド/delimiter属性`に、区切り文字としてカンマを指定する。
(6) | `fieldSetMapper`の設定を行う。 今回は文字列や数字など特別な変換処理が不要なため、 `インスタンス作成のクラス/class属性`に、`org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper`を指定する。 `指定する型/targetType属性`には、変換対象クラスとしてDTOの実装で作成したDTOクラスを指定する。 これにより、(4)で設定した各項目の名前と一致するフィールドに値を自動的に設定したインスタンスを生成する。
(7) | ItemWriterの設定を行う。 `戻り値の型/class属性`に、Spring Batchが提供するフラットファイルへ書き込むための`ItemWriter`の実装クラスである `org.springframework.batch.item.file.FlatFileItemWriter`を指定する。 `@Beanの後に@StepScope/scope属性に、stepスコープ`を指定する。 このチュートリアルでは、`FlatFileItemWriterBuilderのappendメソッド/appendAllowed属性`は`false`(追記しない)、`shouldDeleteIfExistsメソッド/shouldDeleteIfExists`属性は`true`(既存ファイルを削除する)を指定して ジョブを何度実行しても新規にファイルが作成されるようにする。 `transactionalメソッド/transactional属性`に、`true`を指定して、擬似的トランザクション制御を有効にする。
(8) | `FlatFileItemWriterBuilderのresourceメソッド/resource属性`に出力ファイルのパスを設定する。 ジョブ起動時にパラメータで渡すようにするため、出力ファイルパスのパラメータ名を指定している。
(9) | `lineAggregator`の設定を行う。 `インスタンス作成のクラス/class属性`に、対象Beanを1レコードへマッピングするための`org.springframework.batch.item.file.transform.LineAggregator`を指定する。 Beanのプロパティとレコード内の各項目とのマッピングは`FieldExtractor`で行う。
(10) | `DelimitedLineAggregatorのsetDelimiterメソッド/delimiter属性`に、区切り文字としてカンマを指定する。
(11) | `fieldExtractor`の設定を行う。 (12)で指定する各項目の名前に(6)で指定したDTOクラスのフィールドと一致する値をマッピングする。 `インスタンス作成のクラス/class属性`に、`org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor`を指定する。
(12) | `BeanWrapperFieldExtractorのsetNamesメソッド/names属性`に、1レコードの各項目に付与する名前を設定する。

| チャンクモデルのコンポーネントを利用するTasklet実装 このチュートリアルでは、タスクレットモデルでファイルアクセスするジョブの作成を容易に実現するために、 チャンクモデルのコンポーネントである`ItemReader`・`ItemWriter`を利用している。 Tasklet実装の中でチャンクモデルの各種コンポーネントを利用するかどうかは、 チャンクモデルのコンポーネントを利用するTasklet実装を参照して適宜判断してほしい。 ただし、タスクレットモデルでファイルアクセスする場合は`ItemReader`・`ItemWriter`の実装クラスを利用するとよい。

###### 9.4.2.3.4. ロジックの実装

ポイント加算処理を行うビジネスロジッククラスを実装する。

以下の作業を実施する。

1. PointAddTaskletクラスの実装

2. ジョブBean定義ファイルの設定

PointAddTaskletクラスの実装

`Tasklet`インタフェースを実装したPointAddTaskletクラスを作成する。

com.example.batch.tutorial.fileaccess.tasklet.PointAddTasklet

    package com.example.batch.tutorial.fileaccess.tasklet;

    import org.springframework.batch.core.StepContribution;
    import org.springframework.batch.core.scope.context.ChunkContext;
    import org.springframework.batch.core.step.tasklet.Tasklet;
    import org.springframework.batch.item.Chunk;
    import org.springframework.batch.item.ItemStreamException;
    import org.springframework.batch.item.ItemStreamReader;
    import org.springframework.batch.item.ItemStreamWriter;
    import org.springframework.batch.repeat.RepeatStatus;
    import org.springframework.context.annotation.Scope;
    import org.springframework.stereotype.Component;
    import com.example.batch.tutorial.common.dto.MemberInfoDto;

    import jakarta.inject.Inject;
    import java.util.ArrayList;
    import java.util.List;

    @Component // (1)
    @Scope("step") // (2)
    public class PointAddTasklet implements Tasklet {

        private static final String TARGET_STATUS = "1"; // (3)

        private static final String INITIAL_STATUS = "0"; // (4)

        private static final String GOLD_MEMBER = "G"; // (5)

        private static final String NORMAL_MEMBER = "N"; // (6)

        private static final int MAX_POINT = 1000000; // (7)

        private static final int CHUNK_SIZE = 10; // (8)

        @Inject // (9)
        ItemStreamReader<MemberInfoDto> reader; // (10)

        @Inject // (9)
        ItemStreamWriter<MemberInfoDto> writer; // (11)

        @Override
        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { // (12)
            MemberInfoDto item = null;

            List<MemberInfoDto> items = new ArrayList<>(CHUNK_SIZE); // (13)
            try {

                reader.open(chunkContext.getStepContext().getStepExecution().getExecutionContext()); // (14)
                writer.open(chunkContext.getStepContext().getStepExecution().getExecutionContext()); // (14)

                while ((item = reader.read()) != null) { // (15)

                    if (TARGET_STATUS.equals(item.getStatus())) {
                        if (GOLD_MEMBER.equals(item.getType())) {
                            item.setPoint(item.getPoint() + 100);
                        } else if (NORMAL_MEMBER.equals(item.getType())) {
                            item.setPoint(item.getPoint() + 10);
                        }

                        if (item.getPoint() > MAX_POINT) {
                            item.setPoint(MAX_POINT);
                        }

                        item.setStatus(INITIAL_STATUS);
                    }

                    items.add(item);

                    if (items.size() == CHUNK_SIZE) { // (16)
                        writer.write(new Chunk(items)); // (17)
                        items.clear();
                    }
                }

                writer.write(new Chunk(items)); // (18)
            } finally {
                try {
                    reader.close(); // (19)
                } catch (ItemStreamException e) {
                    // do nothing.
                }
                try {
                    writer.close(); // (19)
                } catch (ItemStreamException e) {
                    // do nothing.
                }
            }

            return RepeatStatus.FINISHED; // (20)
        }
    }

表 231. 説明
項番 | 説明
--- | ---
(1) | コンポーネントスキャンの対象とするため、`@Component`アノテーションを付与してBean定義を行う。
(2) | クラスに@Scopeアノテーションを付与して`step`スコープを指定する。
(3) | 定数として、ポイント加算対象とする商品購入フラグ:1を定義する。 本来、このようなフィールド定数は定数クラスなどに定義し、ロジックに定義することはあまりない。 このチュートリアルでは、便宜上、定数として定義していることを留意すること。(以降の定数も同様)
(4) | 定数として、商品購入フラグの初期値:0を定義する。
(5) | 定数として、会員種別:G(ゴールド会員)を定義する。
(6) | 定数として、会員種別:N(一般会員)を定義する。
(7) | 定数として、ポイントの上限値:1000000を定義する。
(8) | 定数として、まとめて処理する単位(一定件数):10を定義する。
(9) | `@Inject`アノテーションを付与して、`ItemStreamReader/ItemStreamWriter`の実装をインジェクションする。
(10) | ファイルアクセスするために`ItemReader`のサブインタフェースである、`ItemStreamReader`として型を定義する。 `ItemStreamReader`はリソースのオープン/クローズを実行する必要がある。
(11) | ファイルアクセスするために`ItemWriter`のサブインタフェースである、`ItemStreamWriter`として型を定義する。 `ItemStreamWriter`はリソースのオープン/クローズを実行する必要がある。
(12) | 商品購入フラグおよび、会員種別に応じてポイント加算するビジネスロジックを実装する。
(13) | 一定件数分の`item`を格納するためのリストを定義する。
(14) | 入出力リソースをオープンする。
(15) | 入力リソース全件を逐次ループ処理する。 `ItemReader#read`は、入力データがすべて読み取り末端に到達した場合、`null`を返却する。
(16) | リストに追加した`item`の数が一定件数に達したかどうかを判定する。 一定件数に達した場合は、(17)でファイルへ出力し、リストを`clear`する。
(17) | 処理したデータをファイルへ出力する。
(18) | 全体の処理件数/一定件数の余り分をファイルへ出力する。
(19) | 入出力リソースをクローズする。
(20) | Taskletの処理が完了したかどうかを返却する。 常に`return RepeatStatus.FINISHED;`と明示する。

ジョブBean定義ファイルの設定

作成したビジネスロジックをジョブとして設定するため、ジョブBean定義ファイルに以下の(1)以降を追記する。

JavaConfig
com.example.batch.tutorial.config.fileaccess.JobPointAddTaskletConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan(value = "com.example.batch.tutorial.fileaccess.tasklet", scopedProxy = ScopedProxyMode.TARGET_CLASS)
    public class JobPointAddTaskletConfig {

        @Bean
        @StepScope
        public FlatFileItemReader<MemberInfoDto> reader(
                @Value("#{jobParameters['inputFile']}") File inputFile) {
            DefaultLineMapper<MemberInfoDto> lineMapper = new DefaultLineMapper<>();
            DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
            lineTokenizer.setNames("id", "type", "status", "point");
            lineTokenizer.setDelimiter(",");
            lineTokenizer.setQuoteCharacter('"');
            BeanWrapperFieldSetMapper<MemberInfoDto> fieldSetMapper = new BeanWrapperFieldSetMapper<>();
            fieldSetMapper.setTargetType(MemberInfoDto.class);
            lineMapper.setLineTokenizer(lineTokenizer);
            lineMapper.setFieldSetMapper(fieldSetMapper);
            return new FlatFileItemReaderBuilder<MemberInfoDto>()
                    .name(ClassUtils.getShortName(FlatFileItemReader.class))
                    .lineMapper(lineMapper)
                    .resource(new FileSystemResource(inputFile))
                    .encoding("MS932")
                    .strict(true)
                    .build();
        }

        @Bean
        @StepScope
        public FlatFileItemWriter<MemberInfoDto> writer(
                @Value("#{jobParameters['outputFile']}") File outputFile) {
            DelimitedLineAggregator<MemberInfoDto> lineAggregator = new DelimitedLineAggregator<>();
            lineAggregator.setDelimiter(",");
            BeanWrapperFieldExtractor<MemberInfoDto> fieldExtractor = new BeanWrapperFieldExtractor<>();
            fieldExtractor.setNames(new String[] {"id", "type", "status", "point"});
            lineAggregator.setFieldExtractor(fieldExtractor);
            return new FlatFileItemWriterBuilder<MemberInfoDto>()
                    .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                    .resource(new FileSystemResource(outputFile))
                    .lineAggregator(lineAggregator)
                    .encoding("UTF-8")
                    .lineSeparator("\n")
                    .append(false)
                    .shouldDeleteIfExists(true)
                    .transactional(true)
                    .build();
        }

        // (2)
        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           PointAddTasklet tasklet) {
            return new StepBuilder("jobPointAddTasklet.step01",
                    jobRepository)
                    .tasklet(tasklet, transactionManager) // (3)
                    .build();
        }

        // (1)
        @Bean
        public Job jobPointAddTasklet(JobRepository jobRepository,
                                                 Step step01) {
            return new JobBuilder("jobPointAddTasklet", jobRepository)
                    .start(step01)
                    .build();
        }
    }

XMLConfig
src/main/resources/META-INF/jobs/fileaccess/jobPointAddTasklet.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd">

        <import resource="classpath:META-INF/spring/job-base-context.xml"/>

        <context:component-scan base-package="com.example.batch.tutorial.fileaccess.tasklet"/>

        <bean id="reader"
              class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
              p:resource="file:#{jobParameters['inputFile']}"
              p:encoding="UTF-8"
              p:strict="true">
            <property name="lineMapper">
                <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
                    <property name="lineTokenizer">
                        <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
                              p:names="id,type,status,point"
                              p:delimiter=","
                              p:quoteCharacter='"'/>
                    </property>
                    <property name="fieldSetMapper">
                        <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
                              p:targetType="com.example.batch.tutorial.common.dto.MemberInfoDto"/>
                    </property>
                </bean>
            </property>
        </bean>

        <bean id="writer"
              class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
              p:resource="file:#{jobParameters['outputFile']}"
              p:encoding="UTF-8"
              p:lineSeparator="&#x0A;"
              p:appendAllowed="false"
              p:shouldDeleteIfExists="true"
              p:transactional="true">
            <property name="lineAggregator">
                <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator"
                      p:delimiter=",">
                    <property name="fieldExtractor">
                        <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                              p:names="id,type,status,point"/>
                    </property>
                </bean>
            </property>
        </bean>

        <!-- (1) -->
        <batch:job id="jobPointAddTasklet" job-repository="jobRepository">
            <batch:step id="jobPointAddTasklet.step01"> <!-- (2) -->
                <batch:tasklet transaction-manager="jobTransactionManager"
                               ref="pointAddTasklet"/> <!-- (3) -->
            </batch:step>
        </batch:job>
    </beans>

表 232. 説明
項番 | 説明
--- | ---
(1) | ジョブの設定を行う。 `JobBuilderのコンストラクタの引数name/id属性`は、1つのバッチアプリケーションに含まれる全ジョブの範囲内で一意とする必要がある。 ここでは、タスクレットモデルのジョブ名として`jobPointAddTasklet`を指定する。
(2) | ステップの設定を行う。 `StepBuilderのコンストラクタの引数name/id属性`は、1つのバッチアプリケーションに含まれる全ジョブの範囲内で一意とする必要はないが、障害発生時に追跡しやすくなる等の様々なメリットがあるため一意とする。 特別な理由がない限り、(1)で指定した`JobBuilderのコンストラクタの引数name/id属性`に[step+連番]を付加する形式とする。 ここでは、タスクレットモデルのジョブのステップ名として`jobPointAddTasklet.step01`を指定する。
(3) | タスクレットの設定を行う。 `StepBuilderのtaskletメソッド/ref属性`に、Taskletの実装クラスのBeanIDである`pointAddTasklet`を指定する。

###### 9.4.2.3.5. ジョブの実行と結果の確認

作成したジョブをSTS上で実行し、結果を確認する。

実行構成からジョブを実行

以下のとおり実行構成を作成し、ジョブを実行する。
実行構成の作成手順は動作確認を参照。

ここでは、正常系データを利用してジョブを実行する。
Argumentsタブに入出力ファイルのパラメータを引数として追加する。

実行構成の設定値

JavaConfig

- Name: 任意の名称(例: Run FileAccessJob for TaskletModel)

- Mainタブ

  - Project: `macchinetta-batch-tutorial`

  - Main class: `org.springframework.batch.core.launch.support.CommandLineJobRunner`

- Argumentsタブ

  - Program arguments: `com.example.batch.tutorial.config.fileaccess.JobPointAddTaskletConfig jobPointAddTasklet inputFile=files/input/input-member-info-data.csv outputFile=files/output/output-member-info-data.csv`

XMLConfig

- Name: 任意の名称(例: Run FileAccessJob for TaskletModel)

- Mainタブ

  - Project: `macchinetta-batch-tutorial`

  - Main class: `org.springframework.batch.core.launch.support.CommandLineJobRunner`

- Argumentsタブ

  - Program arguments: `META-INF/jobs/fileaccess/jobPointAddTasklet.xml jobPointAddTasklet inputFile=files/input/input-member-info-data.csv outputFile=files/output/output-member-info-data.csv`

コンソールログの確認

Console Viewを開き、以下の内容のログが出力されていることを確認する。

- 処理が完了(COMPLETED)し、例外が発生していないこと。

コンソールログ出力例

    (.. omitted)

    [2020/03/10 13:18:03] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddTasklet]] launched with the following parameters: [{inputFile=files/input/input-member-info-data.csv, outputFile=files/output/output-member-info-data.csv, jsr_batch_run_id=118}]
    [2020/03/10 13:18:03] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [jobPointAddTasklet.step01]
    [2020/03/10 13:18:03] [main] [o.s.b.c.s.AbstractStep] [INFO ] Step: [jobPointAddTasklet.step01] executed in 161ms
    [2020/03/10 13:18:03] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddTasklet]] completed with the following parameters: [{inputFile=files/input/input-member-info-data.csv, outputFile=files/output/output-member-info-data.csv, jsr_batch_run_id=118}] and the following status: [COMPLETED] in 229ms

終了コードの確認

終了コードにより、正常終了したことを確認する。
確認手順はジョブの実行と結果の確認を参照。 終了コード(exit value)が0(正常終了)となっていることを確認する。

図 90. 終了コードの確認
会員情報ファイルの確認

会員情報ファイルの入出力内容を比較し、確認内容のとおりとなっていることを確認する。

確認内容

  - 出力ディレクトリに会員情報ファイルが出力されていること

    - 出力ファイル: files/output/output-member-info-data.csv

  - statusフィールド

    - "1"(処理対象)から"0"(初期状態)に更新されていること

  - pointフィールド

    - ポイント加算対象について、会員種別に応じたポイントが加算されていること

      - typeフィールドが"G"(ゴールド会員)の場合は100ポイント

      - typeフィールドが"N"(一般会員)の場合は10ポイント

    - 1,000,000(上限値)を超えたレコードが存在しないこと

会員情報ファイルの入出力内容は以下のとおり。
ファイルのフィールドはid(会員番号)、type(会員種別)、status(商品購入フラグ)、point(ポイント)の順で出力される。

図 91. 会員情報ファイルの入出力内容

#### 9.4.3. 入力データの妥当性検証を行うジョブ

| 前提 チュートリアルの進め方で説明しているとおり、 データベースアクセスでデータ入出力を行うジョブ、ファイルアクセスでデータ入出力を行うジョブに対して、 本ジョブの実装を追加していく形式とする。 ただし、記述はデータベースアクセスするジョブに実装を追加した場合の説明としているため留意すること。

##### 9.4.3.1. 概要

入力データの妥当性検証(以降、入力チェックと呼ぶ)を行うジョブを作成する。

なお、詳細についてはMacchinetta Batch 2.x 開発ガイドラインの入力チェックを参照。

作成するアプリケーションの説明の 背景、処理概要、業務仕様を以下に再掲する。

###### 9.4.3.1.1. 背景

とある量販店では、会員に対してポイントカードを発行している。
会員には「ゴールド会員」「一般会員」の会員種別が存在し、会員種別に応じたサービスを提供している。
今回そのサービスの一環として、月内に商品を購入した会員のうち、 会員種別が「ゴールド会員」の場合は100ポイント、「一般会員」の場合は10ポイントを月末に加算することにした。

###### 9.4.3.1.2. 処理概要

会員種別に応じてポイント加算を行うアプリケーションを 月次バッチ処理としてMacchinetta Batch 2.xを使用して実装する。
入力データにポイントの上限値を超えるデータが存在するか妥当性検証を行う処理を追加実装する。

###### 9.4.3.1.3. 業務仕様

業務仕様は以下のとおり。

- 入力データのポイントが1,000,000ポイントを超過していないことをチェックする

  - チェックエラーとなる場合は、処理を異常終了する(例外ハンドリングは行わない)

- 商品購入フラグが"1"(処理対象)の場合に、会員種別に応じてポイントを加算する

  - 会員種別が"G"(ゴールド会員)の場合は100ポイント、"N"(一般会員)の場合は10ポイント加算する

- 商品購入フラグはポイント加算後に"0"(初期状態)に更新する

- ポイントの上限値は1,000,000ポイントとする

- ポイント加算後に1,000,000ポイントを超えた場合は、1,000,000ポイントに補正する

###### 9.4.3.1.4. テーブル仕様

入出力リソースとなる会員情報テーブルの仕様は以下のとおり。
前提のとおりデータベースアクセスするジョブの場合の説明となるため、ファイルアクセスするジョブの場合の 入出力のリソース仕様はファイル仕様を参照。

表 233. 会員情報テーブル(member_info)
No | 属性名 | カラム名 | PK | データ型 | 桁数 | 説明
--- | --- | --- | --- | --- | --- | ---
1 | 会員番号 | id |  | CHAR | 8 | 会員を一意に示す8桁固定の番号を表す。
2 | 会員種別 | type | - | CHAR | 1 | 会員の種別を以下のとおり表す。 "G"(ゴールド会員)、"N"(一般会員)
3 | 商品購入フラグ | status | - | CHAR | 1 | 月内に商品を買ったかどうかを表す。 商品購入で"1"(処理対象)、月次バッチ処理で"0"(初期状態)に更新される。
4 | ポイント | point | - | INT | 7 | 会員の保有するポイントを表す。 初期値は0。

###### 9.4.3.1.5. ジョブの概要

ここで作成する入力チェックを行うジョブの概要を把握するために、 処理フローおよび処理シーケンスを以下に示す。

前提のとおりデータベースアクセスするジョブの場合の説明となるため、 ファイルアクセスするジョブの場合の処理フローおよび処理シーケンスとは異なる部分があるため留意する。

入力チェックは、単項目チェック、相関項目チェックに分類されるが、ここでは単項目チェックのみを扱う。
単項目チェックは、Bean Validationを利用する。 詳細は入力チェックの分類を参照。

処理フロー概要

  処理フローの概要を以下に示す。

図 92. 入力データの妥当性検証を行うジョブの処理フロー

チャンクモデルの場合の処理シーケンス

  チャンクモデルの場合の処理シーケンスを説明する。
  本ジョブは異常系データを利用することを前提として説明しているため、 このシーケンス図は入力チェックでエラー(異常終了)となった場合を示している。
  入力チェックが正常の場合、入力チェック以降の処理シーケンスはデータベースアクセスのシーケンス図 (ジョブの概要を参照)と同じである。

チャンクモデルの場合、入力チェックは`ItemProcessor`にデータが渡されたタイミングで行う。

橙色のオブジェクトは今回実装するクラスを表す。

図 93. チャンクモデルのシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

2. ステップは、リソースをオープンする。

3. `MyBatisCursorItemReader`は、member_infoテーブルから会員情報をすべて取得(select文の発行)する。

   - 入力データがなくなるまで、以降の処理を繰り返す。

   - チャンク単位で、フレームワークトランザクションを開始する。

   - チャンクサイズに達するまで4から12までの処理を繰り返す。

4. ステップは、`MyBatisCursorItemReader`から入力データを1件取得する。

5. `MyBatisCursorItemReader`は、member_infoテーブルから入力データを1件取得する。

6. member_infoテーブルは、`MyBatisCursorItemReader`に入力データを返却する。

7. `MyBatisCursorItemReader`は、ステップに入力データを返却する。

8. ステップは、`PointAddItemProcessor`で入力データに対して処理を行う。

9. `PointAddItemProcessor`は、`SpringValidator`に入力チェック処理を依頼する。

10. `SpringValidator`は、入力チェックルールに基づき入力チェックを行い、チェックエラーの場合は例外(ValidationException)をスローする。

11. `PointAddItemProcessor`は、入力データを読み込んでポイント加算処理を行う。

12. `PointAddItemProcessor`は、ステップに処理結果を返却する。

13. ステップは、チャンクサイズ分のデータを`MyBatisBatchItemWriter`で出力する。

14. `MyBatisBatchItemWriter`は、member_infoテーブルに対して会員情報の更新(update文の発行)を行う。

    - 4から14までの処理過程で例外が発生すると、以降の処理を行う。

15. ステップはフレームワークトランザクションをロールバックする。

16. ステップはジョブに終了コード(ここでは異常終了:255)を返却する。

タスクレットモデルの場合の処理シーケンス

  タスクレットモデルの場合の処理シーケンスについて説明する。
  本ジョブは異常系データを利用することを前提として説明しているため、 このシーケンス図は入力チェックでエラー(異常終了)となった場合を示している。
  入力チェックが正常の場合、入力チェック以降の処理シーケンスはデータベースアクセスのシーケンス図 (ジョブの概要を参照)と同じである。

タスクレットモデルの場合、入力チェックは`Tasklet#execute()`にて任意のタイミングで行う。
ここでは、データを取得した直後に行っている。

橙色のオブジェクトは今回実装するクラスを表す。

図 94. タスクレットモデルのシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

   - ステップはフレームワークトランザクションを開始する。

2. ステップは`PointAddTasklet`を実行する。

3. `PointAddTasklet`は、リソースをオープンする。

4. `MyBatisCursorItemReader`は、member_infoテーブルから会員情報をすべて取得(select文の発行)する。

   - 入力データがなくなるまで5から13までの処理を繰り返す。

   - 一定件数に達するまで5から11までの処理を繰り返す。

5. `PointAddTasklet`は、`MyBatisCursorItemReader`から入力データを1件取得する。

6. `MyBatisCursorItemReader`は、member_infoテーブルから入力データを1件取得する。

7. member_infoテーブルは、`MyBatisCursorItemReader`に入力データを返却する。

8. `MyBatisCursorItemReader`は、タスクレットに入力データを返却する。

9. `PointAddTasklet`は、`SpringValidator`に入力チェック処理を依頼する。

10. `SpringValidator`は、入力チェックルールに基づき入力チェックを行い、チェックエラーの場合は例外(ValidationException)をスローする。

11. `PointAddTasklet`は、入力データを読み込んでポイント加算処理を行う。

12. `PointAddTasklet`は、一定件数分のデータを`MyBatisBatchItemWriter`で出力する。

13. `MyBatisBatchItemWriter`は、member_infoテーブルに対して会員情報の更新(update文の発行)を行う。

    - 2から13までの処理過程で例外が発生すると、以降の処理を行う。

14. `PointAddTasklet`はステップへ例外(ここではValidationException)をスローする。

15. ステップはフレームワークトランザクションをロールバックする。

16. ステップはジョブに終了コード(ここでは異常終了:255)を返却する。

以降で、チャンクモデル、タスクレットモデルそれぞれの実装方法を説明する。

- チャンクモデルでの実装

- タスクレットモデルでの実装

##### 9.4.3.2. チャンクモデルでの実装

チャンクモデルで入力チェックを行うジョブの作成から実行までを以下の手順で実施する。

1. 入力チェックルールの定義

2. 入力チェック処理の実装

3. ジョブの実行と結果の確認

###### 9.4.3.2.1. 入力チェックルールの定義

入力チェックを行うために、DTOクラスのチェック対象のフィールドにBean Validationのアノテーションを付与する。
入力チェック用のアノテーションについては、Macchinetta Server 1.x 開発ガイドラインのBean Validationのチェックルール およびHibernate Validatorのチェックルールを参照。

チャンクモデル/タスクレットモデルで共通して利用するため、既に実施している場合は読み飛ばしてよい。

ここでは、ポイントが1,000,000(上限値)を超過していないかチェックするためのチェックルールを定義する。

com.example.batch.tutorial.common.dto.MemberInfoDto

    package com.example.batch.tutorial.common.dto;

    import jakarta.validation.constraints.Max;

    public class MemberInfoDto {
        private String id;

        private String type;

        private String status;

        @Max(1000000) // (1)
        private int point;

        // Getter and setter are omitted.
    }

表 234. 説明
項番 | 説明
--- | ---
(1) | 対象のフィールドが指定した数値以下であることを示す@Maxアノテーションを付与する。

###### 9.4.3.2.2. 入力チェック処理の実装

ポイント加算処理を行うビジネスロジッククラスに入力チェック処理を実装する。

既に実装してある`PointAddItemProcessor`クラスに入力チェック処理の実装を追加する。
前提のとおりデータベースアクセスするジョブの場合の説明となるため、ファイルアクセスするジョブの場合の 実装は以下の(1)～(3)のみ追加する。

com.example.batch.tutorial.dbaccess.chunk.PointAddItemProcessor

    // Package and the other import are omitted.

    import jakarta.inject.Inject;
    import org.springframework.batch.item.validator.Validator;

    @Component
    public class PointAddItemProcessor implements ItemProcessor<MemberInfoDto, MemberInfoDto> {
        // Definition of constans are omitted.

        @Inject // (1)
        Validator<MemberInfoDto> validator; // (2)

        @Override
        public MemberInfoDto process(MemberInfoDto item) throws Exception {
            validator.validate(item); // (3)

            // The other codes of bussiness logic are omitted.
        }
    }

表 235. 説明
項番 | 説明
--- | ---
(1) | `SpringValidator`のインスタンスをインジェクトする。
(2) | `org.springframework.batch.item.validator.Validator`の型引数には、 `ItemReader`を通じて取得するDTOを設定する。
(3) | `ItemReader`を通じて取得するDTOを引数として`Validator#validate()`を実行する。 本来、`validate()`を実行する際は入力チェックエラーをハンドリングするためにtry-catchを実装して例外を捕捉するが、 try-catchを利用した例外ハンドリングはtry-catchで例外ハンドリングを行うジョブで説明するため、 ここでは、例外ハンドリングは実装しない。

###### 9.4.3.2.3. ジョブの実行と結果の確認

作成したジョブをSTS上で実行し、結果を確認する。

実行構成からジョブを実行

既に作成してある実行構成から、ジョブを実行する。

ここでは、異常系データを利用してジョブを実行する。
入力チェックを実装したジョブが扱うリソース(データベース or ファイル)によって 入力データの切替方法が異なるため、以下のとおり実行すること。

データベースアクセスでデータ入出力を行うジョブに対して入力チェックを実装した場合

  データベースアクセスでデータ入出力を行うジョブの実行構成からジョブを実行 で作成した実行構成を使ってジョブを実行する。

異常系データを利用するために、`batch-application.properties`のDatabase Initializeで 正常系データのスクリプトをコメントアウトし、異常系データのスクリプトのコメントアウトを解除する。

src/main/resources/batch-application.properties

    # Database Initialize
    tutorial.create-table.script=file:sqls/create-member-info-table.sql
    #tutorial.insert-data.script=file:sqls/insert-member-info-data.sql
    tutorial.insert-data.script=file:sqls/insert-member-info-error-data.sql

ファイルアクセスでデータ入出力を行うジョブに対して入力チェックを実装した場合

  ファイルアクセスでデータ入出力を行うジョブの実行構成からジョブを実行 で作成した実行構成を使ってジョブを実行する。

異常系データを利用するために、実行構成で設定する引数のうち、 入力ファイル(inputFile)のパスを正常系データ(input-member-info-data.csv)から異常系データ(input-member-info-error-data.csv)に変更する。

コンソールログの確認

Console Viewを開き、以下の内容のログが出力されていることを確認する。

- 処理が異常終了(FAILED)していること。

- `org.springframework.batch.item.validator.ValidationException`が発生していること。

コンソールログ出力例

    (.. omitted)

    [2020/03/10 16:04:18] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddChunk]] launched with the following parameters: [{jsr_batch_run_id=140}]
    [2020/03/10 16:04:18] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [jobPointAddChunk.step01]
    [2020/03/10 16:04:18] [main] [o.s.b.c.s.AbstractStep] [ERROR] Encountered an error executing step jobPointAddChunk.step01 in job jobPointAddChunk
    org.springframework.batch.item.validator.ValidationException: Validation failed for com.example.batch.tutorial.common.dto.MemberInfoDto@2b1cd7bc:
    Field error in object 'item' on field 'point': rejected value [1000001]; codes [Max.item.point,Max.point,Max.int,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.point,point]; arguments []; default message [point],1000000]; default message [must be less than or equal to 1000000]
            at org.springframework.batch.item.validator.SpringValidator.validate(SpringValidator.java:54)

    (.. omitted)

    Caused by: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
    Field error in object 'item' on field 'point': rejected value [1000001]; codes [Max.item.point,Max.point,Max.int,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.point,point]; arguments []; default message [point],1000000]; default message [must be less than or equal to 1000000]
            ... 29 common frames omitted
    [2020/03/10 16:04:18] [main] [o.s.b.c.s.AbstractStep] [INFO ] Step: [jobPointAddChunk.step01] executed in 243ms
    [2020/03/10 16:04:18] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddChunk]] completed with the following parameters: [{jsr_batch_run_id=140}] and the following status: [FAILED] in 319ms

終了コードの確認

終了コードにより、異常終了したことを確認する。
確認手順はジョブの実行と結果の確認を参照。 終了コード(exit value)が255(異常終了)となっていることを確認する。

図 95. 終了コードの確認
出力リソースの確認

入力チェックを実装したジョブによって出力リソース(データベース or ファイル)を確認する。

チャンクモデルの場合、中間コミット方式をとっているため、エラー箇所直前のチャンクまで更新が確定していることを確認する。

会員情報テーブルの確認

H2 Consoleを使用して会員情報テーブルの確認を行う。
更新前後の会員情報テーブルの内容を比較し、確認内容のとおりとなっていることを確認する。
確認手順はH2 Consoleを使用してデータベースを参照するを参照。

確認内容

  - 1から10番目のレコード(会員番号が"00000001"から"00000010"のレコード)について

    - statusカラム

      - "1"(処理対象)から"0"(初期状態)に更新されていること

    - pointカラム

      - ポイント加算対象について、会員種別に応じたポイントが加算されていること

        - typeカラムが"G"(ゴールド会員)の場合は100ポイント

        - typeカラムが"N"(一般会員)の場合は10ポイント

  - 11から15番目のレコード(会員番号が"00000011"から"00000015"のレコード)について

    - 更新されていないこと(破線の赤枠で示した範囲)

更新前後の会員情報テーブルの内容を以下に示す。

図 96. 更新前後の会員情報テーブルの内容
会員情報ファイルの確認

会員情報ファイルの入出力内容を比較し、確認内容のとおりとなっていることを確認する。

確認内容

  - 出力ディレクトリに会員情報ファイルが出力されていること

    - 出力ファイル: files/output/output-member-info-data.csv

  - 1から10番目のレコード(会員番号が"00000001"から"00000010"のレコード)について

    - statusフィールド

      - "1"(処理対象)から"0"(初期状態)に更新されていること

    - pointフィールド

      - ポイント加算対象について、会員種別に応じたポイントが加算されていること

        - typeフィールドが"G"(ゴールド会員)の場合は100ポイント

        - typeフィールドが"N"(一般会員)の場合は10ポイント

  - 11から15番目のレコード(会員番号が"00000011"から"00000015"のレコード)について

    - 出力されていないこと(破線の赤枠で示した範囲)

会員情報ファイルの入出力内容を以下に示す。
ファイルのフィールドはid(会員番号)、type(会員種別)、status(商品購入フラグ)、point(ポイント)の順で出力される。

図 97. 会員情報ファイルの入出力内容

##### 9.4.3.3. タスクレットモデルでの実装

タスクレットモデルで入力チェックを行うジョブの作成から実行までを以下の手順で実施する。

1. 入力チェックルールの定義

2. 入力チェック処理の実装

3. ジョブの実行と結果の確認

###### 9.4.3.3.1. 入力チェックルールの定義

入力チェックを行うために、DTOクラスのチェック対象のフィールドにBean Validationのアノテーションを付与する。
入力チェック用のアノテーションについては、Macchinetta Server 1.x 開発ガイドラインのBean Validationのチェックルール およびHibernate Validatorのチェックルールを参照。

チャンクモデル/タスクレットモデルで共通して利用するため、既に実施している場合は読み飛ばしてよい。

ここでは、ポイントが1,000,000(上限値)を超過していないかチェックするためのチェックルールを定義する。

com.example.batch.tutorial.common.dto.MemberInfoDto

    package com.example.batch.tutorial.common.dto;

    import jakarta.validation.constraints.Max;

    public class MemberInfoDto {
        private String id;

        private String type;

        private String status;

        @Max(1000000) // (1)
        private int point;

        // Getter and setter are omitted.
    }

表 236. 説明
項番 | 説明
--- | ---
(1) | 対象のフィールドが指定した数値以下であることを示す@Maxアノテーションを付与する。

###### 9.4.3.3.2. 入力チェック処理の実装

ポイント加算処理を行うビジネスロジッククラスに入力チェック処理を実装する。

既に実装してある`PointAddTasklet`クラスに入力チェック処理の実装を追加する。
前提のとおりデータベースアクセスするジョブの場合の説明となるため、 ファイルアクセスするジョブの場合の実装は以下の(1)～(3)のみ追加する。

com.example.batch.tutorial.dbaccess.tasklet.PointAddTasklet

    // Package and the other import are omitted.

    import jakarta.inject.Inject;
    import org.springframework.batch.item.validator.Validator;

    @Component
    public class PointAddTasklet implements Tasklet {
        // Definition of constans, ItemStreamReader and ItemWriter are omitted.

        @Inject // (1)
        Validator<MemberInfoDto> validator; // (2)

        @Override
        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
            MemberInfoDto item = null;

            List<MemberInfoDto> items = new ArrayList<>(CHUNK_SIZE);

            try {
                reader.open(chunkContext.getStepContext().getStepExecution().getExecutionContext());

                while ((item = reader.read()) != null) {
                    validator.validate(item); // (3)

                    // The other codes of bussiness logic are omitted.
                }

                writer.write(new Chunk(items));
            } finally {
                reader.close();
            }

            return RepeatStatus.FINISHED;
        }
    }

表 237. 説明
項番 | 説明
--- | ---
(1) | `SpringValidator`のインスタンスをインジェクトする。
(2) | `org.springframework.batch.item.validator.Validator`の型引数には、 `ItemReader`を通じて取得するDTOを設定する。
(3) | `ItemReader`を通じて取得するDTOを引数として`Validator#validate()`を実行する。 本来、`validate()`を実行する際は入力チェックエラーをハンドリングするためにtry-catchを実装して例外を捕捉するが、 try-catchを利用した例外ハンドリングはtry-catchで例外ハンドリングを行うジョブで説明するため、 ここでは、例外ハンドリングは実装しない。

###### 9.4.3.3.3. ジョブの実行と結果の確認

作成したジョブをSTS上で実行し、結果を確認する。

実行構成からジョブを実行

既に作成してある実行構成から、ジョブを実行する。

ここでは、異常系データを利用してジョブを実行する。
入力チェックを実装したジョブが扱うリソース(データベース or ファイル)によって 入力データの切替方法が異なるため、以下のとおり実行すること。

データベースアクセスでデータ入出力を行うジョブに対して入力チェックを実装した場合

  データベースアクセスでデータ入出力を行うジョブの実行構成からジョブを実行 で作成した実行構成を使ってジョブを実行する。

異常系データを利用するために、`batch-application.properties`のDatabase Initializeで 正常系データのスクリプトをコメントアウトし、異常系データのスクリプトのコメントアウトを解除する。

src/main/resources/batch-application.properties

    # Database Initialize
    tutorial.create-table.script=file:sqls/create-member-info-table.sql
    #tutorial.insert-data.script=file:sqls/insert-member-info-data.sql
    tutorial.insert-data.script=file:sqls/insert-member-info-error-data.sql

ファイルアクセスでデータ入出力を行うジョブに対して入力チェックを実装した場合

  ファイルアクセスでデータ入出力を行うジョブの実行構成からジョブを実行 で作成した実行構成を使ってジョブを実行する。

異常系データを利用するために、実行構成で設定する引数のうち、 入力ファイル(inputFile)のパスを正常系データ(input-member-info-data.csv)から異常系データ(input-member-info-error-data.csv)に変更する。

コンソールログの確認

Console Viewを開き、以下の内容のログが出力されていることを確認する。

- 処理が異常終了(FAILED)していること。

- `org.springframework.batch.item.validator.ValidationException`が発生していること。

コンソールログ出力例

    (.. omitted)

    [2020/03/10 16:05:44] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddTasklet]] launched with the following parameters: [{jsr_batch_run_id=142}]
    [2020/03/10 16:05:44] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [jobPointAddTasklet.step01]
    [2020/03/10 16:05:44] [main] [o.s.b.c.s.AbstractStep] [ERROR] Encountered an error executing step jobPointAddTasklet.step01 in job jobPointAddTasklet
    org.springframework.batch.item.validator.ValidationException: Validation failed for com.example.batch.tutorial.common.dto.MemberInfoDto@3811510:
    Field error in object 'item' on field 'point': rejected value [1000001]; codes [Max.item.point,Max.point,Max.int,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.point,point]; arguments []; default message [point],1000000]; default message [must be less than or equal to 1000000]
            at org.springframework.batch.item.validator.SpringValidator.validate(SpringValidator.java:54)

    (.. omitted)

    Caused by: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
    Field error in object 'item' on field 'point': rejected value [1000001]; codes [Max.item.point,Max.point,Max.int,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.point,point]; arguments []; default message [point],1000000]; default message [must be less than or equal to 1000000]
            ... 24 common frames omitted
    [2020/03/10 16:05:44] [main] [o.s.b.c.s.AbstractStep] [INFO ] Step: [jobPointAddTasklet.step01] executed in 178ms
    [2020/03/10 16:05:44] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddTasklet]] completed with the following parameters: [{jsr_batch_run_id=142}] and the following status: [FAILED] in 244ms

終了コードの確認

終了コードにより、異常終了したことを確認する。
確認手順はジョブの実行と結果の確認を参照。 終了コード(exit value)が255(異常終了)となっていることを確認する。

図 98. 終了コードの確認
出力リソースの確認

入力チェックを実装したジョブによって出力リソース(データベース or ファイル)を確認する。

タスクレットモデルの場合、一括コミット方式をとっているため、エラーが発生した場合は一切更新されていないことを確認してほしい。

会員情報テーブルの確認

H2 Consoleを使用して会員情報テーブルの確認を行う。
更新前後の会員情報テーブルの内容を比較し、確認内容のとおりとなっていることを確認する。
確認手順はH2 Consoleを使用してデータベースを参照するを参照。

確認内容

  - すべてのレコードについて、データが更新されていないこと

図 99. 初期状態の会員情報テーブルの内容
会員情報ファイルの確認

会員情報ファイルの入出力内容を比較し、確認内容のとおりとなっていることを確認する。

確認内容

  - 出力ディレクトリに会員情報ファイルが空ファイルで出力されていること

    - 出力ファイル: files/output/output-member-info-data.csv

#### 9.4.4. ChunkListenerで例外ハンドリングを行うジョブ

| 前提 チュートリアルの進め方で説明しているとおり、 入力データの妥当性検証を行うジョブに対して、 例外ハンドリングの実装を追加していく形式とする。なお、例外ハンドリング方式にはtry-catchやChunkListenerなど様々な方式がある。 ただし、記述はデータベースアクセスするジョブに実装を追加した場合の説明としているため留意すること。

##### 9.4.4.1. 概要

ChunkListenerで例外ハンドリングを行うジョブを作成する。

なお、詳細についてはMacchinetta Batch 2.x 開発ガイドラインの`ChunkListener`インタフェースによる例外ハンドリングを参照。

| リスナーの用途について ここでは、リスナーによりステップの実行後に例外発生の有無をチェックすることで例外ハンドリングを実現するが、 リスナーの用途は例外ハンドリングに限定されてはいないため、 詳細についてはリスナーを参照。

作成するアプリケーションの説明の 背景、処理概要、業務仕様を以下に再掲する。

###### 9.4.4.1.1. 背景

とある量販店では、会員に対してポイントカードを発行している。
会員には「ゴールド会員」「一般会員」の会員種別が存在し、会員種別に応じたサービスを提供している。
今回そのサービスの一環として、月内に商品を購入した会員のうち、 会員種別が「ゴールド会員」の場合は100ポイント、「一般会員」の場合は10ポイントを月末に加算することにした。

###### 9.4.4.1.2. 処理概要

会員種別に応じてポイント加算を行うアプリケーションを 月次バッチ処理としてMacchinetta Batch 2.xを使用して実装する。
入力データにポイントの上限値を超えるデータが存在するか妥当性検証を行う処理を追加実装し、 エラーの場合ログを出力し、異常終了させる。

###### 9.4.4.1.3. 業務仕様

業務仕様は以下のとおり。

- 入力データのポイントが1,000,000ポイントを超過していないことをチェックする

  - チェックエラーとなる場合は、エラーメッセージをログに出力して処理を異常終了する

  - エラーメッセージはリスナーによる後処理で実現する

  - メッセージ内容は「The point exceeds 1000000.」(ポイントが1,000,000を超えています)とする

- 商品購入フラグが"1"(処理対象)の場合に、会員種別に応じてポイントを加算する

  - 会員種別が"G"(ゴールド会員)の場合は100ポイント、"N"(一般会員)の場合は10ポイント加算する

- 商品購入フラグはポイント加算後に"0"(初期状態)に更新する

- ポイントの上限値は1,000,000ポイントとする

- ポイント加算後に1,000,000ポイントを超えた場合は、1,000,000ポイントに補正する

###### 9.4.4.1.4. テーブル仕様

入出力リソースとなる会員情報テーブルの仕様は以下のとおり。
前提のとおりデータベースアクセスするジョブの場合の説明となるため、ファイルアクセスするジョブの場合の 入出力のリソース仕様はファイル仕様を参照。

表 238. 会員情報テーブル(member_info)
No | 属性名 | カラム名 | PK | データ型 | 桁数 | 説明
--- | --- | --- | --- | --- | --- | ---
1 | 会員番号 | id |  | CHAR | 8 | 会員を一意に示す8桁固定の番号を表す。
2 | 会員種別 | type | - | CHAR | 1 | 会員の種別を以下のとおり表す。 "G"(ゴールド会員)、"N"(一般会員)
3 | 商品購入フラグ | status | - | CHAR | 1 | 月内に商品を買ったかどうかを表す。 商品購入で"1"(処理対象)、月次バッチ処理で"0"(初期状態)に更新される。
4 | ポイント | point | - | INT | 7 | 会員の保有するポイントを表す。 初期値は0。

###### 9.4.4.1.5. ジョブの概要

ここで作成する入力チェックを行うジョブの概要を把握するために、 処理フローおよび処理シーケンスを以下に示す。

前提のとおりデータベースアクセスするジョブの場合の説明となるため、 ファイルアクセスするジョブの場合の処理フローおよび処理シーケンスとは異なる部分があるため留意する。

処理フロー概要

  処理フローの概要を以下に示す。

図 100. 例外ハンドリングを行うジョブの処理フロー

チャンクモデルの場合の処理シーケンス

  チャンクモデルの場合の処理シーケンスを説明する。
  本ジョブは異常系データを利用することを前提として説明しているため、 このシーケンス図は入力チェックでエラー(異常終了)となった場合を示している。
  入力チェックが正常の場合、入力チェック以降の処理シーケンスはデータベースアクセスのシーケンス図 (ジョブの概要を参照)と同じである。

橙色のオブジェクトは今回実装するクラスを表す。

図 101. チャンクモデルのシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

2. ステップは、リソースをオープンする。

3. `MyBatisCursorItemReader`は、member_infoテーブルから会員情報をすべて取得(select文の発行)する。

   - 入力データがなくなるまで、以降の処理を繰り返す。

   - チャンク単位で、フレームワークトランザクションを開始する。

   - チャンクサイズに達するまで4から12までの処理を繰り返す。

4. ステップは、`MyBatisCursorItemReader`から入力データを1件取得する。

5. `MyBatisCursorItemReader`は、member_infoテーブルから入力データを1件取得する。

6. member_infoテーブルは、`MyBatisCursorItemReader`に入力データを返却する。

7. `MyBatisCursorItemReader`は、ステップに入力データを返却する。

8. ステップは、`PointAddItemProcessor`で入力データに対して処理を行う。

9. `PointAddItemProcessor`は、`SpringValidator`に入力チェック処理を依頼する。

10. `SpringValidator`は、入力チェックルールに基づき入力チェックを行い、チェックエラーの場合は例外(ValidationException)をスローする。

11. `PointAddItemProcessor`は、入力データを読み込んでポイント加算処理を行う。

12. `PointAddItemProcessor`は、ステップに処理結果を返却する。

13. ステップは、チャンクサイズ分のデータを`MyBatisBatchItemWriter`で出力する。

14. `MyBatisBatchItemWriter`は、member_infoテーブルに対して会員情報の更新(update文の発行)を行う。

    - 4から14までの処理過程で例外が発生すると、以降の処理を行う。

15. ステップはフレームワークトランザクションをロールバックする。

16. ステップは`ChunkErrorLoggingListener`を実行する。

17. `ChunkErrorLoggingListener`はERRORログ出力処理を行う。

18. ステップはジョブに終了コード(ここでは異常終了:255)を返却する。

タスクレットモデルの場合の処理シーケンス

  タスクレットモデルの場合の処理シーケンスについて説明する。
  本ジョブは異常系データを利用することを前提として説明しているため、 このシーケンス図は入力チェックでエラー(異常終了)となった場合を示している。
  入力チェックが正常の場合、入力チェック以降の処理シーケンスはデータベースアクセスのシーケンス図 (ジョブの概要を参照)と同じである。

橙色のオブジェクトは今回実装するクラスを表す。

図 102. タスクレットモデルのシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

   - ステップはフレームワークトランザクションを開始する。

2. ステップは`PointAddTasklet`を実行する。

3. `PointAddTasklet`は、リソースをオープンする。

4. `MyBatisCursorItemReader`は、member_infoテーブルから会員情報をすべて取得(select文の発行)する。

   - 入力データがなくなるまで5から13までの処理を繰り返す。

   - 一定件数に達するまで5から11までの処理を繰り返す。

5. `PointAddTasklet`は、`MyBatisCursorItemReader`から入力データを1件取得する。

6. `MyBatisCursorItemReader`は、member_infoテーブルから入力データを1件取得する。

7. member_infoテーブルは、`MyBatisCursorItemReader`に入力データを返却する。

8. `MyBatisCursorItemReader`は、タスクレットに入力データを返却する。

9. `PointAddTasklet`は、`SpringValidator`に入力チェック処理を依頼する。

10. `SpringValidator`は、入力チェックルールに基づき入力チェックを行い、チェックエラーの場合は例外(ValidationException)をスローする。

11. `PointAddTasklet`は、入力データを読み込んでポイント加算処理を行う。

12. `PointAddTasklet`は、一定件数分のデータを`MyBatisBatchItemWriter`で出力する。

13. `MyBatisBatchItemWriter`は、member_infoテーブルに対して会員情報の更新(update文の発行)を行う。

    - 2から13までの処理過程で例外が発生すると、以降の処理を行う。

14. `PointAddTasklet`はステップへ例外(ここではValidationException)をスローする。

15. ステップはフレームワークトランザクションをロールバックする。

16. ステップは`ChunkErrorLoggingListener`を実行する。

17. `ChunkErrorLoggingListener`はERRORログ出力処理を行う。

18. ステップはジョブに終了コード(ここでは異常終了:255)を返却する。

以降で、チャンクモデル、タスクレットモデルそれぞれの実装方法を説明する。

- チャンクモデルでの実装

- タスクレットモデルでの実装

##### 9.4.4.2. チャンクモデルでの実装

チャンクモデルで入力チェックを行うジョブの作成から実行までを以下の手順で実施する。

1. メッセージ定義の追加

2. 例外ハンドリングの実装

3. ジョブの実行と結果の確認

###### 9.4.4.2.1. メッセージ定義の追加

コード体系のばらつき防止や、監視対象のキーワードとしての抽出を設計しやすくするため、 ログメッセージはメッセージ定義を使用し、ログ出力時に使用する。

チャンクモデル/タスクレットモデルで共通して利用するため、既に作成している場合は読み飛ばしてよい。

`application-messages.properties`および`LaunchContextConfig.java/launch-context.xml`を以下のとおり設定する。
なお、`LaunchContextConfig.java/launch-context.xml`の設定はブランクプロジェクトに設定済みである。

src/main/resources/i18n/application-messages.properties

    # (1)
    errors.maxInteger=The {0} exceeds {1}.

JavaConfig
com.example.batch.tutorial.config.LaunchContextConfig.java

    // omitted
    @Bean
    public MessageSource messageSource() {
        final ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
        resourceBundleMessageSource.setBasename("i18n/application-messages"); // (2)
        return resourceBundleMessageSource;
    }
    // omitted

XMLConfig
META-INF/spring/launch-context.xml

    <!-- omitted -->

    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"
          p:basenames="i18n/application-messages" /> <!-- (2) -->

    <!-- omitted -->

表 239. 説明
項番 | 説明
--- | ---
(1) | ポイント上限超過時に出力するメッセージを設定する。 {0}に項目名、{1}に上限値を割り当てる。
(2) | プロパティファイルからメッセージを使用するために、`MessageSource`を設定する。

###### 9.4.4.2.2. 例外ハンドリングの実装

例外ハンドリング処理を実装する。

以下の作業を実施する。

1. ChunkErrorLoggingListenerクラスの実装

2. ジョブBean定義ファイルの設定

ChunkErrorLoggingListenerクラスの実装

`ChunkListener`インタフェースを利用して例外ハンドリングする。
ここでは、`ChunkListener`インタフェースの実装クラスとして、例外発生時にERRORログを出力する処理を実装する。

com.example.batch.tutorial.common.listener.ChunkErrorLoggingListener

    package com.example.batch.tutorial.common.listener;

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.batch.core.ChunkListener;
    import org.springframework.batch.core.scope.context.ChunkContext;
    import org.springframework.batch.item.validator.ValidationException;
    import org.springframework.context.MessageSource;
    import org.springframework.stereotype.Component;

    import jakarta.inject.Inject;
    import java.util.Locale;

    @Component
    public class ChunkErrorLoggingListener implements ChunkListener {
        private static final Logger logger = LoggerFactory.getLogger(ChunkErrorLoggingListener.class);

        @Inject
        MessageSource messageSource; // (1)

        @Override
        public void beforeChunk(ChunkContext chunkContext) {
            // do nothing.
        }

        @Override
        public void afterChunk(ChunkContext chunkContext) {
            // do nothing.
        }

        @Override
        public void afterChunkError(ChunkContext chunkContext) {
            Exception e = (Exception) chunkContext.getAttribute(ChunkListener.ROLLBACK_EXCEPTION_KEY); // (2)
            if (e instanceof ValidationException) {
                logger.error(messageSource
                        .getMessage("errors.maxInteger", new String[] { "point", "1000000" }, Locale.getDefault())); // (3)
            }
        }
    }

表 240. 説明
項番 | 説明
--- | ---
(1) | `ResourceBundleMessageSource`のインスタンスをインジェクトする。
(2) | `ROLLBACK_EXCEPTION_KEY`に設定された値をキーにして発生した例外を取得する。
(3) | プロパティファイルからメッセージIDが`errors.maxInteger`のメッセージを取得し、ログに出力する。

ジョブBean定義ファイルの設定

例外ハンドリングをChunkListenerで行うためのジョブBean定義ファイルの設定を以下に示す。

JavaConfig
com.example.batch.tutorial.config.dbaccess.JobPointAddChunkConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan({"com.example.batch.tutorial.dbaccess.chunk",
        "com.example.batch.tutorial.common.listener"}) // (1)
    @MapperScan(basePackages = "com.example.batch.tutorial.common.repository", sqlSessionFactoryRef = "jobSqlSessionFactory")
    public class JobPointAddChunkConfig {

        // omitted

        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           ItemReader<MemberInfoDto> reader,
                           PointAddItemProcessor processor,
                           ItemWriter<MemberInfoDto> writer,
                           ChunkErrorLoggingListener listener) {
            return new StepBuilder("jobPointAddChunk.step01",
                    jobRepository)
                    .<MemberInfoDto, MemberInfoDto>chunk(10,
                            transactionManager)
                    .listener(listener) // (2)
                    .reader(reader)
                    .processor(processor)
                    .writer(writer)
                    .build();
        }

        @Bean
        public Job jobPointAddChunk(JobRepository jobRepository,
                                                 Step step01) {
            return new JobBuilder("jobPointAddChunk", jobRepository)
                    .start(step01)
                    .build();
        }
    }

XMLConfig
src/main/resources/META-INF/jobs/dbaccess/jobPointAddChunk.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd
                 http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

        <!-- omitted -->

        <context:component-scan base-package="com.example.batch.tutorial.dbaccess.chunk,
                com.example.batch.tutorial.common.listener"/> <!-- (1) -->

        <!-- omitted -->

        <batch:job id="jobPointAddChunk" job-repository="jobRepository">
            <batch:step id="jobPointAddChunk.step01">
                <batch:tasklet transaction-manager="jobTransactionManager">
                    <batch:chunk reader="reader"
                                 processor="pointAddItemProcessor"
                                 writer="writer" commit-interval="10"/>
                    <batch:listeners>
                        <batch:listener ref="chunkErrorLoggingListener"/> <!--(2)-->
                    </batch:listeners>
                </batch:tasklet>
            </batch:step>
        </batch:job>

    </beans>

表 241. 説明
項番 | 説明
--- | ---
(1) | コンポーネントスキャン対象とするベースパッケージの設定を行う。 `basePackages属性/base-package属性`に、`ChunkListener`の実装クラスが格納されているパッケージを追加で指定する。
(2) | `StepListener`の実装クラスを設定する。 なお、`ChunkListener`は`StepListener`の拡張インタフェースである。 ここでは、ChunkListenerの実装クラスのBeanIDである`chunkErrorLoggingListener`を指定する。

###### 9.4.4.2.3. ジョブの実行と結果の確認

作成したジョブをSTS上で実行し、結果を確認する。

実行構成からジョブを実行

既に作成してある実行構成から、ジョブを実行する。

ここでは、異常系データを利用してジョブを実行する。
入力チェックを実装したジョブが扱うリソース(データベース or ファイル)によって、 入力データの切替方法が異なるため、以下のとおり実行すること。

データベースアクセスでデータ入出力を行うジョブに対して入力チェックを実装した場合

  データベースアクセスでデータ入出力を行うジョブの実行構成からジョブを実行 で作成した実行構成を使ってジョブを実行する。

異常系データを利用するために、`batch-application.properties`のDatabase Initializeで 正常系データのスクリプトをコメントアウトし、異常系データのスクリプトのコメントアウトを解除する。

src/main/resources/batch-application.properties

    # Database Initialize
    tutorial.create-table.script=file:sqls/create-member-info-table.sql
    #tutorial.insert-data.script=file:sqls/insert-member-info-data.sql
    tutorial.insert-data.script=file:sqls/insert-member-info-error-data.sql

ファイルアクセスでデータ入出力を行うジョブに対して入力チェックを実装した場合

  ファイルアクセスでデータ入出力を行うジョブの実行構成からジョブを実行 で作成した実行構成を使ってジョブを実行する。

異常系データを利用するために、実行構成で設定する引数のうち、 入力ファイル(inputFile)のパスを正常系データ(input-member-info-data.csv)から異常系データ(input-member-info-error-data.csv)に変更する。

コンソールログの確認

Console Viewを開き、以下の内容のログが出力されていることを確認する。

- 処理が異常終了(FAILED)していること。

- `org.springframework.batch.item.validator.ValidationException`が発生していること。

- `com.example.batch.tutorial.common.listener.ChunkErrorLoggingListener`がERRORログとして次のメッセージを出力していること。

  - 「The point exceeds 1000000.」

コンソールログ出力例

    (.. omitted)

    [2020/03/10 16:08:08] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddChunk]] launched with the following parameters: [{jsr_batch_run_id=144}]
    [2020/03/10 16:08:08] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [jobPointAddChunk.step01]
    [2020/03/10 16:08:08] [main] [c.e.b.t.c.l.ChunkErrorLoggingListener] [ERROR] The point exceeds 1000000.
    [2020/03/10 16:08:08] [main] [o.s.b.c.s.AbstractStep] [ERROR] Encountered an error executing step jobPointAddChunk.step01 in job jobPointAddChunk
    org.springframework.batch.item.validator.ValidationException: Validation failed for com.example.batch.tutorial.common.dto.MemberInfoDto@65fe2691:
    Field error in object 'item' on field 'point': rejected value [1000001]; codes [Max.item.point,Max.point,Max.int,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.point,point]; arguments []; default message [point],1000000]; default message [must be less than or equal to 1000000]
            at org.springframework.batch.item.validator.SpringValidator.validate(SpringValidator.java:54)

    (.. omitted)

    Caused by: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
    Field error in object 'item' on field 'point': rejected value [1000001]; codes [Max.item.point,Max.point,Max.int,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.point,point]; arguments []; default message [point],1000000]; default message [must be less than or equal to 1000000]
            ... 29 common frames omitted
    [2020/03/10 16:08:08] [main] [o.s.b.c.s.AbstractStep] [INFO ] Step: [jobPointAddChunk.step01] executed in 235ms
    [2020/03/10 16:08:08] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddChunk]] completed with the following parameters: [{jsr_batch_run_id=144}] and the following status: [FAILED] in 297ms

終了コードの確認

終了コードにより、異常終了したことを確認する。
確認手順はジョブの実行と結果の確認を参照。 終了コード(exit value)が255(異常終了)となっていることを確認する。

図 103. 終了コードの確認
出力リソースの確認

入力チェックを実装したジョブによって出力リソース(データベース or ファイル)を確認する。

チャンクモデルの場合、中間コミット方式をとっているため、エラー箇所直前のチャンクまで更新が確定していることを確認する。

会員情報テーブルの確認

更新前後の会員情報テーブルの内容を比較し、確認内容のとおりとなっていることを確認する。
確認手順はH2 Consoleを使用してデータベースを参照するを参照。

確認内容

  - 1から10番目のレコード(会員番号が"00000001"から"00000010"のレコード)について

    - statusカラム

      - "1"(処理対象)から"0"(初期状態)に更新されていること

    - pointカラム

      - ポイント加算対象について、会員種別に応じたポイントが加算されていること

        - typeカラムが"G"(ゴールド会員)の場合は100ポイント

        - typeカラムが"N"(一般会員)の場合は10ポイント

  - 11から15番目のレコード(会員番号が"00000011"から"00000015"のレコード)について

    - 更新されていないこと(破線の赤枠で示した範囲)

更新前後の会員情報テーブルの内容は以下のとおり。

図 104. 更新前後の会員情報テーブルの内容
会員情報ファイルの確認

会員情報ファイルの入出力内容を比較し、確認内容のとおりとなっていることを確認する。

確認内容

  - 出力ディレクトリに会員情報ファイルが出力されていること

    - 出力ファイル: files/output/output-member-info-data.csv

  - 1から10番目のレコード(会員番号が"00000001"から"00000010"のレコード)について

    - statusフィールド

      - "1"(処理対象)から"0"(初期状態)に更新されていること

    - pointフィールド

      - ポイント加算対象について、会員種別に応じたポイントが加算されていること

        - typeフィールドが"G"(ゴールド会員)の場合は100ポイント

        - typeフィールドが"N"(一般会員)の場合は10ポイント

  - 11から15番目のレコード(会員番号が"00000011"から"00000015"のレコード)について

    - 出力されていないこと(破線の赤枠で示した範囲)

会員情報ファイルの入出力内容は以下のとおり。
ファイルのフィールドはid(会員番号)、type(会員種別)、status(商品購入フラグ)、point(ポイント)の順で出力される。

図 105. 会員情報ファイルの入出力内容

##### 9.4.4.3. タスクレットモデルでの実装

タスクレットモデルで入力チェックを行うジョブの作成から実行までを以下の手順で実施する。

1. メッセージ定義の追加

2. 例外ハンドリングの実装

3. ジョブの実行と結果の確認

###### 9.4.4.3.1. メッセージ定義の追加

コード体系のばらつき防止や、監視対象のキーワードとしての抽出を設計しやすくするため、 ログメッセージはメッセージ定義を使用し、ログ出力時に使用する。

チャンクモデル/タスクレットモデルで共通して利用するため、既に作成している場合は読み飛ばしてよい。

`application-messages.properties`および`LaunchContextConfig.java/launch-context.xml`を以下のとおり設定する。
なお、`LaunchContextConfig.java/launch-context.xml`の設定はブランクプロジェクトに設定済みである。

src/main/resources/i18n/application-messages.properties

    # (1)
    errors.maxInteger=The {0} exceeds {1}.

JavaConfig
com.example.batch.tutorial.config.LaunchContextConfig.java

    // omitted
    @Bean
    public MessageSource messageSource() {
        final ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
        resourceBundleMessageSource.setBasename("i18n/application-messages"); // (2)
        return resourceBundleMessageSource;
    }
    // omitted

XMLConfig
META-INF/spring/launch-context.xml

    <!-- omitted -->

    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"
          p:basenames="i18n/application-messages" /> <!-- (2) -->

    <!-- omitted -->

表 242. 説明
項番 | 説明
--- | ---
(1) | ポイント上限超過時に出力するメッセージを設定する。 {0}に項目名、{1}に上限値を割り当てる。
(2) | プロパティファイルからメッセージを使用するために、`MessageSource`を設定する。

###### 9.4.4.3.2. 例外ハンドリングの実装

例外ハンドリング処理を実装する。

以下の作業を実施する。

1. ChunkErrorLoggingListenerクラスの実装

2. ジョブBean定義ファイルの設定

ChunkErrorLoggingListenerクラスの実装

`ChunkListener`インタフェースを利用して例外ハンドリングする。
ここでは、`ChunkListener`インタフェースの実装クラスとして、例外発生時にERRORログを出力する処理を実装する。

com.example.batch.tutorial.common.listener.ChunkErrorLoggingListener

    package com.example.batch.tutorial.common.listener;

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.batch.core.ChunkListener;
    import org.springframework.batch.core.scope.context.ChunkContext;
    import org.springframework.batch.item.validator.ValidationException;
    import org.springframework.context.MessageSource;
    import org.springframework.stereotype.Component;

    import jakarta.inject.Inject;
    import java.util.Locale;

    @Component
    public class ChunkErrorLoggingListener implements ChunkListener {
        private static final Logger logger = LoggerFactory.getLogger(ChunkErrorLoggingListener.class);

        @Inject
        MessageSource messageSource; // (1)

        @Override
        public void beforeChunk(ChunkContext chunkContext) {
            // do nothing.
        }

        @Override
        public void afterChunk(ChunkContext chunkContext) {
            // do nothing.
        }

        @Override
        public void afterChunkError(ChunkContext chunkContext) {
            Exception e = (Exception) chunkContext.getAttribute(ChunkListener.ROLLBACK_EXCEPTION_KEY); // (2)
            if (e instanceof ValidationException) {
                logger.error(messageSource
                        .getMessage("errors.maxInteger", new String[] { "point", "1000000" }, Locale.getDefault())); // (3)
            }
        }
    }

表 243. 説明
項番 | 説明
--- | ---
(1) | `ResourceBundleMessageSource`のインスタンスをインジェクトする。
(2) | `ROLLBACK_EXCEPTION_KEY`に設定された値をキーにして発生した例外を取得する。
(3) | プロパティファイルからメッセージIDが`errors.maxInteger`のメッセージを取得し、ログに出力する。

ジョブBean定義ファイルの設定

例外ハンドリングをChunkListenerで行うためのジョブBean定義ファイルの設定を以下に示す。

JavaConfig
com.example.batch.tutorial.config.dbaccess.JobPointAddTaskletConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan({"com.example.batch.tutorial.dbaccess.tasklet",
        "com.example.batch.tutorial.common.listener"}) // (1)
    @MapperScan(basePackages = "com.example.batch.tutorial.common.repository", sqlSessionFactoryRef = "jobSqlSessionFactory")
    public class JobPointAddTaskletConfig {

        // omitted

        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           PointAddTasklet tasklet,
                           ChunkErrorLoggingListener listener) {
            return new StepBuilder("jobPointAddTasklet.step01",
                    jobRepository)
                    .tasklet(tasklet, transactionManager)
                    .listener(listener) // (2)
                    .build();
        }

        @Bean
        public Job jobPointAddTasklet(JobRepository jobRepository,
                                                 Step step01) {
            return new JobBuilder("jobPointAddTasklet", jobRepository)
                    .start(step01)
                    .build();
        }
    }

XMLConfig
src/main/resources/META-INF/jobs/dbaccess/jobPointAddTasklet.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd
                 http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

        <!-- omitted -->

        <context:component-scan base-package="com.example.batch.tutorial.dbaccess.tasklet,
                com.example.batch.tutorial.common.listener"/> <!-- (1) -->

        <!-- omitted -->

        <batch:job id="jobPointAddTasklet" job-repository="jobRepository">
            <batch:step id="jobPointAddTasklet.step01">
                <batch:tasklet transaction-manager="jobTransactionManager"
                               ref="pointAddTasklet">
                    <batch:listeners>
                        <batch:listener ref="chunkErrorLoggingListener"/> <!-- (2) -->
                    </batch:listeners>
                </batch:tasklet>
            </batch:step>
        </batch:job>

    </beans>

表 244. 説明
項番 | 説明
--- | ---
(1) | コンポーネントスキャン対象とするベースパッケージの設定を行う。 `basePackages属性/base-package属性`に、`ChunkListener`の実装クラスが格納されているパッケージを追加で指定する。
(2) | `StepListener`の実装クラスを設定する。 なお、`ChunkListener`は`StepListener`の拡張インタフェースである。 ここでは、ChunkListenerの実装クラスのBeanIDである`chunkErrorLoggingListener`を指定する。

###### 9.4.4.3.3. ジョブの実行と結果の確認

作成したジョブをSTS上で実行し、結果を確認する。

実行構成からジョブを実行

既に作成してある実行構成から、ジョブを実行する。

ここでは、異常系データを利用してジョブを実行する。
入力チェックを実装したジョブが扱うリソース(データベース or ファイル)によって 入力データの切替方法が異なるため、以下のとおり実行すること。

データベースアクセスでデータ入出力を行うジョブに対して入力チェックを実装した場合

  データベースアクセスでデータ入出力を行うジョブの実行構成からジョブを実行 で作成した実行構成を使ってジョブを実行する。

異常系データを利用するために、`batch-application.properties`のDatabase Initializeで 正常系データのスクリプトをコメントアウトし、異常系データのスクリプトのコメントアウトを解除する。

src/main/resources/batch-application.properties

    # Database Initialize
    tutorial.create-table.script=file:sqls/create-member-info-table.sql
    #tutorial.insert-data.script=file:sqls/insert-member-info-data.sql
    tutorial.insert-data.script=file:sqls/insert-member-info-error-data.sql

ファイルアクセスでデータ入出力を行うジョブに対して入力チェックを実装した場合

  ファイルアクセスでデータ入出力を行うジョブの実行構成からジョブを実行 で作成した実行構成を使ってジョブを実行する。

異常系データを利用するために、実行構成で設定する引数のうち、 入力ファイル(inputFile)のパスを正常系データ(input-member-info-data.csv)から異常系データ(input-member-info-error-data.csv)に変更する。

コンソールログの確認

Console Viewを開き、以下の内容のログが出力されていることを確認する。

- 処理が異常終了(FAILED)していること。

- `org.springframework.batch.item.validator.ValidationException`が発生していること。

- `com.example.batch.tutorial.common.listener.ChunkErrorLoggingListener`がERRORログとして次のメッセージを出力していること。

  - 「The point exceeds 1000000.」

コンソールログ出力例

    (.. omitted)

    [2020/03/10 16:09:34] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddTasklet]] launched with the following parameters: [{jsr_batch_run_id=146}]
    [2020/03/10 16:09:34] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [jobPointAddTasklet.step01]
    [2020/03/10 16:09:35] [main] [c.e.b.t.c.l.ChunkErrorLoggingListener] [ERROR] The point exceeds 1000000.
    [2020/03/10 16:09:35] [main] [o.s.b.c.s.AbstractStep] [ERROR] Encountered an error executing step jobPointAddTasklet.step01 in job jobPointAddTasklet
    org.springframework.batch.item.validator.ValidationException: Validation failed for com.example.batch.tutorial.common.dto.MemberInfoDto@61514735:
    Field error in object 'item' on field 'point': rejected value [1000001]; codes [Max.item.point,Max.point,Max.int,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.point,point]; arguments []; default message [point],1000000]; default message [must be less than or equal to 1000000]
            at org.springframework.batch.item.validator.SpringValidator.validate(SpringValidator.java:54)

    (.. omitted)

    Caused by: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
    Field error in object 'item' on field 'point': rejected value [1000001]; codes [Max.item.point,Max.point,Max.int,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.point,point]; arguments []; default message [point],1000000]; default message [must be less than or equal to 1000000]
            ... 24 common frames omitted
    [2020/03/10 16:09:35] [main] [o.s.b.c.s.AbstractStep] [INFO ] Step: [jobPointAddTasklet.step01] executed in 193ms
    [2020/03/10 16:09:35] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddTasklet]] completed with the following parameters: [{jsr_batch_run_id=146}] and the following status: [FAILED] in 273ms

終了コードの確認

終了コードにより、異常終了したことを確認する。
確認手順はジョブの実行と結果の確認を参照。 終了コード(exit value)が255(異常終了)となっていることを確認する。

図 106. 終了コードの確認
出力リソースの確認

入力チェックを実装したジョブによって出力リソース(データベース or ファイル)を確認する。

タスクレットモデルの場合、一括コミット方式をとっているため、エラーが発生した場合は一切更新されていないことを確認してほしい。

会員情報テーブルの確認

更新前後の会員情報テーブルの内容を比較し、確認内容のとおりとなっていることを確認する。
確認手順はH2 Consoleを使用してデータベースを参照するを参照。

確認内容

  - すべてのレコードについて、データが更新されていないこと

初期状態の会員情報テーブルの内容を以下に示す。

図 107. 初期状態の会員情報テーブルの内容
会員情報ファイルの確認

会員情報ファイルの入出力内容を比較し、確認内容のとおりとなっていることを確認する。

確認内容

  - 出力ディレクトリに会員情報ファイルが空ファイルで出力されていること

    - 出力ファイル: files/output/output-member-info-data.csv

#### 9.4.5. try-catchで例外ハンドリングを行うジョブ

| 前提 チュートリアルの進め方で説明しているとおり、 入力データの妥当性検証を行うジョブに対して、 例外ハンドリングの実装を追加していく形式とする。なお、例外ハンドリング方式にはtry-catchやChunkListenerなど様々な方式がある。 ただし、記述はデータベースアクセスするジョブに実装を追加した場合の説明としているため留意すること。

##### 9.4.5.1. 概要

try-catchで例外ハンドリングを行うジョブを作成する。

なお、詳細についてはMacchinetta Batch 2.x 開発ガイドラインのItemProcessor内でtry～catchする方法および タスクレットモデルにおける例外ハンドリングを参照。

| 終了コードの意味合いについて 本節では、終了コードは2つの意味合いで扱われており、それぞれの説明を以下に示す。 COMPLETED、FAILEDなどの文字列の終了コードは、ジョブやステップの終了コードである。 0、255などの数値の終了コードは、Javaプロセスの終了コードである。

作成するアプリケーションの説明の 背景、処理概要、業務仕様を以下に再掲する。

###### 9.4.5.1.1. 背景

とある量販店では、会員に対してポイントカードを発行している。
会員には「ゴールド会員」「一般会員」の会員種別が存在し、会員種別に応じたサービスを提供している。
今回そのサービスの一環として、月内に商品を購入した会員のうち、 会員種別が「ゴールド会員」の場合は100ポイント、「一般会員」の場合は10ポイントを月末に加算することにした。

###### 9.4.5.1.2. 処理概要

会員種別に応じてポイント加算を行うアプリケーションを 月次バッチ処理としてMacchinetta Batch 2.xを使用して実装する。
入力データにポイントの上限値を超えるデータが存在するか妥当性検証を行う処理を追加実装し、 エラーの場合は警告メッセージを出力し、スキップして処理を継続する。その際にスキップしたことを示す終了コードを出力する。

###### 9.4.5.1.3. 業務仕様

業務仕様は以下のとおり。

- 入力データのポイントが1,000,000ポイントを超過していないことをチェックする

  - チェックエラーとなる場合は、警告メッセージをログに出力し、対象レコードはスキップして処理を継続する

  - スキップした場合は、スキップしたことを示すために終了コードを"200"(SKIPPED)に変換する

- 商品購入フラグが"1"(処理対象)の場合に、会員種別に応じてポイントを加算する

  - 会員種別が"G"(ゴールド会員)の場合は100ポイント、"N"(一般会員)の場合は10ポイント加算する

- 商品購入フラグはポイント加算後に"0"(初期状態)に更新する

- ポイントの上限値は1,000,000ポイントとする

- ポイント加算後に1,000,000ポイントを超えた場合は、1,000,000ポイントに補正する

###### 9.4.5.1.4. テーブル仕様

入出力リソースとなる会員情報テーブルの仕様は以下のとおり。
前提のとおりデータベースアクセスするジョブの場合の説明となるため、ファイルアクセスするジョブの場合の 入出力のリソース仕様はファイル仕様を参照。

表 245. 会員情報テーブル(member_info)
No | 属性名 | カラム名 | PK | データ型 | 桁数 | 説明
--- | --- | --- | --- | --- | --- | ---
1 | 会員番号 | id |  | CHAR | 8 | 会員を一意に示す8桁固定の番号を表す。
2 | 会員種別 | type | - | CHAR | 1 | 会員の種別を以下のとおり表す。 "G"(ゴールド会員)、"N"(一般会員)
3 | 商品購入フラグ | status | - | CHAR | 1 | 月内に商品を買ったかどうかを表す。 商品購入で"1"(処理対象)、月次バッチ処理で"0"(初期状態)に更新される。
4 | ポイント | point | - | INT | 7 | 会員の保有するポイントを表す。 初期値は0。

###### 9.4.5.1.5. ジョブの概要

ここで作成する入力チェックを行うジョブの概要を把握するために、 処理フローおよび処理シーケンスを以下に示す。

前提のとおりデータベースアクセスするジョブの場合の説明となるため、 ファイルアクセスするジョブの場合の処理フローおよび処理シーケンスとは異なる部分があるため留意する。

処理フロー概要

  処理フローの概要を以下に示す。

図 108. 例外ハンドリングを行うジョブの処理フロー

チャンクモデルの場合の処理シーケンス

  チャンクモデルの場合の処理シーケンスを説明する。
  本ジョブは異常系データを利用することを前提として説明しているため、 このシーケンス図は入力チェックでエラー(警告終了)となった場合を示している。

橙色のオブジェクトは今回実装するクラスを表す。

図 109. チャンクモデルのシーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

2. ステップは、リソースをオープンする。

3. `MyBatisCursorItemReader`は、member_infoテーブルから会員情報をすべて取得(select文の発行)する。

   - 入力データがなくなるまで、以降の処理を繰り返す。

   - チャンク単位で、フレームワークトランザクションを開始する。

   - チャンクサイズに達するまで4から12までの処理を繰り返す。

4. ステップは、`MyBatisCursorItemReader`から入力データを1件取得する。

5. `MyBatisCursorItemReader`は、member_infoテーブルから入力データを1件取得する。

6. member_infoテーブルは、`MyBatisCursorItemReader`に入力データを返却する。

7. `MyBatisCursorItemReader`は、ステップに入力データを返却する。

8. ステップは、`PointAddItemProcessor`で入力データに対して処理を行う。

9. `PointAddItemProcessor`は、`SpringValidator`に入力チェック処理を依頼する。

10. `SpringValidator`は、入力チェックルールに基づき入力チェックを行い、チェックエラーの場合は例外(ValidationException)をスローする。

11. `PointAddItemProcessor`は、入力データを読み込んでポイント加算処理を行う。例外(ValidationException)をキャッチした場合はnullを返却してエラーレコードをスキップする。

12. `PointAddItemProcessor`は、ステップに処理結果を返却する。

13. ステップは、チャンクサイズ分のデータを`MyBatisBatchItemWriter`で出力する。

14. `MyBatisBatchItemWriter`は、member_infoテーブルに対して会員情報の更新(update文の発行)を行う。

15. ステップはフレームワークトランザクションをコミットする。

16. ステップは`StepExitStatusChangeListener`を実行する。

17. `StepExitStatusChangeListener`は、入力データと出力データの件数が異なる場合に`StepExecution`に独自の終了コードとして`SKIPPED`を設定する。

18. ステップはジョブに終了コード(ここでは正常終了:0)を返却する。

19. ジョブは`JobExitCodeChangeListener`を実行する。

20. `JobExitCodeChangeListener`は`StepExecution`から終了コードを取得する。

21. `StepExecution`は`JobExitCodeChangeListener`に終了コードを返却する。

22. `JobExitCodeChangeListener`は最終的なジョブの終了コードとして、ジョブに`SKIPPED`(ここでは警告終了:200)を返却する。

タスクレットモデルの場合の処理シーケンス

  タスクレットモデルの場合の処理シーケンスについて説明する。
  本ジョブは異常系データを利用することを前提として説明しているため、 このシーケンス図は入力チェックでエラー(警告終了)となった場合を示している。

橙色のオブジェクトは今回実装するクラスを表す。

図 110. タスクレットモデルの処理シーケンス図
シーケンス図の説明

1. ジョブからステップが実行される。

   - ステップはフレームワークトランザクションを開始する。

2. ステップは`PointAddTasklet`を実行する。

3. `PointAddTasklet`は、リソースをオープンする。

4. `MyBatisCursorItemReader`は、member_infoテーブルから会員情報をすべて取得(select文の発行)する。

   - 入力データがなくなるまで5から13までの処理を繰り返す。

   - 一定件数に達するまで5から11までの処理を繰り返す。

5. `PointAddTasklet`は、`MyBatisCursorItemReader`から入力データを1件取得する。

6. `MyBatisCursorItemReader`は、member_infoテーブルから入力データを1件取得する。

7. member_infoテーブルは、`MyBatisCursorItemReader`に入力データを返却する。

8. `MyBatisCursorItemReader`は、タスクレットに入力データを返却する。

9. `PointAddTasklet`は、`SpringValidator`に入力チェック処理を依頼する。

10. `SpringValidator`は、入力チェックルールに基づき入力チェックを行い、チェックエラーの場合は例外(ValidationException)をスローする。

11. `PointAddTasklet`は、入力データを読み込んでポイント加算処理を行う。例外(ValidationException)をキャッチした場合はcontinueで処理を継続してエラーレコードをスキップする。

    - スキップした場合、以降の処理はせず5から処理を行う。

12. `PointAddTasklet`は、一定件数分のデータを`MyBatisBatchItemWriter`で出力する。

13. `MyBatisBatchItemWriter`は、member_infoテーブルに対して会員情報の更新(update文の発行)を行う。

14. `PointAddTasklet`は、`StepExecution`に独自の終了コードとして`SKIPPED`を設定する。

15. `PointAddTasklet`はステップへ処理終了を返却する。

16. ステップはフレームワークトランザクションをコミットする。

17. ステップはジョブに終了コード(ここでは正常終了:0)を返却する。

18. ステップは`JobExitCodeChangeListener`を実行する。

19. `JobExitCodeChangeListener`は`StepExecution`から終了コードを取得する。

20. `StepExecution`は`JobExitCodeChangeListener`に終了コードを返却する。

21. ステップはジョブに終了コード(ここでは警告終了:200)を返却する。

| 処理モデルによるスキップ実装について チャンクモデルとタスクレットモデルではスキップ処理の実装方法が異なる。 チャンクモデル ItemProcessor内でtry-catchを実装し、catchブロック内でnullを返却することでエラーデータをスキップする。 ItemReader、ItemWriterでのスキップはスキップを参照。 タスクレットモデル ビジネスロジック内でtry-catchを実装し、catchブロック内でcontinueなど以降の処理をせずに次のデータを取得するようにして処理を継続することでエラーデータをスキップする。

以降で、チャンクモデル、タスクレットモデルそれぞれの実装方法を説明する。

- チャンクモデルでの実装

- タスクレットモデルでの実装

##### 9.4.5.2. チャンクモデルでの実装

チャンクモデルで入力チェックを行うジョブの作成から実行までを以下の手順で実施する。

1. メッセージ定義の追加

2. 終了コードのカスタマイズ

3. 例外ハンドリングの実装

4. ジョブの実行と結果の確認

###### 9.4.5.2.1. メッセージ定義の追加

コード体系のばらつき防止や、監視対象のキーワードとしての抽出を設計しやすくするため、 ログメッセージはメッセージ定義を使用し、ログ出力時に使用する。

チャンクモデル/タスクレットモデルで共通して利用するため、既に作成している場合は読み飛ばしてよい。

`application-messages.properties`および`LaunchContextConfig.java/launch-context.xml`を以下のとおり設定する。
なお、`LaunchContextConfig.java/launch-context.xml`の設定はブランクプロジェクトに設定済みである。

src/main/resources/i18n/application-messages.properties

    # (1)
    errors.maxInteger=The {0} exceeds {1}.

JavaConfig
com.example.batch.tutorial.config.LaunchContextConfig.java

    // omitted
    @Bean
    public MessageSource messageSource() {
        final ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
        resourceBundleMessageSource.setBasename("i18n/application-messages"); // (2)
        return resourceBundleMessageSource;
    }
    // omitted

XMLConfig
META-INF/spring/launch-context.xml

    <!-- omitted -->

    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"
          p:basenames="i18n/application-messages" /> <!-- (2) -->

    <!-- omitted -->

表 246. 説明
項番 | 説明
--- | ---
(1) | ポイント上限超過時に出力するメッセージを設定する。 {0}に項目名、{1}に上限値を割り当てる。
(2) | プロパティファイルからメッセージを使用するために、`MessageSource`を設定する。

###### 9.4.5.2.2. 終了コードのカスタマイズ

ジョブ終了時のjavaプロセスの終了コードをカスタマイズする。
詳細は終了コードのカスタマイズを参照。

以下の作業を実施する。

1. StepExecutionListenerの実装

2. JobExecutionListenerの実装

3. ジョブBean定義ファイルの設定

4. 終了コードのマッピング定義

StepExecutionListenerの実装

`StepExecutionListener`インタフェースを利用してステップの終了コードを条件により変更する。
ここでは、`StepExecutionListener`インタフェースの実装クラスとして、 入力データと出力データの件数が異なる場合に終了コードを`SKIPPED`に変更する処理を実装する。

なお、このクラスはタスクレットモデルでは作成する必要がない。 タスクレットモデルではTaskletの実装クラス内で`StepExecution`クラスに独自の終了コードを設定することができるためである。

com.example.batch.tutorial.common.listener.StepExitStatusChangeListener

    package com.example.batch.tutorial.common.listener;

    import org.springframework.batch.core.ExitStatus;
    import org.springframework.batch.core.StepExecution;
    import org.springframework.batch.core.StepExecutionListener;
    import org.springframework.stereotype.Component;

    @Component
    public class StepExitStatusChangeListener implements StepExecutionListener {

        @Override
        public void beforeStep(StepExecution stepExecution) {
            // do nothing.
        }

        @Override
        public ExitStatus afterStep(StepExecution stepExecution) {
            ExitStatus exitStatus = stepExecution.getExitStatus();
            if (conditionalCheck(stepExecution)) {
                exitStatus = new ExitStatus("SKIPPED"); // (1)
            }
            return exitStatus;
        }

        private boolean conditionalCheck(StepExecution stepExecution) {
            return (stepExecution.getWriteCount() != stepExecution.getReadCount()); // (2)
        }
    }

表 247. 説明
項番 | 説明
--- | ---
(1) | ステップの実行結果に応じて独自の終了コードを設定する。 ここでは、スキップした場合に終了コードとして`SKIPPED`を指定する。
(2) | スキップしたことを判定するため、入力データと出力データの件数の比較を行う。 スキップした場合、入力データと出力データの件数が異なるため、件数の差を利用してスキップの判定を行っている。 ここで件数に差があった場合に(1)が実行される。

JobExecutionListenerの実装

`JobExecutionListener`インタフェースを利用してジョブの終了コードを条件により変更する。
ここでは、`JobExecutionListener`インタフェースの実装クラスとして、 最終的なジョブの終了コードを各ステップの終了コードに合わせて変更する処理を実装する。

com.example.batch.tutorial.common.listener.JobExitCodeChangeListener

    package com.example.batch.tutorial.common.listener;

    import org.springframework.batch.core.ExitStatus;
    import org.springframework.batch.core.JobExecution;
    import org.springframework.batch.core.JobExecutionListener;
    import org.springframework.batch.core.StepExecution;
    import org.springframework.stereotype.Component;

    import java.util.Collection;

    @Component
    public class JobExitCodeChangeListener implements JobExecutionListener {

        @Override
        public void beforeJob(JobExecution jobExecution) {
            // do nothing.
        }

        @Override
        public void afterJob(JobExecution jobExecution) {
            Collection<StepExecution> stepExecutions = jobExecution.getStepExecutions();
            for (StepExecution stepExecution : stepExecutions) { // (1)
                if ("SKIPPED".equals(stepExecution.getExitStatus().getExitCode())) {
                    jobExecution.setExitStatus(new ExitStatus("SKIPPED"));
                    break;
                }
            }
        }
    }

表 248. 説明
項番 | 説明
--- | ---
(1) | ジョブの実行結果に応じて、最終的なジョブの終了コードを`JobExecution`に設定する。 ここではステップから返却された終了コードのいずれかに`SKIPPED`が含まれている場合、 終了コードを`SKIPPED`としている。

ジョブBean定義ファイルの設定

作成したリスナーを利用するためのジョブBean定義ファイルの設定を以下に示す。

JavaConfig
com.example.batch.tutorial.config.dbaccess.JobPointAddChunkConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan({"com.example.batch.tutorial.dbaccess.chunk",
        "com.example.batch.tutorial.common.listener"}) // (1)
    @MapperScan(basePackages = "com.example.batch.tutorial.common.repository", sqlSessionFactoryRef = "jobSqlSessionFactory")
    public class JobPointAddChunkConfig {

        // omitted

        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           ItemReader<MemberInfoDto> reader,
                           PointAddItemProcessor processor,
                           ItemWriter<MemberInfoDto> writer,
                           StepExitStatusChangeListener listener) {
            return new StepBuilder("jobPointAddChunk.step01",
                    jobRepository)
                    .listener(listener) // (2)
                    .<MemberInfoDto, MemberInfoDto>chunk(10,
                            transactionManager)
                    .reader(reader)
                    .processor(processor)
                    .writer(writer)
                    .build();
        }

        @Bean
        public Job jobPointAddChunk(JobRepository jobRepository,
                                                 Step step01,
                                                 JobExitCodeChangeListener listener) {
            return new JobBuilder("jobPointAddChunk", jobRepository)
                    .start(step01)
                    .listener(listener) // (3)
                    .build();
        }
    }

XMLConfig
src/main/resources/META-INF/jobs/dbaccess/jobPointAddChunk.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd
                 http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

        <!-- omitted -->

        <context:component-scan base-package="com.example.batch.tutorial.dbaccess.chunk,
                com.example.batch.tutorial.common.listener"/> <!-- (1) -->

        <!-- omitted -->

        <batch:job id="jobPointAddChunk" job-repository="jobRepository">
            <batch:step id="jobPointAddChunk.step01">
                <batch:tasklet transaction-manager="jobTransactionManager">
                    <batch:chunk reader="reader"
                                 processor="pointAddItemProcessor"
                                 writer="writer" commit-interval="10"/>
                </batch:tasklet>
                <batch:listeners>
                    <batch:listener ref="stepExitStatusChangeListener"/> <!-- (2) -->
                </batch:listeners>
            </batch:step>
            <batch:listeners>
                <batch:listener ref="jobExitCodeChangeListener"/> <!-- (3) -->
            </batch:listeners>
        </batch:job>

    </beans>

表 249. 説明
項番 | 説明
--- | ---
(1) | コンポーネントスキャン対象とするベースパッケージの設定を行う。 `base-package`属性に、`StepExecutionListener`および`JobExecutionListener`の実装クラスが格納されているパッケージを追加で指定する。
(2) | `StepExecutionListener`の実装クラスを設定する。 なお、`StepExecutionListener`は`StepListener`の拡張インタフェースである。 ここでは、`StepExecutionListener`の実装クラスのBeanIDである`stepExitStatusChangeListener`を指定する。
(3) | `JobExecutionListener`の実装クラスを設定する。 ここでは、`JobExecutionListener`の実装クラスのBeanIDである`jobExitCodeChangeListener`を指定する。

| StepExitStatusChangeListenerとJobExitCodeChangeListenerの設定箇所の違いについて `StepExitStatusChangeListener`はステップの実行前後に処理を割り込める`StepListener`に属するため、`<batch:tasklet>`要素内に`<batch:listeners>`.`<batch:listener>`要素によって設定する。 `JobExitCodeChangeListener`はジョブの実行前後に処理を割り込める`JobListener`に属するため、`<batch:job>`要素内に`<batch:listeners>`.`<batch:listener>`要素によって設定する。 詳細はリスナーの設定を参照。

終了コードのマッピング定義

終了コードのマッピングを追加で設定する。

JavaConfig

`LaunchContextConfig.java`に以下のとおり、独自の終了コードを追加する。

com.example.batch.tutorial.config.LaunchContextConfig.java

    // omitted

    @Bean
    public ExitCodeMapper exitCodeMapper() {
        final SimpleJvmExitCodeMapper simpleJvmExitCodeMapper = new SimpleJvmExitCodeMapper();
        final Map<String, Integer> exitCodeMapper = new HashMap<>();
        // ExitStatus
        exitCodeMapper.put("NOOP", 0);
        exitCodeMapper.put("COMPLETED", 0);
        exitCodeMapper.put("STOPPED", 255);
        exitCodeMapper.put("FAILED", 255);
        exitCodeMapper.put("UNKNOWN", 255);
        exitCodeMapper.put("SKIPPED", 200); // (1)
        simpleJvmExitCodeMapper.setMapping(exitCodeMapper);
        return simpleJvmExitCodeMapper;
    }

    // omitted

XMLConfig

`launch-context.xml`に以下のとおり、独自の終了コードを追加する。

META-INF/spring/launch-context.xml

    <!-- omitted -->

    <bean id="exitCodeMapper" class="org.springframework.batch.core.launch.support.SimpleJvmExitCodeMapper">
        <property name="mapping">
            <util:map id="exitCodeMapper" key-type="java.lang.String"
                      value-type="java.lang.Integer">
                <!-- ExitStatus -->
                <entry key="NOOP" value="0" />
                <entry key="COMPLETED" value="0" />
                <entry key="STOPPED" value="255" />
                <entry key="FAILED" value="255" />
                <entry key="UNKNOWN" value="255" />
                <entry key="SKIPPED" value="200" /> <!-- (1) -->
            </util:map>
        </property>
    </bean>

    <!-- omitted -->

表 250. 説明
項番 | 説明
--- | ---
(1) | 独自の終了コードを追加する。 マッピングするためのキーに`SKIPPED`、コード値として`200`を指定する。

###### 9.4.5.2.3. 例外ハンドリングの実装

ポイント加算処理を行うビジネスロジッククラスにtry-catch処理を実装する。
既に実装してある`PointAddItemProcessor`クラスにtry-catch処理の実装を追加する。

前提のとおりデータベースアクセスするジョブの場合の説明となるため、 ファイルアクセスするジョブの場合の実装は以下の(1)～(5)のみ追加する。

com.example.batch.tutorial.dbaccess.chunk.PointAddItemProcessor

    // Package and the other import are omitted.

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.batch.item.validator.ValidationException;
    import org.springframework.context.MessageSource;

    import java.util.Locale;

    @Component
    public class PointAddItemProcessor implements ItemProcessor<MemberInfoDto, MemberInfoDto> {
        // Definition of constans are omitted.

        private static final Logger logger = LoggerFactory.getLogger(PointAddItemProcessor.class); // (1)

        @Inject
        Validator<MemberInfoDto> validator;

        @Inject
        MessageSource messageSource; // (2)

        @Override
        public MemberInfoDto process(MemberInfoDto item) throws Exception {
            try { // (3)
                validator.validate(item);
            } catch (ValidationException e) {
                logger.warn(messageSource
                        .getMessage("errors.maxInteger", new String[] { "point", "1000000" }, Locale.getDefault())); // (4)
                return null; // (5)
            }

            // The other codes of bussiness logic are omitted.
        }
    }

表 251. 説明
項番 | 説明
--- | ---
(1) | ログを出力するために`LoggerFactory`のインスタンスを定義する。
(2) | `MessageSource`のインスタンスをインジェクトする。
(3) | 例外ハンドリングを実装する。 入力チェック処理をtry-catchで囲み、`ValidationException`をハンドリングする。
(4) | プロパティファイルからメッセージIDが`errors.maxInteger`のメッセージを取得し、ログに出力している。
(5) | エラーレコードをスキップするためにnullを返却する。

###### 9.4.5.2.4. ジョブの実行と結果の確認

作成したジョブをSTS上で実行し、結果を確認する。

実行構成からジョブを実行

既に作成してある実行構成からジョブを実行し、結果を確認する。

ここでは、異常系データを利用してジョブを実行する。
例外ハンドリングを実装したジョブが扱うリソース(データベース or ファイル)によって、 入力データの切替方法が異なるため、以下のとおり実行すること。

データベースアクセスでデータ入出力を行うジョブに対して例外ハンドリングを実装した場合

  データベースアクセスでデータ入出力を行うジョブの実行構成からジョブを実行 で作成した実行構成を使ってジョブを実行する。

異常系データを利用するために、`batch-application.properties`のDatabase Initializeで 正常系データのスクリプトをコメントアウトし、異常系データのスクリプトのコメントアウトを解除する。

src/main/resources/batch-application.properties

    # Database Initialize
    tutorial.create-table.script=file:sqls/create-member-info-table.sql
    #tutorial.insert-data.script=file:sqls/insert-member-info-data.sql
    tutorial.insert-data.script=file:sqls/insert-member-info-error-data.sql

ファイルアクセスでデータ入出力を行うジョブに対して例外ハンドリングを実装した場合

  ファイルアクセスでデータ入出力を行うジョブの実行構成からジョブを実行 で作成した実行構成を使ってジョブを実行する。

異常系データを利用するために、実行構成で設定する引数のうち、 入力ファイル(inputFile)のパスを正常系データ(input-member-info-data.csv)から異常系データ(input-member-info-error-data.csv)に変更する。

コンソールログの確認

Console Viewを開き、以下の内容のログが出力されていることを確認する。

- 処理が完了(COMPLETED)し、例外が発生していないこと。

- WARNログとして次のメッセージを出力していること。

  - 「The point exceeds 1000000.」

コンソールログ出力例

    (.. omitted)

    [2020/03/10 16:13:54] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddChunk]] launched with the following parameters: [{jsr_batch_run_id=148}]
    [2020/03/10 16:13:54] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [jobPointAddChunk.step01]
    [2020/03/10 16:13:54] [main] [c.e.b.t.d.c.PointAddItemProcessor] [WARN ] The point exceeds 1000000.
    [2020/03/10 16:13:54] [main] [o.s.b.c.s.AbstractStep] [INFO ] Step: [jobPointAddChunk.step01] executed in 237ms
    [2020/03/10 16:13:54] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddChunk]] completed with the following parameters: [{jsr_batch_run_id=148}] and the following status: [COMPLETED] in 304ms

終了コードの確認

終了コードにより、警告終了したことを確認する。
確認手順はジョブの実行と結果の確認を参照。 終了コード(exit value)が200(警告終了)となっていることを確認する。

図 111. 終了コードの確認
出力リソースの確認

例外ハンドリングを実装したジョブによって出力リソース(データベース or ファイル)を確認する。

スキップを実装しているため、エラーレコード以外の更新対象レコードについては 正常に更新されていることを確認する。

会員情報テーブルの確認

更新前後の会員情報テーブルの内容を比較し、確認内容のとおりとなっていることを確認する。
確認手順はH2 Consoleを使用してデータベースを参照するを参照。

確認内容

  - エラーレコード(会員番号が"000000013")を除くすべてのレコードについて

    - statusカラム

      - "1"(処理対象)から"0"(初期状態)に更新されていること

    - pointカラム

      - ポイント加算対象について、会員種別に応じたポイントが加算されていること

        - typeカラムが"G"(ゴールド会員)の場合は100ポイント

        - typeカラムが"N"(一般会員)の場合は10ポイント

  - エラーレコード(会員番号が"000000013")について

    - 更新されていないこと(破線の赤枠で示した範囲)

更新前後の会員情報テーブルの内容は以下のとおり。

図 112. 更新前後の会員情報テーブルの内容
会員情報ファイルの確認

会員情報ファイルの入出力内容を比較し、確認内容のとおりとなっていることを確認する。

確認内容

  - 出力ディレクトリに会員情報ファイルが出力されていること

    - 出力ファイル: files/output/output-member-info-data.csv

  - エラーレコード(会員番号が"00000013")を除くすべてのレコードについて

    - statusフィールド

      - "1"(処理対象)から"0"(初期状態)に更新されていること

    - pointフィールド

      - ポイント加算対象について、会員種別に応じたポイントが加算されていること

        - typeフィールドが"G"(ゴールド会員)の場合は100ポイント

        - typeフィールドが"N"(一般会員)の場合は10ポイント

  - エラーレコード(会員番号が"00000013")について

    - 出力されていないこと(破線の赤枠で示した範囲)

会員情報ファイルの入出力内容は以下のとおり。
ファイルのフィールドはid(会員番号)、type(会員種別)、status(商品購入フラグ)、point(ポイント)の順で出力される。

図 113. 会員情報ファイルの入出力内容

##### 9.4.5.3. タスクレットモデルでの実装

タスクレットモデルで入力チェックを行うジョブの作成から実行までを以下の手順で実施する。

1. メッセージ定義の追加

2. 終了コードのカスタマイズ

3. 例外ハンドリングの実装

4. ジョブの実行と結果の確認

###### 9.4.5.3.1. メッセージ定義の追加

コード体系のばらつき防止や、監視対象のキーワードとしての抽出を設計しやすくするため、 ログメッセージはメッセージ定義を使用し、ログ出力時に使用する。

チャンクモデル/タスクレットモデルで共通して利用するため、既に作成している場合は読み飛ばしてよい。

`application-messages.properties`および`LaunchContextConfig.java/launch-context.xml`を以下のとおり設定する。
なお、`LaunchContextConfig.java/launch-context.xml`の設定はブランクプロジェクトに設定済みである。

src/main/resources/i18n/application-messages.properties

    # (1)
    errors.maxInteger=The {0} exceeds {1}.

JavaConfig
com.example.batch.tutorial.config.LaunchContextConfig.java

    // omitted
    @Bean
    public MessageSource messageSource() {
        final ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
        resourceBundleMessageSource.setBasename("i18n/application-messages"); // (2)
        return resourceBundleMessageSource;
    }
    // omitted

XMLConfig
META-INF/spring/launch-context.xml

    <!-- omitted -->

    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"
          p:basenames="i18n/application-messages" /> <!-- (2) -->

    <!-- omitted -->

表 252. 説明
項番 | 説明
--- | ---
(1) | ポイント上限超過時に出力するメッセージを設定する。 {0}に項目名、{1}に上限値を割り当てる。
(2) | プロパティファイルからメッセージを使用するために、`MessageSource`を設定する。

###### 9.4.5.3.2. 終了コードのカスタマイズ

ジョブ終了時のjavaプロセスの終了コードをカスタマイズする。
詳細は終了コードのカスタマイズを参照。

以下の作業を実施する。

1. JobExecutionListenerの実装

2. ジョブBean定義ファイルの設定

3. 終了コードのマッピング定義

JobExecutionListenerの実装

`JobExecutionListener`インタフェースを利用してジョブの終了コードを条件により変更する。
ここでは、`JobExecutionListener`インタフェースの実装クラスとして、 最終的なジョブの終了コードを各ステップの終了コードに合わせて変更する処理を実装する。

com.example.batch.tutorial.common.listener.JobExitCodeChangeListener

    package com.example.batch.tutorial.common.listener;

    import org.springframework.batch.core.ExitStatus;
    import org.springframework.batch.core.JobExecution;
    import org.springframework.batch.core.JobExecutionListener;
    import org.springframework.batch.core.StepExecution;
    import org.springframework.stereotype.Component;

    import java.util.Collection;

    @Component
    public class JobExitCodeChangeListener implements JobExecutionListener {

        @Override
        public void beforeJob(JobExecution jobExecution) {
            // do nothing.
        }

        @Override
        public void afterJob(JobExecution jobExecution) {
            Collection<StepExecution> stepExecutions = jobExecution.getStepExecutions();
            for (StepExecution stepExecution : stepExecutions) { // (1)
                if ("SKIPPED".equals(stepExecution.getExitStatus().getExitCode())) {
                    jobExecution.setExitStatus(new ExitStatus("SKIPPED"));
                    break;
                }
            }
        }
    }

表 253. 説明
項番 | 説明
--- | ---
(1) | ジョブの実行結果に応じて、最終的なジョブの終了コードを`JobExecution`に設定する。 ここではステップから返却された終了コードのいずれかに`SKIPPED`が含まれている場合、 終了コードを`SKIPPED`としている。

ジョブBean定義ファイルの設定

作成したリスナーを利用するためのジョブBean定義ファイルの設定を以下に示す。

JavaConfig
com.example.batch.tutorial.config.dbaccess.JobPointAddTaskletConfig.java

    @Configuration
    @Import(JobBaseContextConfig.class)
    @ComponentScan({"com.example.batch.tutorial.dbaccess.tasklet",
        "com.example.batch.tutorial.common.listener"}) // (1)
    @MapperScan(basePackages = "com.example.batch.tutorial.common.repository", sqlSessionFactoryRef = "jobSqlSessionFactory")
    public class JobPointAddTaskletConfig {

        // omitted

        @Bean
        public Step step01(JobRepository jobRepository,
                           @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                           PointAddTasklet tasklet) {
            return new StepBuilder("jobPointAddTasklet.step01",
                    jobRepository)
                    .tasklet(tasklet, transactionManager)
                    .build();
        }

        @Bean
        public Job jobPointAddTasklet(JobRepository jobRepository,
                                                 Step step01,
                                                 JobExitCodeChangeListener listener) {
            return new JobBuilder("jobPointAddTasklet", jobRepository)
                    .start(step01)
                    .listener(listener) // (2)
                    .build();
        }
    }

XMLConfig
src/main/resources/META-INF/jobs/dbaccess/jobPointAddTasklet.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:batch="http://www.springframework.org/schema/batch"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                 http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd
                 http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

        <!-- omitted -->

        <context:component-scan base-package="com.example.batch.tutorial.dbaccess.tasklet,
                com.example.batch.tutorial.common.listener"/> <!-- (1) -->

        <!-- omitted -->

        <batch:job id="jobPointAddTasklet" job-repository="jobRepository">
            <batch:step id="jobPointAddTasklet.step01">
                <batch:tasklet transaction-manager="jobTransactionManager"
                               ref="pointAddTasklet"/>
            </batch:step>
            <batch:listeners>
                <batch:listener ref="jobExitCodeChangeListener"/> <!-- (2) -->
            </batch:listeners>
        </batch:job>

    </beans>

表 254. 説明
項番 | 説明
--- | ---
(1) | コンポーネントスキャン対象とするベースパッケージの設定を行う。 `basePackages属性/base-package属性`に、`StepExecutionListener`および`JobExecutionListener`の実装クラスが格納されているパッケージを追加で指定する。
(2) | `JobExecutionListener`の実装クラスを設定する。 なお、`JobExecutionListener`は`JobListener`の拡張インタフェースである。 ここでは、`JobExecutionListener`の実装クラスのBeanIDである`jobExitCodeChangeListener`を指定する。

終了コードのマッピング定義

終了コードのマッピングを追加で設定する。

チャンクモデル/タスクレットモデルで共通して利用するため、既に実施している場合は読み飛ばしてよい。

JavaConfig

`LaunchContextConfig.java`に以下のとおり、独自の終了コードを追加する。

com.example.batch.tutorial.config.LaunchContextConfig.java

    // omitted

    @Bean
    public ExitCodeMapper exitCodeMapper() {
        final SimpleJvmExitCodeMapper simpleJvmExitCodeMapper = new SimpleJvmExitCodeMapper();
        final Map<String, Integer> exitCodeMapper = new HashMap<>();
        // ExitStatus
        exitCodeMapper.put("NOOP", 0);
        exitCodeMapper.put("COMPLETED", 0);
        exitCodeMapper.put("STOPPED", 255);
        exitCodeMapper.put("FAILED", 255);
        exitCodeMapper.put("UNKNOWN", 255);
        exitCodeMapper.put("SKIPPED", 200);  // (1)
        simpleJvmExitCodeMapper.setMapping(exitCodeMapper);
        return simpleJvmExitCodeMapper;
    }

    // omitted

XMLConfig

`launch-context.xml`に以下のとおり、独自の終了コードを追加する。

META-INF/spring/launch-context.xml

    <!-- omitted -->

    <bean id="exitCodeMapper" class="org.springframework.batch.core.launch.support.SimpleJvmExitCodeMapper">
        <property name="mapping">
            <util:map id="exitCodeMapper" key-type="java.lang.String"
                      value-type="java.lang.Integer">
                <!-- ExitStatus -->
                <entry key="NOOP" value="0" />
                <entry key="COMPLETED" value="0" />
                <entry key="STOPPED" value="255" />
                <entry key="FAILED" value="255" />
                <entry key="UNKNOWN" value="255" />
                <entry key="SKIPPED" value="200" /> <!-- (1) -->
            </util:map>
        </property>
    </bean>

    <!-- omitted -->

表 255. 説明
項番 | 説明
--- | ---
(1) | 独自の終了コードを追加する。 マッピングするためのキーに`SKIPPED`、コード値として`200`を指定する。

###### 9.4.5.3.3. 例外ハンドリングの実装

ポイント加算処理を行うビジネスロジッククラスにtry-catch処理を実装する。
既に実装してある`PointAddItemProcessor`クラスにtry-catch処理の実装を追加する。

前提のとおりデータベースアクセスするジョブの場合の説明となるため、 ファイルアクセスするジョブの場合の実装は以下の(1)～(7)のみ追加する。

com.example.batch.tutorial.dbaccess.tasklet.PointAddTasklet

    // Package and the other import are omitted.

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.batch.core.ExitStatus;
    import org.springframework.batch.item.validator.ValidationException;
    import org.springframework.context.MessageSource;

    import java.util.Locale;

    @Component
    public class PointAddTasklet implements Tasklet {
        // Definition of constans, ItemStreamReader and ItemWriter are omitted.

        private static final Logger logger = LoggerFactory.getLogger(PointAddTasklet.class); // (1)

        @Inject
        Validator<MemberInfoDto> validator;

        @Inject
        MessageSource messageSource; // (2)

        @Override
        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
            MemberInfoDto item = null;

            List<MemberInfoDto> items = new ArrayList<>(CHUNK_SIZE);
            int errorCount = 0; // (3)

            try {
                reader.open(chunkContext.getStepContext().getStepExecution().getExecutionContext());
                while ((item = reader.read()) != null) {
                    try { // (4)
                        validator.validate(item);
                    } catch (ValidationException e) {
                        logger.warn(messageSource
                                .getMessage("errors.maxInteger", new String[] { "point", "1000000" }, Locale.getDefault()));  // (5)
                        errorCount++;
                        continue; // (6)
                    }

                    // The other codes of bussiness logic are omitted.
                }

                writer.write(new Chunk(items));
            } finally {
                reader.close();
            }
            if (errorCount > 0) {
                contribution.setExitStatus(new ExitStatus("SKIPPED")); // (7)
            }
            return RepeatStatus.FINISHED;
        }
    }

表 256. 説明
項番 | 説明
--- | ---
(1) | ログを出力するために`LoggerFactory`のインスタンスを定義する。
(2) | `MessageSource`のインスタンスをインジェクトする。
(3) | 例外の発生を判定するためのカウンターを用意する。 `ValidationException`をキャッチした際にインクリメントする。
(4) | 例外ハンドリングを実装する。 入力チェック処理をtry-catchで囲み、`ValidationException`をハンドリングする。
(5) | プロパティファイルからメッセージIDが`errors.maxInteger`のメッセージを取得し、ログに出力している。
(6) | エラーレコードをスキップするためにcontinueで処理を継続する。
(7) | 独自の終了コードとして`SKIPPED`を設定する。

###### 9.4.5.3.4. ジョブの実行と結果の確認

作成したジョブをSTS上で実行し、結果を確認する。

実行構成からジョブを実行

既に作成してある実行構成からジョブを実行し、結果を確認する。

ここでは、異常系データを利用してジョブを実行する。
例外ハンドリングを実装したジョブが扱うリソース(データベース or ファイル)によって、 入力データの切替方法が異なるため、以下のとおり実行すること。

データベースアクセスでデータ入出力を行うジョブに対して例外ハンドリングを実装した場合

  データベースアクセスでデータ入出力を行うジョブの実行構成からジョブを実行 で作成した実行構成を使ってジョブを実行する。

異常系データを利用するために、`batch-application.properties`のDatabase Initializeで 正常系データのスクリプトをコメントアウトし、異常系データのスクリプトのコメントアウトを解除する。

src/main/resources/batch-application.properties

    # Database Initialize
    tutorial.create-table.script=file:sqls/create-member-info-table.sql
    #tutorial.insert-data.script=file:sqls/insert-member-info-data.sql
    tutorial.insert-data.script=file:sqls/insert-member-info-error-data.sql

ファイルアクセスでデータ入出力を行うジョブに対して例外ハンドリングを実装した場合

  ファイルアクセスでデータ入出力を行うジョブの実行構成からジョブを実行 で作成した実行構成を使ってジョブを実行する。

異常系データを利用するために、実行構成で設定する引数のうち、 入力ファイル(inputFile)のパスを正常系データ(input-member-info-data.csv)から異常系データ(input-member-info-error-data.csv)に変更する。

コンソールログの確認

Console Viewを開き、以下の内容のログが出力されていることを確認する。

- 処理が完了(COMPLETED)し、例外が発生していないこと。

- WARNログとして次のメッセージを出力していること。

  - 「The point exceeds 1000000.」

コンソールログ出力例

    (.. omitted)

    [2020/03/10 16:15:14] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddTasklet]] launched with the following parameters: [{jsr_batch_run_id=152}]
    [2020/03/10 16:15:14] [main] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [jobPointAddTasklet.step01]
    [2020/03/10 16:15:14] [main] [c.e.b.t.d.t.PointAddTasklet] [WARN ] The point exceeds 1000000.
    [2020/03/10 16:15:14] [main] [o.s.b.c.s.AbstractStep] [INFO ] Step: [jobPointAddTasklet.step01] executed in 204ms
    [2020/03/10 16:15:14] [main] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddTasklet]] completed with the following parameters: [{jsr_batch_run_id=152}] and the following status: [COMPLETED] in 271ms

終了コードの確認

終了コードにより、警告終了したことを確認する。
確認手順はジョブの実行と結果の確認を参照。 終了コード(exit value)が200(警告終了)となっていることを確認する。

図 114. 終了コードの確認
出力リソースの確認

例外ハンドリングを実装したジョブによって出力リソース(データベース or ファイル)を確認する。

スキップを実装しているため、エラーレコード以外の更新対象レコードについては 正常に更新されていることを確認する。

会員情報テーブルの確認

更新前後の会員情報テーブルの内容を比較し、確認内容のとおりとなっていることを確認する。
確認手順はH2 Consoleを使用してデータベースを参照するを参照。

確認内容

  - エラーレコード(会員番号が"000000013")を除くすべてのレコードについて

    - statusカラム

      - "1"(処理対象)から"0"(初期状態)に更新されていること

    - pointカラム

      - ポイント加算対象について、会員種別に応じたポイントが加算されていること

        - typeカラムが"G"(ゴールド会員)の場合は100ポイント

        - typeカラムが"N"(一般会員)の場合は10ポイント

  - エラーレコード(会員番号が"000000013")について

    - 更新されていないこと(破線の赤枠で示した範囲)

更新前後の会員情報テーブルの内容は以下のとおり。

図 115. 更新前後の会員情報テーブルの内容
会員情報ファイルの確認

会員情報ファイルの入出力内容を比較し、確認内容のとおりとなっていることを確認する。

確認内容

  - 出力ディレクトリに会員情報ファイルが出力されていること

    - 出力ファイル: files/output/output-member-info-data.csv

  - エラーレコード(会員番号が"00000013")を除くすべてのレコードについて

    - statusフィールド

      - "1"(処理対象)から"0"(初期状態)に更新されていること

    - pointフィールド

      - ポイント加算対象について、会員種別に応じたポイントが加算されていること

        - typeフィールドが"G"(ゴールド会員)の場合は100ポイント

        - typeフィールドが"N"(一般会員)の場合は10ポイント

  - エラーレコード(会員番号が"00000013")について

    - 出力されていないこと(破線の赤枠で示した範囲)

会員情報ファイルの入出力内容は以下のとおり。
ファイルのフィールドはid(会員番号)、type(会員種別)、status(商品購入フラグ)、point(ポイント)の順で出力される。

図 116. 会員情報ファイルの入出力内容

#### 9.4.6. 非同期実行方式のジョブ

| 前提 チュートリアルの進め方で説明しているとおり、 既に作成済みのジョブに対して、非同期実行していく形式とする。 非同期実行方式にはDBポーリングを利用する方式と Webコンテナを利用する方式があるが、 チュートリアルではDBポーリングを利用する方式の説明を行う。

##### 9.4.6.1. 概要

DBポーリングを利用してジョブを非同期実行する。

なお、詳細についてはMacchinetta Batch 2.x 開発ガイドラインの非同期実行(DBポーリング)を参照。
また、アプリケーションの背景、処理概要、業務仕様は、バッチジョブの実装の各チュートリアルジョブを参照。

以降では、DBポーリングによるジョブの非同期実行方法を以下の手順で説明する。

1. 準備

2. 非同期バッチデーモンを起動

3. ジョブ情報をジョブ要求テーブルに登録

4. ジョブの実行と結果の確認

5. 非同期バッチデーモンの停止

6. ジョブ実行状態の確認

##### 9.4.6.2. 準備

非同期実行(DBポーリング)を行うための準備作業を実施する。

実施する作業は以下のとおり。

1. ポーリング処理の設定

2. ジョブの設定

3. 入力リソースの設定

###### 9.4.6.2.1. ポーリング処理の設定

非同期実行に必要な設定は、`batch-application.properties`で行う。
ブランクプロジェクトには設定済みであるため、詳細な説明は割愛する。 各項目の説明は各種設定のポーリング処理の設定を参照。

src/main/resources/batch-application.properties

    # TERASOLUNA AsyncBatchDaemon settings.
    # (1)
    async-batch-daemon.scheduler.size=1
    async-batch-daemon.schema.script=classpath:org/terasoluna/batch/async/db/schema-h2.sql
    # (2)
    async-batch-daemon.job-concurrency-num=3
    # (3)
    async-batch-daemon.polling-interval=10000
    # (4)
    async-batch-daemon.polling-initial-delay=1000
    # (5)
    async-batch-daemon.polling-stop-file-path=/tmp/stop-async-batch-daemon

表 257. 説明
項番 | 説明
--- | ---
(1) | DBポーリング処理で起動される`TaskScheduler`のスレッドプールサイズを設定する。
(2) | ポーリング時に一括で取得する件数を設定する。
(3) | ポーリング周期(ミリ秒単位)を設定する。 前回タスクの実行完了時点から指定時間後にタスクを実行する。 ここでは、10000ミリ秒(10秒)を指定する。
(4) | ポーリング初回起動遅延時間(ミリ秒単位)を設定する。 アプリケーションの起動から指定時間後にポーリングを初回実行する。 ここでは、1000ミリ秒(1秒)を指定する。
(5) | 非同期バッチデーモンを停止させるための終了ファイルパスを設定する。 本チュートリアルは、Windows環境で実施することを前提としているため、 この設定の場合はC:\tmp配下にstop-async-batch-daemonファイルを置くことになる。

###### 9.4.6.2.2. ジョブの設定

JavaConfig

非同期実行する対象のジョブは、`AsyncBatchDaemonConfig.java`の`automaticJobRegistrar`に設定する。

例としてデータベースアクセスでデータ入出力を行うジョブ(チャンクモデル)を指定した設定を以下に示す。

com.example.batch.tutorial.config.AsyncBatchDaemonConfig.java

    // omitted
    @Bean
    public AutomaticJobRegistrar automaticJobRegistrar(ResourceLoader resourceLoader, JobRegistry jobRegistry,
                                                       ApplicationContextFactory[] applicationContextFactories) throws Exception {
        final AutomaticJobRegistrar automaticJobRegistrar = new AutomaticJobRegistrar();
        final DefaultJobLoader defaultJobLoader = new DefaultJobLoader();
        defaultJobLoader.setJobRegistry(jobRegistry);
        automaticJobRegistrar.setApplicationContextFactories(applicationContextFactories);
        automaticJobRegistrar.setJobLoader(defaultJobLoader);
        return automaticJobRegistrar;
    }

    @Bean
    public ApplicationContextFactory[] applicationContextFactories(final ApplicationContext ctx) throws IOException {
        return new ApplicationContextFactoryHelper(ctx).load("classpath:org/terasoluna/batch/tutorial/config/dbaccess/JobPointAddChunkConfig.class"); // (1)
    }
    // omitted

表 258. 説明
項番 | 説明
--- | ---
(1) | 非同期実行する対象ジョブのBean定義ファイルを指定する。 ワイルドカード(**/*)の利用も可能である。 ジョブの指定に際してはジョブの設定に記載してある注意事項を参照。 `ApplicationContextFactoryHelper` は JavaConfigで `ApplicationContextFactory` の生成を補助するヘルパクラスである。コンストラクタには各ジョブで親となる共通定義の `ApplicationContext` を指定する。

XMLConfig

非同期実行する対象のジョブは、`async-batch-daemon.xml`の`automaticJobRegistrar`に設定する。

例としてデータベースアクセスでデータ入出力を行うジョブ(チャンクモデル)を指定した設定を以下に示す。

META-INF/spring/async-batch-daemon.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:jdbc="http://www.springframework.org/schema/jdbc"
           xmlns:c="http://www.springframework.org/schema/c"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:task="http://www.springframework.org/schema/task"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd
                http://www.springframework.org/schema/task https://www.springframework.org/schema/task/spring-task.xsd
                http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

        <!-- omitted -->

        <bean id="automaticJobRegistrar" class="org.springframework.batch.core.configuration.support.AutomaticJobRegistrar">
            <property name="applicationContextFactories">
                <bean class="org.springframework.batch.core.configuration.support.ClasspathXmlApplicationContextsFactoryBean"
                    p:resources="classpath:/META-INF/jobs/dbaccess/jobPointAddChunk.xml" /> <!-- (1) -->
            </property>
            <property name="jobLoader">
                <bean class="org.springframework.batch.core.configuration.support.DefaultJobLoader"
                    p:jobRegistry-ref="jobRegistry" />
            </property>
        </bean>

        <!-- omitted -->

    </beans>

表 259. 説明
項番 | 説明
--- | ---
(1) | 非同期実行する対象ジョブのBean定義ファイルを指定する。 ワイルドカード(**/*)の利用も可能である。 ジョブの指定に際してはジョブの設定に記載してある注意事項を参照。

| ジョブ設計上の留意点 非同期実行(DBポーリング)の特性上、同一ジョブの並列実行が可能になっているので、並列実行した場合に同一ジョブが影響を与えないようにする必要がある。 本チュートリアルでは、データベースアクセスのジョブとファイルアクセスのジョブで同じジョブIDを用いている。 チュートリアルの中で、これらのジョブを並列実行することはないが、同じジョブIDのジョブを複数指定する場合はエラーとなってしまうため、 ジョブの設計時に留意する必要がある。

###### 9.4.6.2.3. 入力リソースの設定

非同期実行でジョブを実行する際の入力リソース(データベース or ファイル)の設定を行う。
ここでは、正常系データを利用するジョブを実行する。

データベースアクセスするジョブとファイルアクセスするジョブの場合の入力リソースの設定を以下に示す。

データベースアクセスするジョブの場合

  `batch-application.properties`のDatabase Initializeのスクリプトを以下のとおり設定する。

src/main/resources/batch-application.properties

    # Database Initialize
    tutorial.create-table.script=file:sqls/create-member-info-table.sql
    tutorial.insert-data.script=file:sqls/insert-member-info-data.sql
    #tutorial.insert-data.script=file:sqls/insert-member-info-error-data.sql

ファイルアクセスするジョブの場合

  事前に、入力ファイルが配備されていること、および出力ディレクトリが存在することを確認しておくこと。

  - 入力ファイル

    - files/input/input-member-info-data.csv

  - 出力ディレクトリ

    - files/output/

| 本チュートリアルにおける入力リソースのデータ準備について データベースアクセスするジョブの場合、非同期バッチデーモン起動時(ApplicationContext生成時)にINSERTのSQLを実行し、 データベースにデータを準備している。 ファイルアクセスするジョブの場合、入力ファイルをディレクトリに配置し、 ジョブ要求テーブルへジョブ情報の登録時にそのジョブ情報のパラメータ部として入出力ファイルのパスを指定する。

##### 9.4.6.3. 非同期バッチデーモンを起動

Macchinetta Batch 2.xが提供する`AsyncBatchDaemon`を起動する。

実行構成を以下のとおり作成し、非同期バッチデーモンを起動する。
作成手順はRun Configuration(実行構成)の作成を参照。

JavaConfig

表 260. Run Configurationsで設定する値
項目名(タブ名) | 値
--- | ---
Name | 任意の名称(例: Run Job With AsyncBatchDaemon)
Project(Main) | terasoluna-batch-tutorial
Main class(Main) | org.terasoluna.batch.async.db.AsyncBatchDaemon
Program arguments(Arguments) | com.example.batch.tutorial.config.AsyncBatchDaemonConfig

XMLConfig

表 261. Run Configurationsで設定する値
項目名(タブ名) | 値
--- | ---
Name | 任意の名称(例: Run Job With AsyncBatchDaemon)
Project(Main) | terasoluna-batch-tutorial
Main class(Main) | org.terasoluna.batch.async.db.AsyncBatchDaemon
Program arguments(Arguments) | META-INF/spring/async-batch-daemon.xml

非同期バッチデーモンを起動すると、ポーリングプロセスが10秒間隔(`batch-application.properties`の`async-batch-daemon.polling-interval`に指定したミリ秒)で実行される。
ログの出力例を以下に示す。
このログではポーリングプロセスが3回実行されたことがわかる。

コンソールログの出力例

    [2020/03/10 17:26:03] [main] [o.t.b.a.d.AsyncBatchDaemon] [INFO ] Async Batch Daemon start.

    (.. omitted)

    [2020/03/10 17:26:05] [main] [o.s.s.c.ThreadPoolTaskExecutor] [INFO ] Initializing ExecutorService
    [2020/03/10 17:26:05] [main] [o.s.s.c.ThreadPoolTaskScheduler] [INFO ] Initializing ExecutorService 'daemonTaskScheduler'
    [2020/03/10 17:26:05] [main] [o.t.b.a.d.AsyncBatchDaemon] [INFO ] Async Batch Daemon will start watching the creation of a polling stop file. [Path:\tmp\stop-async-batch-daemon]
    [2020/03/10 17:26:06] [daemonTaskScheduler-1] [o.t.b.a.d.JobRequestPollTask] [INFO ] Polling processing.
    [2020/03/10 17:26:16] [daemonTaskScheduler-1] [o.t.b.a.d.JobRequestPollTask] [INFO ] Polling processing.
    [2020/03/10 17:26:26] [daemonTaskScheduler-1] [o.t.b.a.d.JobRequestPollTask] [INFO ] Polling processing.

##### 9.4.6.4. ジョブ情報をジョブ要求テーブルに登録

ジョブを実行するための情報をジョブ要求テーブル(batch_job_request)に登録するSQL(INSERT文)を発行する。

ジョブ要求テーブルのテーブル仕様はジョブ要求テーブルの構造を参照。

STS上でSQLを実行する方法を以下に記す。

SQL実行手順

1. H2 Consoleを表示する。
   H2 Consoleの表示手順はH2 Consoleを使用してデータベースを参照するを参照。

2. SQLを記述する。
   データベースアクセスするジョブとファイルアクセスするジョブを実行するためのSQLをチャンクモデルの例で以下に示す。

データベースアクセスするジョブの場合

  記述するSQLを以下に示す。

データベースアクセスするジョブの実行要求用SQL

    INSERT INTO batch_job_request(job_name,job_parameter,polling_status,create_date)
    VALUES ('jobPointAddChunk', '', 'INIT', current_timestamp);

ファイルアクセスするジョブの場合

  記述するSQLを以下に示す。

ファイルアクセスするジョブの実行要求用SQL

    INSERT INTO batch_job_request(job_name,job_parameter,polling_status,create_date)
    VALUES ('jobPointAddChunk', 'inputFile=files/input/input-member-info-data.csv outputFile=files/output/output-member-info-data.csv', 'INIT', current_timestamp);

SQL記述後のイメージを以下に記す。
ここでは、データベースアクセスするジョブの実行要求用SQLを記述している。

3. [Shift]キー + [Enter]キーでSQLを実行する。

図 117. Execute SQL

4. ジョブ要求テーブルを確認する。
   下図のとおり、ジョブ要求テーブルにジョブを実行するための情報が登録されていることを確認する。
   `POLLING_STATUS`は`INIT`で登録したが、既にポーリングが行われた場合は、`POLLING_STATUS`が`POLLED`もしくは`EXECUTED`となっている。
   `POLLING_STATUS`の詳細についてはポーリングステータス(polling_status)の遷移パターンを参照。

図 118. ジョブ要求テーブルの確認

##### 9.4.6.5. ジョブの実行と結果の確認

非同期実行対象ジョブの実行結果を確認する。

###### 9.4.6.5.1. コンソールログの確認

Console Viewを開き、以下の内容のログが出力されていることを確認する。

- 処理が完了(COMPLETED)し、例外が発生していないこと。

コンソールログ出力例

    (.. omitted)

    [2020/03/10 17:27:06] [daemonTaskScheduler-1] [o.t.b.a.d.JobRequestPollTask] [INFO ] Polling processing.
    [2020/03/10 17:27:06] [daemonTaskExecutor-1] [o.s.b.c.l.s.SimpleJobOperator] [INFO ] Checking status of job with name=jobPointAddChunk
    [2020/03/10 17:27:06] [daemonTaskExecutor-1] [o.s.b.c.l.s.SimpleJobOperator] [INFO ] Attempting to launch job with name=jobPointAddChunk and parameters=
    [2020/03/10 17:27:06] [daemonTaskExecutor-1] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddChunk]] launched with the following parameters: [{jsr_batch_run_id=158}]
    [2020/03/10 17:27:07] [daemonTaskExecutor-1] [o.s.b.c.j.SimpleStepHandler] [INFO ] Executing step: [jobPointAddChunk.step01]
    [2020/03/10 17:27:07] [daemonTaskExecutor-1] [o.s.b.c.s.AbstractStep] [INFO ] Step: [jobPointAddChunk.step01] executed in 191ms
    [2020/03/10 17:27:07] [daemonTaskExecutor-1] [o.s.b.c.l.s.TaskExecutorJobLauncher] [INFO ] Job: [FlowJob: [name=jobPointAddChunk]] completed with the following parameters: [{jsr_batch_run_id=158}] and the following status: [COMPLETED] in 246ms
    [2020/03/10 17:27:16] [daemonTaskScheduler-1] [o.t.b.a.d.JobRequestPollTask] [INFO ] Polling processing.

###### 9.4.6.5.2. 終了コードの確認

非同期実行の場合、STSのDebug Viewで実行対象ジョブの終了コードを確認することはできない。
ジョブの実行状態はジョブ実行状態の確認で確認する。

###### 9.4.6.5.3. 出力リソースの確認

実行したジョブによって出力リソース(データベース or ファイル)を確認する。

データベースアクセスするジョブの場合

更新前後の会員情報テーブルの内容を比較し、確認内容のとおりとなっていることを確認する。
確認手順はH2 Consoleを使用してデータベースを参照するを参照。

確認内容

  - statusカラム

    - "1"(処理対象)から"0"(初期状態)に更新されていること

  - pointカラム

    - ポイント加算対象について、会員種別に応じたポイントが加算されていること

      - typeカラムが"G"(ゴールド会員)の場合は100ポイント

      - typeカラムが"N"(一般会員)の場合は10ポイント

    - 1,000,000(上限値)を超えたレコードが存在しないこと

更新前後の会員情報テーブルの内容を以下に示す。

図 119. 更新前後の会員情報テーブルの内容
ファイルアクセスするジョブの場合

会員情報ファイルの入出力内容を比較し、確認内容のとおりとなっていることを確認する。

確認内容

  - 出力ディレクトリに会員情報ファイルが出力されていること

    - 出力ファイル: files/output/output-member-info-data.csv

  - statusフィールド

    - "1"(処理対象)から"0"(初期状態)に更新されていること

  - pointフィールド

    - ポイント加算対象について、会員種別に応じたポイントが加算されていること

      - typeフィールドが"G"(ゴールド会員)の場合は100ポイント

      - typeフィールドが"N"(一般会員)の場合は10ポイント

    - 1,000,000(上限値)を超えたレコードが存在しないこと

会員情報ファイルの入出力内容を以下に示す。
ファイルのフィールドはid(会員番号)、type(会員種別)、status(商品購入フラグ)、point(ポイント)の順で出力される。

図 120. 会員情報ファイルの入出力内容

##### 9.4.6.6. 非同期バッチデーモンの停止

終了ファイルを作成し、非同期バッチデーモンを停止する。

ポーリング処理の設定で設定したとおり、 C:\tmpにstop-async-batch-daemonファイル(空ファイル)を作成する。

図 121. 終了ファイル作成

STSのコンソールで以下のとおり非同期バッチデーモンが停止していることを確認する。

非同期バッチデーモンの停止を確認

    (.. omitted)

    [2020/03/10 17:32:17] [daemonTaskScheduler-1] [o.t.b.a.d.JobRequestPollTask] [INFO ] Polling processing.
    [2020/03/10 17:32:26] [main] [o.t.b.a.d.AsyncBatchDaemon] [INFO ] Async Batch Daemon has detected the polling stop file, and then shutdown now!
    [2020/03/10 17:32:26] [main] [o.s.s.c.ThreadPoolTaskScheduler] [INFO ] Shutting down ExecutorService 'daemonTaskScheduler'
    [2020/03/10 17:32:26] [main] [o.s.s.c.ThreadPoolTaskExecutor] [INFO ] Shutting down ExecutorService
    [2020/03/10 17:32:26] [main] [o.t.b.a.d.JobRequestPollTask] [INFO ] JobRequestPollTask is called shutdown.
    [2020/03/10 17:32:26] [main] [o.s.s.c.ThreadPoolTaskScheduler] [INFO ] Shutting down ExecutorService 'daemonTaskScheduler'
    [2020/03/10 17:32:26] [main] [o.s.s.c.ThreadPoolTaskExecutor] [INFO ] Shutting down ExecutorService
    [2020/03/10 17:32:26] [main] [o.t.b.a.d.AsyncBatchDaemon] [INFO ] Async Batch Daemon stopped after all jobs completed.

##### 9.4.6.7. ジョブ実行状態の確認

JobRepositoryのメタデータテーブルでジョブの状態・実行結果を確認する。ここでは、`batch_job_execution`を参照する。

ジョブの状態を確認するためのSQLを以下に示す。

ジョブの状態確認用SQL

    SELECT job_execution_id,start_time,end_time,exit_code FROM batch_job_execution WHERE job_execution_id =
    (SELECT max(job_execution_id) FROM batch_job_request WHERE job_execution_id IS NOT NULL);

このSQLでは、最後に実行されたジョブの実行状態を取得するようにしている。

SQLの実行結果は、STS上でSQL実行後に表示されるSQL Results Viewにて確認できる。
下図のとおり、終了コード(EXIT_CODE)がCOMPLETED(正常終了)となっていることを確認する。
なお、ジョブの終了コードとプロセスの終了コードのマッピングについては、終了コードのマッピングを参照。

図 122. ジョブの状態確認

### 9.5. おわりに

このチュートリアルでは、以下の内容を学習した。

Macchinetta Batch 2.xによる基本的なバッチジョブの実装方法

- データベースアクセスでデータ入出力を行うジョブ

- ファイルアクセスでデータ入出力を行うジョブ

- 入力データの妥当性検証を行うジョブ

- ChunkListenerで例外ハンドリングを行うジョブ

- try-catchで例外ハンドリングを行うジョブ

- 非同期実行方式のジョブ

なお、Macchinetta Batch 2.xを利用し、バッチアプリケーションを開発する際は利用時の注意点に示す指針に沿って進めてほしい。

## 10. 利用時の注意点

### 10.1. Macchinetta Batch 2.xの注意点について

ここでは、各節で説明しているMacchinetta Batch 2.xを利用する際の、ルールや注意点についてリストにまとめる。 ユーザはバッチアプリケーションを開発する際、以降に示すポイントに留意して進めてほしい。

| ここでは、特に重要な注意点を挙げているのみであり、あらゆる検討事項を網羅しているわけではない。 ユーザは必ず利用する機能を一読すること。

バッチ処理で考慮する原則と注意点

- 単一のバッチ処理は可能な限り簡素化し、複雑な論理構造を避ける。

- 複数のジョブで同じことを何度もしない。

- システムリソースの利用を最小限にし、不要な物理入出力を避け、メモリ上での操作を活用する。

Macchinetta Batch 2.xの指針

- JavaConfig版の利用

  - 前バージョンからの移行でない限り、原則としてJavaConfig版を利用する

- バッチアプリケーションの開発

  - 1ジョブ=1Bean定義(1ジョブ定義) として作成する

  - 1ステップ=1バッチ処理=1ビジネスロジック として作成する

- チャンクモデル

  - 大量データを効率よく処理したい場合に利用する。

- タスクレットモデル

  - シンプルな処理や、定型化しにくい処理、データを一括で処理したい場合に利用する。

- 同期実行

  - スケジュールどおりにジョブを起動したり、複数のジョブを組み合わせてバッチ処理行う場合に利用する。

- 非同期実行(DBポーリング)

  - ディレード処理、処理時間が短いジョブの連続実行、大量ジョブの集約などに利用する。

- 非同期実行(Webコンテナ)

  - DBポーリングと同様だが、起動までの即時性が求められる場合にはこちらを利用する。

- JobRepositoryの管理

  - Spring Batch はジョブの起動状態・実行結果の記録に`JobRepository`を使用する。

  - Macchinetta Batch 2.xでは、以下のすべてに該当する場合は永続化は任意としてよい。

    - 同期型ジョブ実行のみでMacchinetta Batch 2.xを使用する。

    - ジョブの停止・リスタートを含め、ジョブの実行管理はすべてジョブスケジューラに委ねる。

      - Spring Batchがもつ`JobRepository`を前提としたリスタートを利用しない。

  - これらに該当する場合は`JobRepository`が使用するRDBMSの選択肢として、インメモリ・組み込み型データベースである`H2`を利用する。 一方で非同期実行を利用する場合や、Spring Batchの停止・リスタートを活用する場合は、ジョブの実行状態・結果を永続化可能なRDBMSが必要となる。
    この点については、ジョブの管理も一読のこと。

チャンクモデルとタスクレットモデルの使い分け

- チャンクモデル

  - 大量のデータを安定して処理したい場合

  - 件数ベースリスタートをしたい場合

- タスクレットモデル

  - リカバリを限りなくシンプルにしたい場合

  - 処理の内容をまとめたい場合

チャンクモデルとタスクレットモデルの使い分けも一読のこと。

Beanスコープの統一

- Tasklet実装では、Injectされるコンポーネントのスコープに合わせる。

- Composite系コンポーネントは、委譲するコンポーネントのスコープに合わせる。

- JobParameterを使用する場合は、`step`のスコープにする。

- Step単位でインスタンス変数を確保したい場合は、`step`のスコープにする。

性能チューニングポイント

- チャンクサイズを調整する

  - チャンクを利用するときは、コミット件数を適度なサイズにする。サイズを大きくしすぎない。

- フェッチサイズを調整する

  - データベースアクセスでは、フェッチサイズを適度なサイズにする。サイズを大きくしすぎない。

- ファイル読み込みを効率化する

  - 専用の`FieldSetMapper`インタフェース実装を用意する。

- 並列処理・多重処理

  - 出来る限りジョブスケジューラによって実現する。

- 分散処理

  - 出来る限りジョブスケジューラによって実現する。

非同期実行(DBポーリング)

- インメモリデータベースの使用

  - 長期連続運用するには向かず、定期的に再起動する運用が望ましい。

  - 長期連続運用で利用したい場合は、定期的に`JobRepository`からデータを削除するなどのメンテナンス作業が必須である。

- 登録ジョブの絞込み

  - 非同期実行することを前提に設計・実装されたジョブを指定する。

- 性能劣化もあり得るため、超ショートバッチの大量処理は向いていない。

- 同一ジョブの並列実行が可能になっているので、並列実行した場合に同一ジョブが影響を与えないようにする必要がある

非同期実行(Webコンテナ)

- 基本的な検討事項は、非同期実行(DBポーリング)と同じ。

- スレッドプールの調整をする。

  - 非同期実行のスレッドプールとは別に、Webコンテナのリクエストスレッドや同一筐体内で動作している他のアプリケーションも含めて検討する必要がある。

- Webとバッチとでは、データソース、MyBatis設定、`Mapper`インタフェースは相互参照はできない。

- スレッドプール枯渇によるジョブの起動失敗はジョブ起動時に捕捉できないので、別途確認する手段を用意しておく。

データベースアクセスとトランザクション

- 「ItemWriterに`MyBatisBatchItemWriter`を使用する」と「ItemProcessorで`Mapper`インタフェースを使用し更新処理をする」は同時にできない。

  - MyBatisには、同一トランザクション内で2つ以上の`ExecutorType`で実行してはいけないという制約があるため。 `Mapper`インタフェース(出力)を参照。

- データベースの同一テーブルへ入出力する際の注意点

  - 読み取り一貫性を担保するための情報が出力(UPDATEの発行)により失われた結果、入力(SELECT)にてエラーが発生することがある。 以下の対策を検討する。

    - データベースに依存になるが、情報を確保する領域を大きくする。

    - 入力データを分割し多重処理を行う。

ファイルアクセス

- 以下の固定長ファイルを扱う場合は、TERASOLUNA Batch 5.xが提供する部品を必ず使う。

  - マルチバイト文字を含む固定長ファイル

  - 改行なし固定長ファイル

- フッタレコードを読み飛ばす場合は、OSコマンドによる対応が必要。

排他制御

- 複数ジョブを同時実行する場合は、排他制御の必要がないようにジョブ設計を行う。

  - アクセスするリソースや処理対象をジョブごとに分割することが基本である。

- デッドロックが発生しないように設計を行う。

- ファイルの排他制御は、タスクレットモデルで実装すること。

異常系への対応

- 例外ハンドリングではトランザクション処理を行わない。

- 処理モデルでChunkListenerの挙動が異なることに注意する。

  - リソースのオープン･クローズで発生した例外は、

    - チャンクモデル…`ChunkListener`インタフェースが捕捉するスコープ外となる。

    - タスクレットモデル…`ChunkListener`インタフェースが捕捉するスコープ内となる。

- 入力チェックエラーは、チェックエラーの原因となる入力リソースを修正しない限り、リスタートしても回復不可能である

- JobRepositoryに障害が発生した時の対処方法を検討する必要がある。

ExecutionContextについて

- `ExecutionContext`は`JobRepository`へ格納されるため、以下の制約がある。

  - `ExecutionContext`へ格納するオブジェクトは、`java.io.Serializable`を実装したクラスでなければならない。

  - 格納できるサイズに制限がある。

終了コード

- Javaプロセス強制終了時の終了コードとバッチアプリケーションの終了コードとは明確に区別する。

  - バッチアプリケーションによるプロセスの終了コードを1に設定することは厳禁とする。

並列処理と多重処理

- `Multi Thread Step`は利用しない。

- 処理内容によっては、リソース競合とデッドロックが発生する可能性に注意する。

Macchinetta Batch Framework (2.x) Development Guideline - version 2.6.1.RELEASE, 2026-3-30
> INDEX