モダンJavaバッチ開発:Spring Batch 複雑なバッチ処理を操る!ジョブフロー制御の基本と実践

あなたのバッチ処理、もっと賢く、もっと速くできます!

「バッチ処理が複雑になってきて、コードがスパゲッティ状態…」
「処理時間が長すぎて、もっと効率化したい…」
「エラー発生時のリカバリが大変…」

エンジニアの皆さん、日々の業務でこんな悩みを抱えていませんか?

Spring Batchは、単なるデータの読み書きだけでなく、複雑な業務ロジックを伴うバッチ処理を「エレガントに」「堅牢に」「効率的に」構築するための強力なフレームワークです。
ジョブの実行順序や条件分岐、並列処理といった「フロー制御」の機能は、バッチ処理を次のレベルへと引き上げるキーとなります。

このハンズオンガイドでは、Spring Batchの「高度なジョブフロー制御」に焦点を当て、以下の5つの代表的なパターンを具体的なコードと実行例を通じて徹底解説します。

  1. 順次フロー (Sequential Flow):
    • 最も基本的な、ステップを順番に実行するフロー。
  2. 条件付きフロー (Conditional Flow):
    • ステップの終了ステータス(ExitStatus)に基づいて、次に実行するステップを決定するフロー。
  3. 並行フロー (Split Flow):
    • 複数のフローを同時に並列実行し、処理時間を短縮するフロー。
  4. Deciderによるプログラム的フロー決定:
    • JobExecutionDeciderを用いて、Javaコードによる複雑な条件分岐ロジックを実装するフロー。
  5. ジョブの終了ステータス制御:
    • ステップの終了ステータスをカスタマイズし、それに基づいてフローを制御する方法。

この記事を読み終える頃には、あなたは複雑なバッチ処理の設計・実装に自信を持ち、より質の高いシステム開発に貢献できるようになるでしょう。


目次


対象読者

  • Spring Batchの基本的な概念を理解し、より複雑なジョブフロー制御を学びたい開発者
  • 既存のバッチ処理を改善し、効率化したいと考えているエンジニア
  • JavaとSpring Frameworkを用いたバッチ開発のベストプラクティスに興味がある方

1. Flow, Split, Deciderによるジョブフロー制御:複雑なバッチ処理のオーケストレーション

実際のビジネス要件では、バッチ処理は単一のステップで完結することは稀です。
複数のステップを連携させ、条件に応じて処理を分岐させたり、複数の処理を並行して実行したりするなど、複雑なフロー制御が求められます。

Spring Batchは、このような複雑なジョブフローを柔軟かつ宣言的に定義するための強力な機能として、FlowSplitDeciderを提供します。

これらの機能を活用することで、バッチ処理のロジックを明確にし、再利用性や保守性を高めることができます。

  • Flow (フロー):
    • 複数のステップや他のフローをまとめた論理的な単位です。再利用可能なサブフローを定義する際に役立ちます。
  • Split (スプリット):
    • 複数のフローを並行して実行するための機能です。処理時間を短縮したい場合に利用します。
  • Decider (デサイダー):
    • ステップの実行結果に基づいて、次にどのステップやフローに進むかをプログラム的に決定するためのコンポーネントです。複雑な条件分岐を実装する際に使用します。

これらの要素を組み合わせることで、ビジネスロジックに即した柔軟なバッチ処理のオーケストレーションが可能になります。


Spring Batchのジョブフロー制御の詳細については、以下の記事で詳細に解説していますので、是非ご覧ください。


2. 実装の準備:共通コンポーネントの作成

各フローの実装に入る前に、今回のハンズオンで共通して利用するコンポーネントを準備します。


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


2-1. LoggingTasklet: ステップの実行を記録する汎用タスクレット

どのステップが実行されたかをコンソールログで簡単に確認できるよう、ステップ名とジョブ名をログに出力するだけのシンプルなTaskletを作成します。
@StepScopeを利用して、実行時に自身のステップ名を取得している点がポイントです。

src/main/java/com/example/springbatchh2crud/tasklet/flow/LoggingTasklet.java

package com.example.springbatchh2crud.tasklet.flow;

import lombok.extern.slf4j.Slf4j;
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.Value;
import org.springframework.stereotype.Component;
import org.springframework.batch.core.configuration.annotation.StepScope;

@Component
@StepScope
@Slf4j
public class LoggingTasklet implements Tasklet {

    private final String stepName;

    /**
     * コンストラクタでステップ名を受け取ります。
     * `@StepScope`と`#{stepExecution.stepName}`を組み合わせることで、`Tasklet`が実行される際に`Spring Batch`が自動的に現在のステップ名をインジェクションしてくれます。
     * これにより、どのステップでこの`Tasklet`が使われても、自身のステップ名を正確にログに出力できます。
     */
    public LoggingTasklet(@Value("#{stepExecution.stepName}") String stepName) {
        this.stepName = stepName;
    }

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        String jobName = chunkContext.getStepContext().getJobName();
        log.info("=====>> Executing Job: [{}], Step: [{}]", jobName, stepName);
        return RepeatStatus.FINISHED;
    }
}

2-2. RandomStatusTasklet: 運命の分かれ道!ランダムに成功/失敗するTasklet

条件付きフローの「もしも」を体験するために、このTaskletはまるで「コイントス」のように、実行するたびにランダムで成功 (COMPLETED) または失敗 (FAILED) します。

Math.random()の結果が0.5未満の場合、意図的にRuntimeExceptionをスローしてステップを失敗させ、その後のフローがどう分岐するかを見ることができます。

src/main/java/com/example/springbatchh2crud/tasklet/flow/RandomStatusTasklet.java

package com.example.springbatchh2crud.tasklet.flow;

import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.StepScope;
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.Value;
import org.springframework.stereotype.Component;

@Component
@StepScope
@Slf4j
public class RandomStatusTasklet implements Tasklet {

    private final String stepName;

    public RandomStatusTasklet(@Value("#{stepExecution.stepName}") String stepName) {
        this.stepName = stepName;
    }

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        String jobName = chunkContext.getStepContext().getJobName();
        log.info("=====>> Executing Job: [{}], Step: [{}]", jobName, stepName);

        if (Math.random() < 0.5) {
            log.warn("<<<<<===== Step [{}] is FAILED", stepName);
            throw new RuntimeException("Step failed randomly!");
        }

        log.info("<<<<<===== Step [{}] is COMPLETED", stepName);
        return RepeatStatus.FINISHED;
    }
}

2-3. RandomDecider: ランダムに遷移先を決定するDecider

プログラムによるフロー決定を実現するため、JobExecutionDeciderを実装したクラスを作成します。

decideメソッド内で、ランダムにCONTINUEまたはSKIPというカスタムステータスを返却します。
このステータスが、ジョブフローの行き先を決定する条件となります。

src/main/java/com/example/springbatchh2crud/decider/RandomDecider.java

package com.example.springbatchh2crud.decider;

import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.job.flow.FlowExecutionStatus;
import org.springframework.batch.core.job.flow.JobExecutionDecider;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class RandomDecider implements JobExecutionDecider {

    @Override
    public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
        String jobName = jobExecution.getJobInstance().getJobName();

        if (Math.random() < 0.7) {
            log.info("=====>> Job: [{}], Decider result: [CONTINUE]", jobName);
            return new FlowExecutionStatus("CONTINUE");
        } else {
            log.info("=====>> Job: [{}], Decider result: [SKIP]", jobName);
            return new FlowExecutionStatus("SKIP");
        }
    }
}

3. 各種フローの実装

それでは、準備したコンポーネントを使って5つのジョブフローを実装していきましょう。

メンテナンス性を考慮し、各ジョブ定義は個別の設定ファイル(@Configurationクラス)に分割します。


より詳細な情報は、Spring Batchの公式ドキュメントControlling Step Flow章をご参照ください。


3-1. 順次フロー (Sequential Flow)

最も基本的なフローであり、バッチ処理の「工場ライン」をイメージすると分かりやすいでしょう。

start()で最初のステップを配置し、next()で次のステップを連結していくことで、定義した順番通りにステップが一つずつ確実に実行されます。

💡こんな時に使う!

  • データの抽出 → 加工 → 格納のように、処理順序が厳密に決まっている場合
  • 前処理が完了しないと次処理に進めない依存関係がある場合

src/main/java/com/example/springbatchh2crud/config/flow/SequentialFlowConfig.java

package com.example.springbatchh2crud.config.flow;

import com.example.springbatchh2crud.tasklet.flow.LoggingTasklet;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
@RequiredArgsConstructor
@Slf4j
public class SequentialFlowConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final LoggingTasklet loggingTasklet;

    @Bean
    public Step sequentialStep1() {
        return new StepBuilder("sequentialStep1", jobRepository)
                .tasklet(loggingTasklet, transactionManager)
                .build();
    }

    @Bean
    public Step sequentialStep2() {
        return new StepBuilder("sequentialStep2", jobRepository)
                .tasklet(loggingTasklet, transactionManager)
                .build();
    }

    @Bean
    public Step sequentialStep3() {
        return new StepBuilder("sequentialStep3", jobRepository)
                .tasklet(loggingTasklet, transactionManager)
                .build();
    }

    @Bean
    public Job sequentialFlowJob() {
        log.info("Defining sequentialFlowJob");
        return new JobBuilder("sequentialFlowJob", jobRepository)
                .start(sequentialStep1())
                .next(sequentialStep2())
                .next(sequentialStep3())
                .build();
    }
}

3-2. 条件付きフロー (Conditional Flow)

ステップの実行結果(ExitStatus)に応じて、次に実行する処理を分岐させます。on()メソッドの引数にステータスパターンを指定し、to()で行き先を指定します。

  • on("FAILED"): ステップが失敗した場合の遷移を定義します。
  • on("*"): ワイルドカード。任意のステータス(この文脈ではFAILED以外)にマッチします。

図:条件付きフロー (Conditional Flow)イメージ

条件付きフロー (Conditional Flow)イメージ

src/main/java/com/example/springbatchh2crud/config/flow/ConditionalFlowConfig.java

package com.example.springbatchh2crud.config.flow;

import com.example.springbatchh2crud.tasklet.flow.LoggingTasklet;
import com.example.springbatchh2crud.tasklet.flow.RandomStatusTasklet;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
@RequiredArgsConstructor
@Slf4j
public class ConditionalFlowConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final RandomStatusTasklet randomStatusTasklet;
    private final LoggingTasklet loggingTasklet;

    @Bean
    public Step conditionalStartStep() {
        return new StepBuilder("conditionalStartStep", jobRepository)
                .tasklet(randomStatusTasklet, transactionManager)
                .build();
    }

    @Bean
    public Step successStep() {
        return new StepBuilder("successStep", jobRepository)
                .tasklet(loggingTasklet, transactionManager)
                .build();
    }

    @Bean
    public Step failureStep() {
        return new StepBuilder("failureStep", jobRepository)
                .tasklet(loggingTasklet, transactionManager)
                .build();
    }

    @Bean
    public Job conditionalFlowJob() {
        log.info("Defining conditionalFlowJob");
        return new JobBuilder("conditionalFlowJob", jobRepository)
                .start(conditionalStartStep())
                .on("FAILED").to(failureStep())
                .from(conditionalStartStep()).on("*").to(successStep())
                .end()
                .build();
    }
}

3-3. 並行フロー (Split Flow)

関連性の低い複数の処理を並行実行することで、バッチ処理全体の時間を短縮できます。

まず、並列させたい一連のステップをFlowオブジェクトとしてまとめます。
その後、split()TaskExecutorを指定し、add()で並列実行したいFlowを追加していきます。

図:並行フロー (Split Flow)イメージ

並行フロー (Split Flow)イメージ

src/main/java/com/example/springbatchh2crud/config/flow/SplitFlowConfig.java

package com.example.springbatchh2crud.config.flow;

import com.example.springbatchh2crud.tasklet.flow.LoggingTasklet;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.FlowBuilder;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.job.flow.Flow;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
@RequiredArgsConstructor
@Slf4j
public class SplitFlowConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final LoggingTasklet loggingTasklet;

    @Bean
    public Step splitStepA1() {
        return new StepBuilder("splitStepA1", jobRepository)
                .tasklet(loggingTasklet, transactionManager)
                .build();
    }

    @Bean
    public Step splitStepA2() {
        return new StepBuilder("splitStepA2", jobRepository)
                .tasklet(loggingTasklet, transactionManager)
                .build();
    }

    @Bean
    public Step splitStepB1() {
        return new StepBuilder("splitStepB1", jobRepository)
                .tasklet(loggingTasklet, transactionManager)
                .build();
    }

    @Bean
    public Flow splitFlowA() {
        return new FlowBuilder<Flow>("splitFlowA")
                .start(splitStepA1())
                .next(splitStepA2())
                .build();
    }

    @Bean
    public Flow splitFlowB() {
        return new FlowBuilder<Flow>("splitFlowB")
                .start(splitStepB1())
                .build();
    }

    @Bean
    public Job splitFlowJob() {
        log.info("Defining splitFlowJob");
        return new JobBuilder("splitFlowJob", jobRepository)
                .start(splitFlowA())
                .split(new SimpleAsyncTaskExecutor())
                .add(splitFlowB())
                .end()
                .build();
    }
}

3-4. Deciderによるプログラム的フロー決定

on()だけでは表現しきれない複雑な条件分岐は、JobExecutionDecider(Decider)に任せましょう。

Deciderは、外部APIのレスポンス内容やDBの特定テーブルのレコード数など、任意の条件に基づいて遷移先を決定できる強力なコンポーネントです。

next()にDeciderのBeanを指定し、Deciderが返すカスタムステータス(今回はCONTINUE or SKIP)をon()で捕捉してフローを分岐させます。

図:Deciderによるプログラム的フローイメージ

Deciderによるプログラム的フローイメージ

src/main/java/com/example/springbatchh2crud/config/flow/DeciderFlowConfig.java

package com.example.springbatchh2crud.config.flow;

import com.example.springbatchh2crud.decider.RandomDecider;
import com.example.springbatchh2crud.tasklet.flow.LoggingTasklet;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
@RequiredArgsConstructor
@Slf4j
public class DeciderFlowConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final LoggingTasklet loggingTasklet;
    private final RandomDecider randomDecider;

    @Bean
    public Step deciderStartStep() {
        return new StepBuilder("deciderStartStep", jobRepository)
                .tasklet(loggingTasklet, transactionManager)
                .build();
    }

    @Bean
    public Step continueStep() {
        return new StepBuilder("continueStep", jobRepository)
                .tasklet(loggingTasklet, transactionManager)
                .build();
    }

    @Bean
    public Step skippedStep() {
        return new StepBuilder("skippedStep", jobRepository)
                .tasklet(loggingTasklet, transactionManager)
                .build();
    }

    @Bean
    public Job deciderFlowJob() {
        log.info("Defining deciderFlowJob");
        return new JobBuilder("deciderFlowJob", jobRepository)
                .start(deciderStartStep())
                .next(randomDecider)
                .on("CONTINUE").to(continueStep())
                .from(randomDecider).on("SKIP").to(skippedStep())
                .end()
                .build();
    }
}

3-5. ジョブの終了ステータス制御

ステップのExitStatusは、デフォルトのCOMPLETEDFAILEDだけでなく、カスタム値を設定できます。これにより、よりきめ細やかなフロー制御が可能になります。

以下の例では、statusControlStepBExitStatusCUSTOM_STATUSに設定し、on("CUSTOM_STATUS")で特別な後続処理(customEndStep)へ遷移させています。

図:ジョブの終了ステータス制御フローイメージ

ジョブの終了ステータス制御フローイメージ

src/main/java/com/example/springbatchh2crud/config/flow/StatusControlFlowConfig.java

package com.example.springbatchh2crud.config.flow;

import com.example.springbatchh2crud.tasklet.flow.LoggingTasklet;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.ExitStatus;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
@RequiredArgsConstructor
@Slf4j
public class StatusControlFlowConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final LoggingTasklet loggingTasklet;

    @Bean
    public Step statusControlStepA() {
        return new StepBuilder("statusControlStepA", jobRepository)
                .tasklet(loggingTasklet, transactionManager)
                .build();
    }

    @Bean
    public Step statusControlStepB() {
        return new StepBuilder("statusControlStepB", jobRepository)
                .tasklet((contribution, chunkContext) -> {
                    log.info("=====>> Executing Job: [{}], Step: [statusControlStepB]",
                            chunkContext.getStepContext().getJobName());
                    log.info("<<<<<===== Step [statusControlStepB] finished with custom status: [CUSTOM_STATUS]");
                    contribution.setExitStatus(new ExitStatus("CUSTOM_STATUS"));
                    return RepeatStatus.FINISHED; // Taskletの処理が完了したことを示す
                }, transactionManager)
                .build();
    }

    @Bean
    public Step successEndStep() {
        return new StepBuilder("successEndStep", jobRepository)
                .tasklet(loggingTasklet, transactionManager)
                .build();
    }

    @Bean
    public Step customEndStep() {
        return new StepBuilder("customEndStep", jobRepository)
                .tasklet(loggingTasklet, transactionManager)
                .build();
    }

    @Bean
    public Job statusControlFlowJob() {
        log.info("Defining statusControlFlowJob");
        return new JobBuilder("statusControlFlowJob", jobRepository)
                .start(statusControlStepA())
                .next(statusControlStepB())
                .on("CUSTOM_STATUS").to(customEndStep())
                .from(statusControlStepB()).on("*").to(successEndStep())
                .end()
                .build();
    }
}

4. 実行と確認

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


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

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

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

// ... (抜粋) ...
@RestController
@RequestMapping("/launch")
@Slf4j
public class JobLaunchController {
    // ... (コンストラクタで各Jobをインジェクション) ...

    // ... (共通のlaunchJobメソッド) ...

    // --- Flow Control Jobs ---

    @PostMapping("/jobs/sequential-flow")
    public ResponseEntity<String> launchSequentialFlowJob() {
        return launchJob(sequentialFlowJob);
    }

    @PostMapping("/jobs/conditional-flow")
    public ResponseEntity<String> launchConditionalFlowJob() {
        return launchJob(conditionalFlowJob);
    }

    @PostMapping("/jobs/split-flow")
    public ResponseEntity<String> launchSplitFlowJob() {
        return launchJob(splitFlowJob);
    }

    @PostMapping("/jobs/decider-flow")
    public ResponseEntity<String> launchDeciderFlowJob() {
        return launchJob(deciderFlowJob);
    }

    @PostMapping("/jobs/status-control-flow")
    public ResponseEntity<String> launchStatusControlFlowJob() {
        return launchJob(statusControlFlowJob);
    }
}

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

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

src/main/resources/static/index.html

<!-- ... (抜粋) ... -->
        <h2 class="group-title">Flow Control Jobs</h2>

        <div class="job-card">
            <h3>Sequential Flow Job</h3>
            <p><code>sequentialFlowJob</code>: 複数のステップを順番に実行する基本的なフロー。</p>
            <form action="/launch/jobs/sequential-flow" method="POST">
                <button type="submit">Launch sequentialFlowJob</button>
            </form>
        </div>

        <div class="job-card">
            <h3>Conditional Flow Job</h3>
            <p><code>conditionalFlowJob</code>: ステップの実行結果に応じて、次に実行するステップを切り替えるフロー。</p>
            <form action="/launch/jobs/conditional-flow" method="POST">
                <button type="submit">Launch conditionalFlowJob</button>
            </form>
        </div>

        <div class="job-card">
            <h3>Split Flow Job</h3>
            <p><code>splitFlowJob</code>: 複数のフローを並行して実行するフロー。</p>
            <form action="/launch/jobs/split-flow" method="POST">
                <button type="submit">Launch splitFlowJob</button>
            </form>
        </div>

        <div class="job-card">
            <h3>Decider Flow Job</h3>
            <p><code>deciderFlowJob</code>: Deciderを使用して、プログラム的に次のステップを決定するフロー。</p>
            <form action="/launch/jobs/decider-flow" method="POST">
                <button type="submit">Launch deciderFlowJob</button>
            </form>
        </div>

        <div class="job-card">
            <h3>Status Control Flow Job</h3>
            <p><code>statusControlFlowJob</code>: ステップの終了ステータスをカスタマイズし、ジョブの最終ステータスを制御するフロー。</p>
            <form action="/launch/jobs/status-control-flow" method="POST">
                <button type="submit">Launch statusControlFlowJob</button>
            </form>
        </div>
<!-- ... (抜粋) ... -->

4-3. 動作確認の手順

1. アプリケーションの起動

まず、以下のコマンドでアプリケーションを起動します。

./mvnw spring-boot:run

2. ブラウザからの実行

ブラウザで http://localhost:8080 を開きます。”Flow Control Jobs”セクションにある各ジョブの “Launch” ボタンをクリックすることで、それぞれのジョブを実行できます。

3. curlコマンドでの実行

別のターミナルから、以下のcurlコマンドを実行することでも各ジョブを起動できます。

# 順次フロー
curl -X POST http://localhost:8080/launch/jobs/sequential-flow

# 条件付きフロー
curl -X POST http://localhost:8080/launch/jobs/conditional-flow

# 並行フロー
curl -X POST http://localhost:8080/launch/jobs/split-flow

# Deciderフロー
curl -X POST http://localhost:8080/launch/jobs/decider-flow

# ステータス制御フロー
curl -X POST http://localhost:8080/launch/jobs/status-control-flow

実行後、アプリケーションを起動したターミナルのコンソールログを確認し、それぞれのジョブが設計通りのフローで実行されていることを確認します。


4-4. 実行結果のログサンプル

以下に、各ジョブを実行した際のコンソールログのサンプルと、その解説を示します。ご自身の結果と比較してみてください。

1. sequentialFlowJob のログ

c.e.s.tasklet.flow.LoggingTasklet        : =====>> Executing Job: [sequentialFlowJob], Step: [sequentialStep1]
o.s.batch.core.step.AbstractStep         : Step: [sequentialStep1] executed in 10ms
o.s.batch.core.job.SimpleStepHandler     : Executing step: [sequentialStep2]
c.e.s.tasklet.flow.LoggingTasklet        : =====>> Executing Job: [sequentialFlowJob], Step: [sequentialStep2]
o.s.batch.core.step.AbstractStep         : Step: [sequentialStep2] executed in 1ms
o.s.batch.core.job.SimpleStepHandler     : Executing step: [sequentialStep3]
c.e.s.tasklet.flow.LoggingTasklet        : =====>> Executing Job: [sequentialFlowJob], Step: [sequentialStep3]
o.s.batch.core.step.AbstractStep         : Step: [sequentialStep3] executed in 1ms

解説: sequentialStep1sequentialStep2sequentialStep3 の順で、定義通りにステップが逐次実行されていることがわかります。

2. conditionalFlowJob のログ (失敗ケース)

c.e.s.tasklet.flow.RandomStatusTasklet   : =====>> Executing Job: [conditionalFlowJob], Step: [conditionalStartStep]
c.e.s.tasklet.flow.RandomStatusTasklet   : <<<<<===== Step [conditionalStartStep] is FAILED
o.s.batch.core.step.AbstractStep         : Encountered an error executing step conditionalStartStep in job conditionalFlowJob
...
o.s.batch.core.job.SimpleStepHandler     : Executing step: [failureStep]
c.e.s.tasklet.flow.LoggingTasklet        : =====>> Executing Job: [conditionalFlowJob], Step: [failureStep]

解説: conditionalStartStepがランダムに失敗し、ExitStatusFAILEDになった結果、.on("FAILED").to(failureStep)の定義に従ってfailureStepに正しく遷移しています。成功した場合はsuccessStepが実行されます。

3. splitFlowJob のログ

[cTaskExecutor-2] o.s.batch.core.job.SimpleStepHandler     : Executing step: [splitStepA1]
[cTaskExecutor-1] o.s.batch.core.job.SimpleStepHandler     : Executing step: [splitStepB1]
[cTaskExecutor-2] c.e.s.tasklet.flow.LoggingTasklet        : =====>> Executing Job: [splitFlowJob], Step: [splitStepA1]
[cTaskExecutor-1] c.e.s.tasklet.flow.LoggingTasklet        : =====>> Executing Job: [splitFlowJob], Step: [splitStepB1]

解説: splitStepA1splitStepB1が、それぞれ異なるスレッド(cTaskExecutor-1, cTaskExecutor-2)で実行開始されています。これにより、2つのフローが並行して動作していることが確認できます。

4. deciderFlowJob のログ (SKIPケース)

c.e.s.tasklet.flow.LoggingTasklet        : =====>> Executing Job: [deciderFlowJob], Step: [deciderStartStep]
...
c.e.s.decider.RandomDecider              : =====>> Job: [deciderFlowJob], Decider result: [SKIP]
o.s.batch.core.job.SimpleStepHandler     : Executing step: [skippedStep]
c.e.s.tasklet.flow.LoggingTasklet        : =====>> Executing Job: [deciderFlowJob], Step: [skippedStep]

解説: RandomDeciderSKIPというステータスを返し、その結果に基づいて.on("SKIP").to(skippedStep)の定義通りにskippedStepが実行されています。DeciderCONTINUEを返した場合はcontinueStepが実行されます。

5. statusControlFlowJob のログ

c.e.s.tasklet.flow.LoggingTasklet        : =====>> Executing Job: [statusControlFlowJob], Step: [statusControlStepA]
...
o.s.batch.core.job.SimpleStepHandler     : Executing step: [statusControlStepB]
c.e.s.c.flow.StatusControlFlowConfig     : <<<<<===== Step [statusControlStepB] finished with custom status: [CUSTOM_STATUS]
...
o.s.batch.core.job.SimpleStepHandler     : Executing step: [customEndStep]
c.e.s.tasklet.flow.LoggingTasklet        : =====>> Executing Job: [statusControlFlowJob], Step: [customEndStep]

解説: statusControlStepBがカスタムの終了ステータスCUSTOM_STATUSを返却し、.on("CUSTOM_STATUS").to(customEndStep)の定義に従ってcustomEndStepへ正しく遷移していることが確認できます。


まとめ:Spring Batchで、あなたのバッチ処理はもっと進化する!

このハンズオンを通じて、Spring Batchが提供する宣言的で強力なジョブフロー制御の仕組みを深く理解できたことと思います。

順次フローから条件分岐、並列処理、そしてDeciderによる動的な制御まで、これらの機能を組み合わせることで、あなたのバッチ処理は単なるデータの流れから、複雑な業務ロジックを堅牢かつ効率的に実行する「賢いシステム」へと進化します。


免責事項

本記事の内容は、執筆時点での情報に基づいています。
技術の進歩や環境の変化により、内容が古くなる可能性や、特定の環境では動作しない可能性があります。
本記事の内容を実践する際は、ご自身の責任において十分な検証を行ってください。
本記事によって生じたいかなる損害についても、筆者および公開元は一切の責任を負いません。


SNSでもご購読できます。

コメントを残す

*