モダンJavaバッチ開発:もうエラーで悩まない!Spring Batchリトライ戦略ハンズオン

なぜSpring Batchのリトライ戦略が必要なのか?

Spring Batchでバッチ処理を開発していると、以下のような課題に直面したことはありませんか?

  • 一時的なネットワークの瞬断やデータベースのデッドロックで、バッチ処理全体が失敗してしまう。
  • 外部APIの呼び出しが一時的に失敗しただけで、再実行が必要になる。
  • 夜中にエラーアラートが鳴り響き、対応に追われる。

そんな悩みを解決するのが、Spring Batchのリトライ戦略です。

これらの問題の多くは、一時的なエラーであり、少し時間を置いて再試行すれば成功する可能性があります。Spring Batchのリトライ戦略は、このような一時的な障害からバッチ処理を回復させ、システムの堅牢性を高めるための重要なメカニズムです。

この記事を読むことで、あなたは以下の具体的なメリットを得られます。

  • バッチ処理の一時的なエラーで夜中に呼び出されることが少なくなります。
  • 堅牢なシステムを構築できるスキルが身につきます。

本記事では、Spring Batch(および関連ライブラリのSpring Retry)が提供する3つの主要なリトライ戦略を、具体的なコードと実行ログを交えながらハンズオン形式で詳しく解説します。


目次


対象読者

  • Spring Batchの基本的な概念を理解している方
  • バッチ処理におけるエラーハンドリングや堅牢性向上に課題を感じている方
  • Spring Batchのリトライ戦略を具体的にどのように実装すれば良いか知りたい方
  • JavaとSpring Frameworkに関する基本的な知識をお持ちの方

1. Spring Batchリトライ戦略の全体像

Spring Batchは、Spring Retryライブラリと連携してリトライ機能を提供します。これにより、開発者は宣言的またはプログラム的にリトライロジックを組み込むことができます。

ネットワークの一時的な切断、データベースのデッドロック、外部サービスの応答遅延など、再試行によって成功する可能性のあるエラーに対して特に有効です。

主なリトライ戦略と設定要素は以下の通りです。


1-1. Stepレベルの宣言的リトライ

Spring BatchのStep内で、ItemProcessorItemWriterで発生する例外に対してリトライを設定する方法です。最もシンプルで、Chunk指向のStepで利用されます。

  • 有効化:
    • stepBuilder.faultTolerant() を呼び出すことでリトライ機能を有効にします。
  • リトライ対象例外:
    • retry(Exception.class) でリトライの対象となる例外を指定します。
  • リトライ回数:
    • retryLimit(int) で最大試行回数(初回試行を含む)を設定します。

1-2. @Retryableによるメソッドレベルのリトライ

Spring Retryが提供するアノテーションベースのリトライです。Spring Batchのコンテキストに限らず、任意のSpring管理対象コンポーネントのメソッドに適用できます。

  • 有効化:
    • アプリケーションのメインクラスに@EnableRetryアノテーションを付与します。
  • 設定:
    • リトライ対象メソッドに@Retryableアノテーションを付与し、valueで例外、maxAttemptsで最大試行回数、backoffでリトライ間の待機時間などを設定します。
  • リカバリー:
    • 全てのリトライが失敗した場合のフォールバック処理として、@Recoverアノテーションを付与したメソッドを定義できます。

1-3. RetryTemplateによるプログラム的リトライ

最も柔軟性の高いリトライ方法で、コード内でRetryTemplateインスタンスを生成し、リトライポリシーやバックオフポリシーを細かく制御します。

  • ポリシー設定:
    • SimpleRetryPolicyExponentialBackOffPolicyなどをRetryTemplateに設定し、リトライの条件(回数、例外の種類)や待機戦略を定義します。
  • 実行:
    • retryTemplate.execute()メソッドにリトライ対象の処理(RetryCallback)と、リトライ失敗時のリカバリー処理(RecoveryCallback)を渡して実行します。

1-4. どのリトライ戦略を選ぶべきか?

  • Stepレベル:
    • Chunk指向のStepにおけるProcessor/Writerの単純な一時的エラーに。
  • @Retryable:
    • サービス層など、特定のビジネスロジックメソッドの一時的エラーに。
  • RetryTemplate:
    • リトライ条件や待機時間を動的に変更するなど、複雑で詳細な制御が必要な場合に。

一時的なエラーに対してはリトライを、永続的なエラーに対してはスキップや失敗として処理するなど、エラーの種類に応じた適切なハンドリングが堅牢なバッチ処理の鍵となります。


Spring Batchのスキップとリスナーの詳細については、以下の記事で詳細に解説していますので、是非ご覧ください。


2. 3つのリトライ戦略をハンズオンで学ぶ

本セクションでは、Spring Batchが提供する3つの主要なリトライ戦略について、具体的なコードと実行例を通じて詳細に解説します。

それぞれの戦略がどのように機能し、どのようなシナリオで最適であるかをハンズオン形式で学びましょう。


2-1. ハンズオンアプリケーションの目的

本アプリケーションは、Spring Batchにおける堅牢なエラーハンドリングの要である「リトライ戦略」を実践的に学ぶことを目的としています。

特に、一時的な障害からバッチ処理を回復させるための3つの主要なリトライ方法(Stepレベル、@Retryableアノテーション、RetryTemplate)について、具体的なコードと実行例を通じてその動作と特性を深く理解することを目指します。


2-2. アプリケーションの概要

Spring BatchのJobとしてretryJobを定義し、このJob内で3つの異なるリトライ戦略を実演する独立したStepを実行します。

新しく retryJob という名前のJobを作成します。このJobは、上記3つの戦略を実践するための3つの独立したStepで構成されます。

  • retryJob
    • stepLevelRetryStep: Stepレベルの宣言的リトライを実践
    • annotationRetryStep: @Retryableによるリトライを実践
    • programmaticRetryStep: RetryTemplateによるリトライを実践

各Stepは、意図的に例外を発生させることでリトライメカニズムがどのように機能するかを示し、それぞれの戦略の適用シナリオを明確にします。


2-3. 主要な仕様

  • 使用技術:
    • Spring Boot, Spring Batch, Spring Retry, Java。データ永続化にはH2 Databaseを使用します。
  • Job構成: retryJobは以下の3つのStepで構成されます。
    • stepLevelRetryStep:
      • ItemProcessor内で例外を発生させ、Spring BatchのStepBuilderによる宣言的なリトライ(faultTolerant(), retry(), retryLimit())を実演します。
    • annotationRetryStep:
      • @EnableRetryを有効にした上で、@Retryableアノテーションを付与したサービスメソッドをTaskletから呼び出し、メソッドレベルのリトライと@Recoverによるリカバリー処理を実演します。
    • programmaticRetryStep:
      • Tasklet内でRetryTemplateを直接インスタンス化し、SimpleRetryPolicyを設定してプログラム的にリトライ処理を制御する方法を実演します。
  • 実行方法:
    • Web UI (http://localhost:8080/) から提供されるボタンを通じてJobを起動できます。
    • REST API (curl -X POST http://localhost:8080/launch/jobs/retry-job) を使用してJobを直接実行することも可能です。

2-4. フォルダ構成

主要なソースコードはsrc/main/java/com/example/springbatchh2crud/配下に配置され、関心事ごとに以下のパッケージに分割されています。

src/main/java/com/example/springbatchh2crud/
├── SpringBatchH2CrudApplication.java  // アプリケーションのエントリポイント、@EnableRetryを有効化
├── config/
   └── retry/
       └── RetryJobConfig.java        // リトライ関連のJobおよびStep定義
└── retry/
    ├── processor/
       └── RetryableItemProcessor.java  // Stepレベルリトライ用のItemProcessor
    ├── service/
       └── RetryableService.java      // @Retryableアノテーションを使用するサービス
    └── tasklet/
        ├── AnnotationRetryTasklet.java  // @Retryableサービスを呼び出すTasklet
        └── ProgrammaticRetryTasklet.java// RetryTemplateを使用するTasklet

Spring Batchのサンプルコードのベースを、以下の記事で提供していますので、本記事の実装をする際に、是非ご活用ください。


それでは、各Stepの実装を詳しく見ていきましょう。


3. 【Step 1】:Stepレベルの宣言的リトライ

これは、Chunk-orientedなStepにおいて、ProcessorやWriterで発生したエラーをリトライする最も基本的な方法です。StepBuilder の設定だけで実現できます。


3-1. 実装コード

1. 意図的に例外を発生させる ItemProcessor

まず、特定のアイテム(この例では文字列 “b”)が渡された場合に、最初の2回は IllegalStateException をスローし、3回目に成功する ItemProcessor を作成します。

src/main/java/com/example/springbatchh2crud/retry/processor/RetryableItemProcessor.java

package com.example.springbatchh2crud.retry.processor;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.item.ItemProcessor;

/**
 * StepレベルのリトライをテストするためのItemProcessor実装。
 * 特定のアイテム("b")が渡された場合に意図的に例外を発生させ、
 * Spring Batchのリトライメカニズムが機能することを示す。
 */
public class RetryableItemProcessor implements ItemProcessor<String, String> {

  // ロギングのためのLoggerインスタンス
  private static final Logger log = LoggerFactory.getLogger(RetryableItemProcessor.class);
  // アイテムごとのリトライ回数を追跡するためのマップ
  private final ConcurrentHashMap<String, AtomicInteger> retryCounts = new ConcurrentHashMap<>();

  /**
   * アイテムを処理します。
   * アイテムが "b" の場合、最初の2回は意図的にIllegalStateExceptionをスローし、
   * 3回目の試行で成功するように動作します。
   * これはSpring Batchのリトライ機能をテストするために使用されます。
   *
   * @param item 処理対象のアイテム
   * @return 処理されたアイテムの文字列
   * @throws Exception 処理中にエラーが発生した場合
   */
  @Override
  public String process(String item) throws Exception {
    log.info("Processing item: {}", item);

    // アイテムが "b" の場合にのみ例外を発生させる
    if ("b".equals(item)) {
      // アイテムのリトライ回数をインクリメント
      int attempt =
          retryCounts.computeIfAbsent(item, k -> new AtomicInteger(0)).incrementAndGet();
      log.info("Attempt {} for item '{}'", attempt, item);
      // 2回目までの試行では例外をスロー
      if (attempt <= 2) {
        log.warn("Throwing exception for item '{}', attempt {}", item, attempt);
        throw new IllegalStateException("Failed to process item: " + item);
      }
    }
    // 3回目以降の試行、またはアイテムが "b" でない場合は正常に処理
    log.info("Successfully processed item: {}", item);
    return "Processed " + item;
  }
}

4. 【Step 2】:@Retryableによるメソッドレベルのリトライ

Spring Retryライブラリが提供する @Retryable アノテーションを使うと、サービス層のメソッドなど、より広範囲な箇所で宣言的にリトライを実装できます。


4-1. 実装コード

1. Spring Retryの有効化

まず、メインアプリケーションクラスに @EnableRetry アノテーションを付与して、この機能を有効にします。

src/main/java/com/example/springbatchh2crud/SpringBatchH2CrudApplication.java

@SpringBootApplication
@EnableRetry // Spring Retry機能を有効にするアノテーション
public class SpringBatchH2CrudApplication {
    // ...
}

2. @Retryable を持つサービス

リトライさせたいメソッドに @Retryable アノテーションを付与します。リトライがすべて失敗した場合のフォールバック処理として、@Recover アノテーションを付与したメソッドを定義できます。

src/main/java/com/example/springbatchh2crud/retry/service/RetryableService.java

package com.example.springbatchh2crud.retry.service;

import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

/**
 * `@Retryable`アノテーションを使用したメソッドレベルのリトライを実演するサービス。
 * 意図的に例外を発生させ、指定された回数リトライした後、リカバリー処理を実行する。
 */
@Service
public class RetryableService {

    // ロギングのためのLoggerインスタンス
    private static final Logger log = LoggerFactory.getLogger(
        RetryableService.class
    );
    // メソッドの呼び出し回数を追跡するカウンター
    private final AtomicInteger counter = new AtomicInteger(0);

    /**
     * リトライ可能なメソッド。
     * `retryFor`でリトライ対象の例外、`maxAttempts`で最大試行回数、
     * `backoff`でリトライ間の待機時間を設定する。
     * 最初の2回は例外をスローし、3回目で成功する。
     */
    @Retryable(
        retryFor = { RuntimeException.class }, // リトライ対象の例外
        maxAttempts = 3, // 最大試行回数(初回試行を含む)
        backoff = @Backoff(delay = 100) // 100ミリ秒の待機後にリトライ
    )
    public void retryableMethod() {
        int attempt = counter.incrementAndGet();
        log.info("Attempting retryableMethod, attempt number {}", attempt);
        if (attempt <= 2) {
            log.warn(
                "Throwing exception in retryableMethod, attempt {}",
                attempt
            );
            throw new RuntimeException("Intentional exception for retry");
        }
        log.info("retryableMethod succeeded on attempt {}", attempt);
    }

    /**
     * `retryableMethod`の全てのリトライが失敗した場合に呼び出されるリカバリーメソッド。
     *
     * @param e リトライ中に発生した最後の例外
     */
    @Recover
    public void recover(RuntimeException e) {
        log.error(
            "All retry attempts failed for retryableMethod. Executing recovery logic.",
            e
        );
    }

    /**
     * カウンターをリセットする。
     */
    public void reset() {
        counter.set(0);
    }
}

3. サービスを呼び出す Tasklet

このサービスを呼び出すだけのシンプルな Tasklet を用意します。

src/main/java/com/example/springbatchh2crud/retry/tasklet/AnnotationRetryTasklet.java

package com.example.springbatchh2crud.retry.tasklet;

import com.example.springbatchh2crud.retry.service.RetryableService;
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.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * `@Retryable`アノテーションが適用されたサービスメソッドを呼び出すTasklet。
 * このTaskletは、`RetryableService`の`retryableMethod`を実行し、
 * メソッドレベルのリトライ動作をトリガーする。
 */
@Component
public class AnnotationRetryTasklet implements Tasklet {

  // RetryableServiceを自動インジェクション
  @Autowired private RetryableService retryableService;

  /**
   * Taskletの実行ロジック。
   * `RetryableService`のカウンターをリセットし、`retryableMethod`を呼び出す。
   *
   * @param contribution 現在のStepの貢献オブジェクト
   * @param chunkContext 現在のChunkのコンテキスト
   * @return 処理の繰り返しステータス
   * @throws Exception 実行中にエラーが発生した場合
   */
  @Override
  public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext)
      throws Exception {
    retryableService.reset(); // 実行前にカウンターをリセット
    retryableService.retryableMethod(); // リトライ可能なメソッドを呼び出す
    return RepeatStatus.FINISHED; // 処理完了
  }
}

5. 【Step 3】:RetryTemplateによるプログラム的リトライ

RetryTemplate を使うと、リトライポリシー(何回リトライするか)やバックオフポリシー(リトライ間にどれくらい待つか)を動的に変更するなど、最も柔軟で詳細なリトライ制御をコードで記述できます。


5-1. 実装コード

Tasklet の中で RetryTemplate を直接使用します。

src/main/java/com/example/springbatchh2crud/retry/tasklet/ProgrammaticRetryTasklet.java

package com.example.springbatchh2crud.retry.tasklet;

import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.repeat.RepeatStatus;
import org.springframework.retry.RecoveryCallback;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Component;

/**
 * `RetryTemplate`を使用したプログラム的リトライを実演するTasklet。
 * `RetryTemplate`を介してリトライポリシーとリカバリーコールバックを設定し、
 * 意図的に例外を発生させてリトライ動作を確認する。
 */
@Component
public class ProgrammaticRetryTasklet implements Tasklet {

  // ロギングのためのLoggerインスタンス
  private static final Logger log = LoggerFactory.getLogger(ProgrammaticRetryTasklet.class);

  /**
   * Taskletの実行ロジック。
   * `RetryTemplate`を初期化し、リトライポリシーを設定してリトライ処理を実行する。
   *
   * @param contribution 現在のStepの貢献オブジェクト
   * @param chunkContext 現在のChunkのコンテキスト
   * @return 処理の繰り返しステータス
   * @throws Exception 実行中にエラーが発生した場合
   */
  @Override
  public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext)
      throws Exception {
    // RetryTemplateのインスタンスを作成
    RetryTemplate retryTemplate = new RetryTemplate();
    // シンプルなリトライポリシーを設定(最大試行回数3回)
    SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
    retryPolicy.setMaxAttempts(3);
    retryTemplate.setRetryPolicy(retryPolicy);

    // リトライ回数を追跡するためのカウンター
    final AtomicInteger counter = new AtomicInteger(0);

    // RetryTemplateを実行
    String result =
        retryTemplate.execute(
            // リトライ対象の処理を定義するRetryCallback
            new RetryCallback<String, RuntimeException>() {
              @Override
              public String doWithRetry(RetryContext context) throws RuntimeException {
                int attempt = counter.incrementAndGet();
                log.info("Attempting programmatic retry, attempt number {}", attempt);
                // 2回目までの試行では例外をスロー
                if (attempt <= 2) {
                  log.warn("Throwing exception in programmatic retry, attempt {}", attempt);
                  throw new RuntimeException("Intentional exception for programmatic retry");
                }
                // 3回目以降の試行では成功
                log.info("Programmatic retry succeeded on attempt {}", attempt);
                return "Success";
              }
            },
            // 全てのリトライが失敗した場合のリカバリー処理を定義するRecoveryCallback
            new RecoveryCallback<String>() {
              @Override
              public String recover(RetryContext context) throws Exception {
                log.error(
                    "All programmatic retry attempts failed. Executing recovery logic.",
                    context.getLastThrowable());
                return "Recovered";
              }
            });

    log.info("Final result of programmatic retry: {}", result);
    return RepeatStatus.FINISHED; // 処理完了
  }
}

6. 【Step 4】:RetryJobConfig.java の作成

6-1. 実装コード

Stepの定義

次に、リトライジョブ用の設定ファイル RetryJobConfig.java でStepを定義します。

config パッケージ配下に retry パッケージを新たに作成し、そこに RetryJobConfig.java を配置します。

src/main/java/com/example/springbatchh2crud/config/retry/RetryJobConfig.java

package com.example.springbatchh2crud.config.retry;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.support.IteratorItemReader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

/**
 * リトライ戦略を実演するためのSpring Batch Job設定クラス。
 * Stepレベル、@Retryableアノテーション、RetryTemplateの3つのリトライ戦略を含むJobを定義する。
 */
@Configuration
public class RetryJobConfig {

  private final JobRepository jobRepository;
  private final PlatformTransactionManager transactionManager;

  // コンストラクタインジェクションでJobRepositoryとPlatformTransactionManagerを取得
  public RetryJobConfig(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
    this.jobRepository = jobRepository;
    this.transactionManager = transactionManager;
  }

  /**
   * リトライ可能なItemReaderを定義。
   * "a", "b", "c" の文字列を読み込むシンプルなリーダー。
   */
  @Bean
  public ItemReader<String> retryableItemReader() {
    return new IteratorItemReader<>(java.util.Arrays.asList("a", "b", "c"));
  }

  /**
   * リトライ可能なItemProcessorを定義。
   * 特定のアイテムで意図的に例外を発生させる。
   */
  @Bean
  public ItemProcessor<String, String> retryableItemProcessor() {
    return new com.example.springbatchh2crud.retry.processor.RetryableItemProcessor();
  }

  /**
   * ログにアイテムを出力するItemWriterを定義。
   */
  @Bean
  public ItemWriter<String> logItemWriter() {
    return items ->
        items.forEach(
            item ->
                org.slf4j.LoggerFactory.getLogger("logItemWriter")
                    .info("Writing item: {}", item));
  }

  /**
   * Stepレベルの宣言的リトライを実演するStepを定義。
   * ItemProcessorで発生するIllegalStateExceptionを3回までリトライする。
   */
  @Bean
  public Step stepLevelRetryStep() {
    return new StepBuilder("stepLevelRetryStep", jobRepository)
        .<String, String>chunk(1, transactionManager) // 1チャンクごとに処理
        .reader(retryableItemReader()) // ItemReaderを設定
        .processor(retryableItemProcessor()) // ItemProcessorを設定
        .writer(logItemWriter()) // ItemWriterを設定
        .faultTolerant() // フォールトトレラント機能を有効化
        .retry(IllegalStateException.class) // IllegalStateExceptionをリトライ対象とする
        .retryLimit(3) // 最大3回(初回試行+2回のリトライ)まで試行
        .build();
  }

  /**
   * `@Retryable`アノテーションによるメソッドレベルのリトライを実演するStepを定義。
   * AnnotationRetryTaskletを実行する。
   */
  @Bean
  public Step annotationRetryStep(
      com.example.springbatchh2crud.retry.tasklet.AnnotationRetryTasklet tasklet) {
    return new StepBuilder("annotationRetryStep", jobRepository)
        .tasklet(tasklet, transactionManager) // Taskletを設定
        .build();
  }

  /**
   * `RetryTemplate`によるプログラム的リトライを実演するStepを定義。
   * ProgrammaticRetryTaskletを実行する。
   */
  @Bean
  public Step programmaticRetryStep(
      com.example.springbatchh2crud.retry.tasklet.ProgrammaticRetryTasklet tasklet) {
    return new StepBuilder("programmaticRetryStep", jobRepository)
        .tasklet(tasklet, transactionManager) // Taskletを設定
        .build();
  }

  /**
   * 3つのリトライ戦略Stepを含むJobを定義。
   * 各Stepは順次実行される。
   */
  @Bean
  public Job retryJob(
      Step stepLevelRetryStep, Step annotationRetryStep, Step programmaticRetryStep) {
    return new JobBuilder("retryJob", jobRepository)
        .start(stepLevelRetryStep) // 最初のStep
        .next(annotationRetryStep) // 次のStep
        .next(programmaticRetryStep) // 最後のStep
        .build();
  }
}

.faultTolerant() を呼び出した後、.retry() でリトライ対象の例外クラスを、.retryLimit() でリトライ回数の上限を指定します。

Note: retryLimit(3) は、最初の試行を含めて合計3回試行することを意味します。つまり、リトライ自体は最大2回までとなります。

[1回目試行] -> 失敗 -> [1回目リトライ] -> 失敗 -> [2回目リトライ] -> 成功/失敗
(合計3回試行)

src/main/java/com/example/springbatchh2crud/config/retry/RetryJobConfig.java (抜粋)

@Bean
public Step stepLevelRetryStep() {
    return new StepBuilder("stepLevelRetryStep", jobRepository)
        .<String, String>chunk(1, transactionManager)
        .reader(retryableItemReader()) // "a", "b", "c" を読み込むReader
        .processor(retryableItemProcessor())
        .writer(logItemWriter()) // ログに出力するだけのWriter
        .faultTolerant() // リトライ機能を有効化
        .retry(IllegalStateException.class) // リトライ対象の例外
        .retryLimit(3) // 最大リトライ回数 (最初の試行 + 2回のリトライ)
        .build();
}

Note: 今回は、BatchConfig.javaではなく、別のConfigクラスを作成しました。これにより、Configクラス はリトライ関連のコードが分割され、よりスリムになります。
Spring Bootは @Configuration アノテーションが付いたクラスを自動的にスキャンしてBean定義を読み込むため、アプリケーションの動作は変わらず、コードは見通しがよくなります。
このように設定を適切に分割することは、大規模なアプリケーションを開発する上で非常に重要なプラクティスです。


7. 【Step 5】:ジョブ実行用インタフェースの作成

実装したジョブフローを手動で実行して動作を確認するための仕組みを整備します。
ここでは、APIやWebブラウザでジョブを実行するための仕組みや確認方法を解説します。


7-1. APIエンドポイントの追加

JobLaunchControllerに、作成した5つのジョブを起動するための@PostMappingエンドポイントを追加します。

src/main/java/com/example/springbatchh2crud/controller/JobLaunchController.java

// --- Retry Job ---
@PostMapping("/jobs/retry-job")
public ResponseEntity<String> launchRetryJob() {
    return launchJob(retryJob);
}

7-2. Webブラウザからの実行

Webブラウザから各ジョブを簡単に実行できるよう、index.htmlに起動ボタンを追加します。

src/main/resources/static/index.html

<h2 class="group-title">Error Handling and Retry Jobs</h2>

<div class="job-card">
    <h3>Retry Job</h3>
    <p>
        <code>retryJob</code>:
        Stepレベルのリトライ、@Retryableアノテーション、RetryTemplateを使用したプログラム的リトライなど、Spring
        Batchのエラーハンドリングとリトライ戦略を実演するジョブ。
    </p>
    <form action="/launch-job/jobs/retry-job" method="POST">
        <button type="submit">Launch retryJob</button>
    </form>
</div>

8. 【Step 6】:実行方法と実行結果

8-1. 実行方法

実装した retryJob は、以下のいずれかの方法で実行できます。

1. Web UIからの実行

アプリケーションを起動し、ブラウザで http://localhost:8080/ にアクセスします。
「Error Handling and Retry Jobs」セクションにある “Launch retryJob” ボタンをクリックしてください。

2. APIでの直接実行

ターミナルから以下のcurlコマンドを実行します。

curl -X POST http://localhost:8080/launch/jobs/retry-job

どちらの方法でも、アプリケーションのコンソールに本記事で解説したようなログが出力され、リトライの動作を実際に確認できます。


8-2. 実行結果と解説

Step 1: Stepレベルの宣言的リトライ

このジョブを実行すると、コンソールに以下のようなログが出力されます。

// ...
INFO --- [nio-8080-exec-1] c.e.s.r.p.RetryableItemProcessor         : Processing item: b
INFO --- [nio-8080-exec-1] c.e.s.r.p.RetryableItemProcessor         : Attempt 1 for item 'b'
WARN --- [nio-8080-exec-1] c.e.s.r.p.RetryableItemProcessor         : Throwing exception for item 'b', attempt 1
INFO --- [nio-8080-exec-1] c.e.s.r.p.RetryableItemProcessor         : Processing item: b
INFO --- [nio-8080-exec-1] c.e.s.r.p.RetryableItemProcessor         : Attempt 2 for item 'b'
WARN --- [nio-8080-exec-1] c.e.s.r.p.RetryableItemProcessor         : Throwing exception for item 'b', attempt 2
INFO --- [nio-8080-exec-1] c.e.s.r.p.RetryableItemProcessor         : Processing item: b
INFO --- [nio-8080-exec-1] c.e.s.r.p.RetryableItemProcessor         : Attempt 3 for item 'b'
INFO --- [nio-8080-exec-1] c.e.s.r.p.RetryableItemProcessor         : Successfully processed item: b
INFO --- [nio-8080-exec-1] logItemWriter                            : Writing item: Processed b
// ...

ログから、アイテム “b” の処理で IllegalStateException が2回発生し、その都度処理がリトライされ、3回目の試行で正常に成功していることが明確にわかります。


Step 2: @Retryableによるメソッドレベルのリトライ

このStepが実行されると、以下のようなログが出力されます。

// ...
INFO --- [nio-8080-exec-1] c.e.s.retry.service.RetryableService     : Attempting retryableMethod, attempt number 1
WARN --- [nio-8080-exec-1] c.e.s.retry.service.RetryableService     : Throwing exception in retryableMethod, attempt 1
// (100ms待機)
INFO --- [nio-8080-exec-1] c.e.s.retry.service.RetryableService     : Attempting retryableMethod, attempt number 2
WARN --- [nio-8080-exec-1] c.e.s.retry.service.RetryableService     : Throwing exception in retryableMethod, attempt 2
// (100ms待機)
INFO --- [nio-8080-exec-1] c.e.s.retry.service.RetryableService     : Attempting retryableMethod, attempt number 3
INFO --- [nio-8080-exec-1] c.e.s.retry.service.RetryableService     : retryableMethod succeeded on attempt 3
// ...

@Retryableアノテーションが機能し、retryableMethodが3回目に成功していることがわかります。もしmaxAttemptsを2に設定すれば、リトライが尽きてrecoverメソッドが呼び出され、そのログが出力されるはずです。


実行結果と解説:Step 3: RetryTemplateによるプログラム的リトライ

このStepが実行されると、以下のようなログが出力されます。

// ...
INFO --- [nio-8080-exec-1] c.e.s.r.t.ProgrammaticRetryTasklet       : Attempting programmatic retry, attempt number 1
WARN --- [nio-8080-exec-1] c.e.s.r.t.ProgrammaticRetryTasklet       : Throwing exception in programmatic retry, attempt 1
INFO --- [nio-8080-exec-1] c.e.s.r.t.ProgrammaticRetryTasklet       : Attempting programmatic retry, attempt number 2
WARN --- [nio-8080-exec-1] c.e.s.r.t.ProgrammaticRetryTasklet       : Throwing exception in programmatic retry, attempt 2
INFO --- [nio-8080-exec-1] c.e.s.r.t.ProgrammaticRetryTasklet       : Attempting programmatic retry, attempt number 3
INFO --- [nio-8080-exec-1] c.e.s.r.t.ProgrammaticRetryTasklet       : Programmatic retry succeeded on attempt 3
INFO --- [nio-8080-exec-1] c.e.s.r.t.ProgrammaticRetryTasklet       : Final result of programmatic retry: Success
// ...

RetryTemplateexecute メソッド内で定義した RetryCallback が3回実行され、最終的に成功して “Success” という文字列が返されていることがわかります。


まとめ:どのリトライ戦略を選ぶべきか?

3つの方法の特徴と使い分けを以下にまとめます。

戦略主な特徴おすすめのケース
StepレベルSpring Batch標準。設定が簡単。Chunk-orientedなStepのProcessor/Writerで発生する単純なエラーをリトライしたい場合。
@Retryable宣言的でコードが綺麗。Spring Batchに依存しない。サービス層など、ビジネスロジック内の特定メソッドの呼び出しをリトライしたい場合。
RetryTemplate最も柔軟性が高い。動的なポリシー設定が可能。実行条件によってリトライ回数や待機時間を変えたいなど、複雑で詳細なリトライ制御が必要な場合。

まずは最もシンプルな Stepレベルのリトライ から検討し、要件が複雑になるにつれて @RetryableRetryTemplate へと選択肢を広げていくのが良いアプローチです。

この記事が役に立ったと感じたら、ぜひSNSでシェアしてください!


免責事項

本記事の内容は、公開時点での情報に基づいて作成されています。
時間の経過とともに情報が古くなる可能性や、技術的な詳細が変更される可能性があります。
本記事の情報を利用したことで生じた、いかなる損害についても、筆者および公開元は一切の責任を負いません。
読者の皆様自身の責任において、情報の正確性を確認し、利用してください。


SNSでもご購読できます。

コメントを残す

*