概要

業務システムのバッチ処理は、夜間の締め処理やデータ移行、日次集計など多くの場面で必要になります。Spring Batch のようなフレームワークを導入すれば一通りの機能は揃いますが、学習コストや設定の複雑さから、小規模なバッチやフレームワーク導入が難しい現場では Pure Java で組み立てる判断も珍しくありません。この記事では、バッチ処理を「前処理・本処理・後処理」の 3 フェーズに分離する BatchJob インターフェースを軸に、終了コードを表す ExitCode、実行時の共有データを運ぶ JobContext、そしてそれらを束ねる SimpleBatchRunner を定義します。フレームワークの内部構造を理解するうえでも、この骨格設計は有用です。

使いどころ

夜間バッチの共通実行基盤を自前で構築し、ジョブの追加・差し替えを容易にする

既存の手続き型バッチをインターフェースベースにリファクタリングして保守性を高める

Spring Batch を導入する前段階として、バッチの基本構造を社内で共有する

コード例

BatchJob インターフェースと SimpleBatchRunner
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

enum ExitCode {
    SUCCESS(0), WARNING(1), ERROR(2);
    private final int code;
    ExitCode(int code) { this.code = code; }
    public int getCode() { return code; }
}

class JobContext {
    private final String jobName;
    private final Map<String, Object> attributes = new HashMap<String, Object>();
    private long startTimeMillis;

    public JobContext(String jobName) { this.jobName = jobName; }
    public String getJobName() { return jobName; }
    public void setAttribute(String key, Object value) { attributes.put(key, value); }

    @SuppressWarnings("unchecked")
    public <T> T getAttribute(String key, Class<T> type) {
        Object value = attributes.get(key);
        return (value != null && type.isInstance(value)) ? (T) value : null;
    }
    public long getStartTimeMillis() { return startTimeMillis; }
    public void setStartTimeMillis(long v) { this.startTimeMillis = v; }
}

interface BatchJob {
    void initialize(JobContext context) throws Exception;
    ExitCode execute(JobContext context) throws Exception;
    void terminate(JobContext context);
}

class SimpleBatchRunner {
    private static final Logger LOGGER = Logger.getLogger(SimpleBatchRunner.class.getName());

    public ExitCode run(BatchJob job, JobContext context) {
        ExitCode exitCode = ExitCode.ERROR;
        context.setStartTimeMillis(System.currentTimeMillis());
        LOGGER.info("[" + context.getJobName() + "] ジョブ開始");
        try {
            job.initialize(context);
            exitCode = job.execute(context);
        } catch (Exception e) {
            LOGGER.log(Level.SEVERE, "[" + context.getJobName() + "] エラー", e);
        } finally {
            try { job.terminate(context); } catch (Exception e) {
                LOGGER.log(Level.WARNING, "後処理エラー", e);
            }
            long elapsed = System.currentTimeMillis() - context.getStartTimeMillis();
            LOGGER.info("[" + context.getJobName() + "] 終了 (" + elapsed + "ms) コード: " + exitCode.getCode());
        }
        return exitCode;
    }
}

class SampleJob implements BatchJob {
    @Override public void initialize(JobContext ctx) { ctx.setAttribute("count", Integer.valueOf(0)); }
    @Override public ExitCode execute(JobContext ctx) { ctx.setAttribute("count", Integer.valueOf(42)); return ExitCode.SUCCESS; }
    @Override public void terminate(JobContext ctx) {
        System.out.println("処理件数: " + ctx.getAttribute("count", Integer.class));
    }
}

public class BatchFrameworkDesign {
    public static void main(String[] args) {
        ExitCode result = new SimpleBatchRunner().run(new SampleJob(), new JobContext("SampleJob"));
        System.out.println("終了コード: " + result.getCode());
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

sealed interface で BatchJob の実装クラスを制限できる。switch 式で ExitCode ごとの処理分岐を簡潔に書ける。

Java 17
// Java 17: sealed interface で実装を制限
public sealed interface BatchJob
        permits CsvImportJob, ReportJob {
    void initialize(JobContext context) throws Exception;
    ExitCode execute(JobContext context) throws Exception;
    void terminate(JobContext context);
}

Library Comparison

Pure Java(自前フレームワーク)小規模バッチやフレームワーク導入が制約される現場。ジョブの種類が少ない場合。リトライ、スキップ、チャンク分割は自前で実装する必要がある。
Spring Batchチャンクステップ、リトライ、ジョブフロー、実行履歴管理が必要な場合。Spring 依存が前提。小規模バッチには過剰なことが多い。
JSR 352(JBatch)Jakarta EE 環境でバッチを標準仕様に沿って実装する場合。実装ランタイムに依存する。SE 環境では追加設定が必要。

注意点

ExitCode を System.exit() で返す場合、JVM シャットダウンフックとの順序に注意すること。finally ブロックが実行されない場合がある

JobContext に何でも入れると依存関係が不透明になる。入れるデータの型と用途を決めておくこと

initialize() で例外が出た場合に terminate() を呼ぶかどうか、呼び出し側の責務を明確にしておくこと

バッチの戻り値を int ではなく enum にすることで、未定義コードの混入を防ぐ

FAQ

BatchJob のメソッドに戻り値を持たせるべきですか。

execute() は ExitCode を返す設計が実用的です。initialize() と terminate() は void で十分です。

JobContext は Map で実装して問題ありませんか。

小規模なら HashMap で十分です。キー名の定数化と取得時のキャストをラッパーメソッドに集約すると保守しやすくなります。

前処理と後処理は本当に分ける必要がありますか。

前処理でファイル確認やDB接続確認をすることで、本処理前にエラーを検知できます。後処理はリソース解放の保証に有用です。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます