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

# ChunkListenerで例外ハンドリングを行うジョブ

| 前提 チュートリアルの進め方で説明しているとおり、 入力データの妥当性検証を行うジョブに対して、 例外ハンドリングの実装を追加していく形式とする。なお、例外ハンドリング方式にはtry-catchやChunkListenerなど様々な方式がある。 ただし、記述はデータベースアクセスするジョブに実装を追加した場合の説明としているため留意すること。

## 概要

ChunkListenerで例外ハンドリングを行うジョブを作成する。

なお、詳細についてはMacchinetta Batch 2.x 開発ガイドラインの`ChunkListener`インタフェースによる例外ハンドリングを参照。

| リスナーの用途について ここでは、リスナーによりステップの実行後に例外発生の有無をチェックすることで例外ハンドリングを実現するが、 リスナーの用途は例外ハンドリングに限定されてはいないため、 詳細についてはリスナーを参照。

作成するアプリケーションの説明の 背景、処理概要、業務仕様を以下に再掲する。

### 背景

とある量販店では、会員に対してポイントカードを発行している。
会員には「ゴールド会員」「一般会員」の会員種別が存在し、会員種別に応じたサービスを提供している。
今回そのサービスの一環として、月内に商品を購入した会員のうち、 会員種別が「ゴールド会員」の場合は100ポイント、「一般会員」の場合は10ポイントを月末に加算することにした。

### 処理概要

会員種別に応じてポイント加算を行うアプリケーションを 月次バッチ処理としてMacchinetta Batch 2.xを使用して実装する。
入力データにポイントの上限値を超えるデータが存在するか妥当性検証を行う処理を追加実装し、 エラーの場合ログを出力し、異常終了させる。

### 業務仕様

業務仕様は以下のとおり。

- 入力データのポイントが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ポイントに補正する

### テーブル仕様

入出力リソースとなる会員情報テーブルの仕様は以下のとおり。
前提のとおりデータベースアクセスするジョブの場合の説明となるため、ファイルアクセスするジョブの場合の 入出力のリソース仕様はファイル仕様を参照。

表 1. 会員情報テーブル(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。

### ジョブの概要

ここで作成する入力チェックを行うジョブの概要を把握するために、 処理フローおよび処理シーケンスを以下に示す。

前提のとおりデータベースアクセスするジョブの場合の説明となるため、 ファイルアクセスするジョブの場合の処理フローおよび処理シーケンスとは異なる部分があるため留意する。

処理フロー概要

  処理フローの概要を以下に示す。

図 1. 例外ハンドリングを行うジョブの処理フロー

チャンクモデルの場合の処理シーケンス

  チャンクモデルの場合の処理シーケンスを説明する。
  本ジョブは異常系データを利用することを前提として説明しているため、 このシーケンス図は入力チェックでエラー(異常終了)となった場合を示している。
  入力チェックが正常の場合、入力チェック以降の処理シーケンスはデータベースアクセスのシーケンス図 (ジョブの概要を参照)と同じである。

橙色のオブジェクトは今回実装するクラスを表す。

図 2. チャンクモデルのシーケンス図
シーケンス図の説明

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)を返却する。

タスクレットモデルの場合の処理シーケンス

  タスクレットモデルの場合の処理シーケンスについて説明する。
  本ジョブは異常系データを利用することを前提として説明しているため、 このシーケンス図は入力チェックでエラー(異常終了)となった場合を示している。
  入力チェックが正常の場合、入力チェック以降の処理シーケンスはデータベースアクセスのシーケンス図 (ジョブの概要を参照)と同じである。

橙色のオブジェクトは今回実装するクラスを表す。

図 3. タスクレットモデルのシーケンス図
シーケンス図の説明

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)を返却する。

以降で、チャンクモデル、タスクレットモデルそれぞれの実装方法を説明する。

- チャンクモデルでの実装

- タスクレットモデルでの実装

## チャンクモデルでの実装

チャンクモデルで入力チェックを行うジョブの作成から実行までを以下の手順で実施する。

1. メッセージ定義の追加

2. 例外ハンドリングの実装

3. ジョブの実行と結果の確認

### メッセージ定義の追加

コード体系のばらつき防止や、監視対象のキーワードとしての抽出を設計しやすくするため、 ログメッセージはメッセージ定義を使用し、ログ出力時に使用する。

チャンクモデル/タスクレットモデルで共通して利用するため、既に作成している場合は読み飛ばしてよい。

`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 -->

表 2. 説明
項番 | 説明
--- | ---
(1) | ポイント上限超過時に出力するメッセージを設定する。 {0}に項目名、{1}に上限値を割り当てる。
(2) | プロパティファイルからメッセージを使用するために、`MessageSource`を設定する。

### 例外ハンドリングの実装

例外ハンドリング処理を実装する。

以下の作業を実施する。

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)
            }
        }
    }

表 3. 説明
項番 | 説明
--- | ---
(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>

表 4. 説明
項番 | 説明
--- | ---
(1) | コンポーネントスキャン対象とするベースパッケージの設定を行う。 `basePackages属性/base-package属性`に、`ChunkListener`の実装クラスが格納されているパッケージを追加で指定する。
(2) | `StepListener`の実装クラスを設定する。 なお、`ChunkListener`は`StepListener`の拡張インタフェースである。 ここでは、ChunkListenerの実装クラスのBeanIDである`chunkErrorLoggingListener`を指定する。

### ジョブの実行と結果の確認

作成したジョブを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(異常終了)となっていることを確認する。

図 4. 終了コードの確認

#### 出力リソースの確認

入力チェックを実装したジョブによって出力リソース(データベース or ファイル)を確認する。

チャンクモデルの場合、中間コミット方式をとっているため、エラー箇所直前のチャンクまで更新が確定していることを確認する。

##### 会員情報テーブルの確認

更新前後の会員情報テーブルの内容を比較し、確認内容のとおりとなっていることを確認する。
確認手順はH2 Consoleを使用してデータベースを参照するを参照。

確認内容

  - 1から10番目のレコード(会員番号が"00000001"から"00000010"のレコード)について

    - statusカラム

      - "1"(処理対象)から"0"(初期状態)に更新されていること

    - pointカラム

      - ポイント加算対象について、会員種別に応じたポイントが加算されていること

        - typeカラムが"G"(ゴールド会員)の場合は100ポイント

        - typeカラムが"N"(一般会員)の場合は10ポイント

  - 11から15番目のレコード(会員番号が"00000011"から"00000015"のレコード)について

    - 更新されていないこと(破線の赤枠で示した範囲)

更新前後の会員情報テーブルの内容は以下のとおり。

図 5. 更新前後の会員情報テーブルの内容

##### 会員情報ファイルの確認

会員情報ファイルの入出力内容を比較し、確認内容のとおりとなっていることを確認する。

確認内容

  - 出力ディレクトリに会員情報ファイルが出力されていること

    - 出力ファイル: 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(ポイント)の順で出力される。

図 6. 会員情報ファイルの入出力内容

## タスクレットモデルでの実装

タスクレットモデルで入力チェックを行うジョブの作成から実行までを以下の手順で実施する。

1. メッセージ定義の追加

2. 例外ハンドリングの実装

3. ジョブの実行と結果の確認

### メッセージ定義の追加

コード体系のばらつき防止や、監視対象のキーワードとしての抽出を設計しやすくするため、 ログメッセージはメッセージ定義を使用し、ログ出力時に使用する。

チャンクモデル/タスクレットモデルで共通して利用するため、既に作成している場合は読み飛ばしてよい。

`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 -->

表 5. 説明
項番 | 説明
--- | ---
(1) | ポイント上限超過時に出力するメッセージを設定する。 {0}に項目名、{1}に上限値を割り当てる。
(2) | プロパティファイルからメッセージを使用するために、`MessageSource`を設定する。

### 例外ハンドリングの実装

例外ハンドリング処理を実装する。

以下の作業を実施する。

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)
            }
        }
    }

表 6. 説明
項番 | 説明
--- | ---
(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>

表 7. 説明
項番 | 説明
--- | ---
(1) | コンポーネントスキャン対象とするベースパッケージの設定を行う。 `basePackages属性/base-package属性`に、`ChunkListener`の実装クラスが格納されているパッケージを追加で指定する。
(2) | `StepListener`の実装クラスを設定する。 なお、`ChunkListener`は`StepListener`の拡張インタフェースである。 ここでは、ChunkListenerの実装クラスのBeanIDである`chunkErrorLoggingListener`を指定する。

### ジョブの実行と結果の確認

作成したジョブを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(異常終了)となっていることを確認する。

図 7. 終了コードの確認

#### 出力リソースの確認

入力チェックを実装したジョブによって出力リソース(データベース or ファイル)を確認する。

タスクレットモデルの場合、一括コミット方式をとっているため、エラーが発生した場合は一切更新されていないことを確認してほしい。

##### 会員情報テーブルの確認

更新前後の会員情報テーブルの内容を比較し、確認内容のとおりとなっていることを確認する。
確認手順はH2 Consoleを使用してデータベースを参照するを参照。

確認内容

  - すべてのレコードについて、データが更新されていないこと

初期状態の会員情報テーブルの内容を以下に示す。

図 8. 初期状態の会員情報テーブルの内容

##### 会員情報ファイルの確認

会員情報ファイルの入出力内容を比較し、確認内容のとおりとなっていることを確認する。

確認内容

  - 出力ディレクトリに会員情報ファイルが空ファイルで出力されていること

    - 出力ファイル: files/output/output-member-info-data.csv

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