本ページはプロモーションが含まれています

Java入門

【2026年版】Javaバッチ処理の実装方法3選と設計のコツ

CATEGORY
REV.
DRAWN BY
TOM
SHEETS
7枚
Batch(バッチ処理の実装方法3選)

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

𝕏

Javaでバッチ処理を作るだけなら、mainメソッドを1つ書いてcronで呼べば足ります。

難しいのは動かすことではなく、止まったあとの始末です。

夜間に走らせた処理が途中で止まり、再実行すると二重登録になりそうで手が出せない。この状況を作らない設計が、バッチ開発の本題になります。

この記事では、Javaでバッチを作る3つの方法と選び方、手元で実行して確かめたサンプル、再実行に強い設計のポイント5つをまとめました。

Spring Batchの中身は別記事に任せ、ここは「何で作るか」を決める入口に絞っています。

Java 23.0.1で動作を確認しています(2026年10月)。

この記事でわかること

  • バッチ処理の定義と、リアルタイム処理との違い
  • Javaでバッチを作る3つの方法と、規模別の選び方
  • mainメソッドだけで作る日次集計バッチ(実行結果つき)
  • 冪等性・再実行・トランザクション・ログ・失敗時の備えという設計ポイント5つ

バッチ処理とは?リアルタイム処理との違い

バッチ処理とは、まとめたデータを人の操作なしに一括で処理する方式です。

夜間の売上集計、月末の請求書作成、他システムへのデータ移行などが典型で、Javaに限らず業務システムの裏側で広く使われています。

設計の考え方は言語を問わず共通なので、後半の設計ポイントは他の言語でもそのまま使えます。

バッチ処理が向いている処理

バッチ処理が向くのは、結果がすぐに要らず、件数が多く、正確さが問われる処理です。

リアルタイム処理との違いを表にまとめます。

観点バッチ処理リアルタイム処理
動くきっかけ時刻・件数のまとまり利用者の操作・イベント
重視するもの正確さ・処理量(スループット)応答の速さ
失敗したとき原因を直して再実行する利用者にエラーを返す
典型例日次集計、月次請求、データ移行画面操作、決済、チャット

リアルタイム処理は「失敗を利用者に見せる」処理、バッチ処理は「失敗を後から取り戻す」処理です。取り戻せる設計になっているかどうかが、バッチの出来を決めます。

バッチファイル(.bat)とは別物

バッチファイルは、Windowsで拡張子 .bat のコマンドを順に実行するファイルです。

バッチ処理は処理の方式を指す言葉なので、別の概念です。

.bat でJavaプログラムを起動して定期実行することはできますが、それはバッチ処理を実現する手段の1つにすぎません。

Javaでバッチ処理を作る3つの方法

Javaでバッチを作る方法は、処理を起動する主体で3つに分けると選びやすくなります。

OSのスケジューラが起動するのか、常駐するJavaアプリが自分で起動するのか、フレームワークが実行管理まで担うのか、という違いです。

次の図は、失敗時の再開が必要か、常駐アプリの中で動かすか、という順に判断する流れをまとめたものです。

バッチ処理の作り方を選ぶ判断表(Q1から順に答える)
QUESTION / 質問
はい
いいえ
Q1. 失敗したら途中から再開したい?件数ごとにコミットしたい場合も含む
方法3:Spring Batch・jBatchバッチフレームワーク
Q2へ
Q2. Webアプリなど常駐するJavaの中で動かす?
Q3へ
方法1:main+cron・launchd・タスクスケジューラOSのスケジューラが起動する
Q3. 暦指定やジョブの永続化が必要?
方法2:Quartzアプリ内スケジューラ
方法2:ScheduledExecutorServiceアプリ内スケジューラ(追加ライブラリ不要)

3つの方法を並べると、次のようになります。

方法向く場面再実行・リトライ追加ライブラリ
1. main+OSスケジューラ単発の定期処理、1ジョブ1プロセス自作不要
2. アプリ内スケジューラ常駐アプリが定期的に動かす処理自作(Quartzは実行状態を保存できる)ScheduledExecutorServiceは不要、Quartzは必要
3. バッチフレームワーク大量データ、途中再開、件数ごとのコミット標準機能必要

方法1:mainメソッド+OSのスケジューラ

mainメソッドに処理を書き、cron(Linux・macOS)、launchd(macOS)、タスクスケジューラ(Windows)から起動する方法です。

最も単純で、処理が終わればプロセスごと消えるため、メモリの掃除や常駐の管理を考えなくて済みます。

小さな定期処理なら、この形で足ります。複雑さが必要になった時点で、方法2か3へ乗り換えれば間に合います。

弱点は、再実行や失敗時の通知をすべて自分で書く点です。この記事の後半で扱う設計ポイントは、主にこの弱点を埋める話になります。

方法2:アプリ内で動かすスケジューラ

Webアプリのように常駐するJavaプロセスが、自分で処理を起動する方法です。

JDK標準の ScheduledExecutorService なら、追加ライブラリなしで一定間隔の実行ができます。

毎月最終金曜の朝のように暦に沿って指定したい場合や、祝日を除外したい場合、ジョブの状態をDBに保存したい場合は、Quartzの使い方で扱っているQuartzが候補です。

ScheduledExecutorService には、知らないと痛い仕様があります。

Javadocには、実行中のタスクが例外を出すと以降の実行は抑止される、と書かれています。

実際に、2回目だけ例外を投げるタスクで確かめました。

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class SchedulerDemo {
    public static void main(String[] args) throws Exception {
        AtomicInteger count = new AtomicInteger();
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

        scheduler.scheduleWithFixedDelay(() -> {
            int n = count.incrementAndGet();
            try {
                System.out.println(n + "回目を実行");
                if (n == 2) {
                    throw new IllegalStateException("2回目だけ失敗");
                }
            } catch (Exception e) {
                // 握りつぶさず記録し、次回の実行は止めない
                System.err.println("失敗: " + e.getMessage());
            }
        }, 0, 200, TimeUnit.MILLISECONDS);

        Thread.sleep(1000);
        scheduler.shutdown();
        scheduler.awaitTermination(5, TimeUnit.SECONDS);
        System.out.println("合計 " + count.get() + " 回");
    }
}

1秒間に200ミリ秒間隔で動かした結果は、try-catchありで5回、なしだと2回で止まりました。

しかも、なしの場合はスタックトレースも出ず、プロセスは正常終了します。

「気づいたら定期実行が止まっていた」の原因として、最も静かなパターンです。タスクの最外周は、必ず try-catch で囲んでください。

実行確認のメモ
Java 23.0.1で java SchedulerDemo.java として実行しました。scheduleAtFixedRate でも、Javadocの記述は同じです。

方法3:バッチフレームワーク(Spring Batch・jBatch)

大量データを一定件数ごとにコミットしたい、失敗したら途中から再開したい、という要件が出たら、フレームワークの出番です。

代表は Spring Batch で、実行履歴をDBに保存して再実行を管理する仕組みを標準で持っています。

Jakarta EEのアプリケーションサーバー上で動かすなら、標準仕様の Jakarta Batch(jBatch) が選択肢に入ります。

Job・Step・Chunkの構造、Spring Boot 3・4での設定、リトライとスキップの書き方は、Spring Batchとは?Bootとの違いにまとめています。

この記事では深入りしません。「フレームワークに頼る前に、自作でどこまで行けるか」を測る材料を、次の章から出していきます。

mainメソッドで作る最小のJavaバッチ

方法1の実物を作ります。入力は日付つきの売上CSV、出力は商品コード別の合計CSVです。

作りは単純でも、あとで効く3点を最初から入れました。日付を引数で受けること、結果を一時ファイル経由で置き換えること、終了コードで結果を伝えることです。

サンプルコード:日次売上の集計

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.time.LocalDate;
import java.util.Map;
import java.util.TreeMap;

/** 指定日の売上CSVを商品コード別に集計して書き出すバッチ。 */
public class DailySalesBatch {

    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("使い方: java DailySalesBatch.java 2026-10-04");
            System.exit(64);
        }
        LocalDate target = LocalDate.parse(args[0]);
        Path in = Path.of("sales_" + target + ".csv");
        Path out = Path.of("summary_" + target + ".csv");
        Path tmp = Path.of("summary_" + target + ".csv.tmp");

        try {
            Map<String, Long> totals = new TreeMap<>();
            int bad = 0;
            int lineNo = 0;
            // 1行ずつ読むので、入力が大きくてもメモリを食わない
            try (BufferedReader reader = Files.newBufferedReader(in, StandardCharsets.UTF_8)) {
                String line;
                while ((line = reader.readLine()) != null) {
                    lineNo++;
                    String[] cols = line.split(",");
                    try {
                        totals.merge(cols[0], Long.parseLong(cols[1].trim()), Long::sum);
                    } catch (RuntimeException e) {
                        bad++;
                        System.err.println("WARN " + lineNo + "行目をスキップ: " + line);
                    }
                }
            }

            // 一時ファイルに書いてから置き換える。途中で落ちても out は壊れない
            StringBuilder sb = new StringBuilder();
            totals.forEach((k, v) -> sb.append(k).append(',').append(v).append('\n'));
            Files.writeString(tmp, sb.toString(), StandardCharsets.UTF_8);
            Files.move(tmp, out, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);

            System.out.println("INFO 集計完了 商品数=" + totals.size() + " スキップ=" + bad);
            System.exit(bad > 0 ? 1 : 0);
        } catch (IOException e) {
            System.err.println("ERROR 入出力に失敗: " + e);
            System.exit(2);
        }
    }
}

JDK 11以降なら、コンパイルなしで java DailySalesBatch.java 2026-10-04 と実行できます。

本番ではGradleかMavenでJARにして java -jar で起動する形が一般的ですが、動作の確認にはソース直接実行が手軽です。

実行結果と終了コードの見方

次の5行を sales_2026-10-04.csv に置いて実行しました。4行目は金額が文字、5行目は列が足りない不正行です。

A001,1200
A002,800
A001,300
A003,abc
A002

結果は、不正行を警告つきでスキップしたうえで、有効な3行を集計して終了コード1で終わりました。

WARN 4行目をスキップ: A003,abc
WARN 5行目をスキップ: A002
INFO 集計完了 商品数=2 スキップ=2

出力の summary_2026-10-04.csv は A001,1500 と A002,800 の2行です。

同じ入力でもう一度実行し、出力ファイルのmd5を比べると、1回目と2回目で一致しました。再実行しても結果が変わらない、というのが次章の冪等性です。

終了コードは4種類に分けています。

終了コード意味運用側の動き
0全件成功何もしない
1完了したが不正行あり不正行を確認する
2入出力エラーで中断原因を直して再実行する
64引数が足りない呼び出し側の設定を直す

64は、UNIX系で「使い方の誤り」に使う慣習の番号です。

入力ファイルがない2026-10-05を指定すると、NoSuchFileException を出して終了コード2になりました。

cron・launchd・タスクスケジューラで定期実行する

起動はOSに任せます。Linuxのcronなら、毎日午前2時30分に前日分を集計する設定は次のとおりです。

30 2 * * * cd /opt/batch && java DailySalesBatch.java $(date -d yesterday +\%F) >> /var/log/batch/daily_sales.log 2>&1

date -d yesterday はGNU版dateの書き方で、Linuxで使えます。crontabでは % を \% と書く決まりです。

macOSで同じことをするなら、launchdで定期実行する手順が参考になります。私が個人のスクリプトを回すときも、macOSではlaunchdを使っています。

cronで初めて動かすと、手元で動いたはずの処理が動かない場面によく出会います。理由はたいてい次のどれかです。

  • cronの環境変数は最小限で、PATH に java が含まれていない場合がある(フルパスで書く)
  • 作業ディレクトリが想定と違う(cd で移動するか、絶対パスで書く)
  • 標準出力と標準エラーを捨てている(>> ログ 2>&1 で残す)
  • 前回の実行が終わる前に次が始まる(ロックファイルや、Linuxなら flock で防ぐ)

Windowsのタスクスケジューラも考え方は同じで、実行ファイルのフルパスと「開始」ディレクトリを明示します。

失敗しにくいバッチの設計ポイント5つ

ここからは、言語を問わないバッチ設計の要点を5つ見ていきます。

先に挙げた3つの方法のどれを選んでも、この5点は自分で決める必要があります。方法3のフレームワークを使えば、一部は機能として肩代わりしてくれるだけです。

冪等性:再実行しても結果が同じになる

冪等性とは、同じ処理を何回実行しても結果が同じになる性質です。

バッチは必ず一度は失敗するので、冪等でなければ「再実行が怖い」状態になります。

冪等でない作りと冪等な作り(途中で止まって再実行したとき)
NG / 冪等でない作り毎回 INSERT するだけ再実行すると、処理済みの分が二重に登録される
OK / 冪等な作り対象日の結果を丸ごと置き換える何回実行しても、最後は同じ状態になる

冪等にする方法は、出力先によって選びます。

出力先冪等にする方法
ファイル一時ファイルに書いてから置き換える(前章のサンプル)
DB(日単位の集計)同じトランザクション内で、対象日を削除してから入れ直す
DB(1件ずつ更新)UPSERT を使う(PostgreSQLは ON CONFLICT DO UPDATE、MySQLは ON DUPLICATE KEY UPDATE、標準SQLは MERGE)
外部API・メール処理済みIDを記録し、2回目以降は送らない

もう1つ、冪等の前提になる習慣があります。サンプルのように、対象日を LocalDate.now() ではなく引数で受けることです。

現在日付で動くバッチは、3日前の分だけを作り直したくなったときに手の打ちようがありません。

再実行:どこからやり直すかを決めておく

失敗したとき、やり直しの範囲は3段階で考えられます。

自作するなら、上から順に試してください。

  1. 全部やり直す(冪等なら最も安全で、実装も最小)
  2. 対象期間やID範囲を引数で指定して、そこだけやり直す
  3. 処理済みの位置を保存しておき、続きから再開する

3は実装が一気に重くなります。位置の保存とデータの書き込みが別々だと、保存だけ成功して書き込みが失敗する、といった食い違いが生まれるためです。

ここまで必要になった時点が、Spring Batchのようなフレームワークに乗り換える合図です。実行履歴をDBに持って、続きから再開する機能が標準で用意されています。

トランザクション:コミットの単位を決める

コミットの単位は、次の3通りから選びます。

単位利点欠点
全件で1回失敗したら全部元に戻り、状態が単純件数が多いとロックが長く、ログ領域も食う
1件ごと失敗しても済んだ分が残る遅い。途中までの状態が残る
一定件数(チャンク)速度と巻き戻し範囲のバランスが良い途中までコミット済みの状態が残る

大量データでは、チャンクが現実的な落としどころです。JDBCで1000件ごとにコミットする骨格は次のとおりです。

static void insertInChunks(Connection con, List<Sale> sales) throws SQLException {
    final int chunkSize = 1000;
    String sql = "INSERT INTO daily_sales (sales_date, product_code, amount) VALUES (?, ?, ?)";
    con.setAutoCommit(false);
    try (PreparedStatement ps = con.prepareStatement(sql)) {
        int inChunk = 0;
        for (Sale s : sales) {
            ps.setDate(1, Date.valueOf(s.date()));
            ps.setString(2, s.code());
            ps.setLong(3, s.amount());
            ps.addBatch();
            if (++inChunk == chunkSize) {
                ps.executeBatch();
                con.commit();
                inChunk = 0;
            }
        }
        if (inChunk > 0) {
            ps.executeBatch();
            con.commit();
        }
    } catch (SQLException e) {
        con.rollback();
        throw e;
    }
}

Sale は日付・商品コード・金額を持つ record です。接続先のDB用ドライバを用意すれば、そのまま呼べます。

失敗時に巻き戻るのは直近のチャンクだけで、それ以前の分はコミット済みで残ります。だから、チャンク方式は前節の冪等性とセットで使います。

コミットとロールバックの仕組み自体は、Javaトランザクションの解説を参照してください。

ログ:何件目のどのデータかを残す

バッチのログは、あとから再実行の判断をするための材料です。

開始と終了の時刻、読み込んだ件数、書き込んだ件数、スキップした件数の4つは、最低限そろえておきます。

サンプルでも、不正行は「4行目をスキップ: A003,abc」と、位置と中身が分かる形で出しました。

新人のころは、原因調査のために System.out.println を埋め込みながら動かしていました。

本番で原因不明のエラーが出たとき、残っているログから分かるのは「落ちた」ことだけで、特定に何日もかかりました。

バッチは夜間に無人で動くので、その場で調べ直せません。落ちた時点の入力と位置が、ログに残っているかどうかですべてが決まります。

実務では System.out ではなく、ログレベルとファイル出力を管理できるロギングライブラリを使います。設定はlogback.xmlの書き方にまとめています。

ログに出してはいけないものもあります。

  • パスワード・トークン・接続文字列
  • 氏名・住所・電話番号などの個人情報(代わりにIDを出す)
  • 1件ごとの全項目ダンプ(件数が多いと、ログ自体が先に溢れる)

失敗時のリカバリ:検知・通知・リトライ

失敗への備えは、検知・通知・復旧の3段で考えます。

  1. 検知:終了コードが0以外なら失敗とみなす。方法2では、例外を握ってログに出す
  2. 通知:失敗がメールやチャットに届くようにする。cronなら MAILTO や、ラッパースクリプトからのWebhook通知
  3. 復旧:原因を直して再実行する。一時的な障害だけは自動リトライする

リトライは、効く場面が限られます。

DB接続のタイムアウトや外部APIの一時的な503のように、時間を置けば直る障害には有効です。

一方、不正データやプログラムの不具合は、何度試しても結果が変わりません。リトライを3回、間隔を数秒から数分と決めて、それでも失敗したら人に知らせて止めるのが基本です。

最後に、実行が長引いたときの備えです。

前回の実行が終わらないまま次の起動時刻が来ると、同じ入力を2つのプロセスが同時に処理します。多重起動の防止(ロックファイル、flock、DBの排他制御)と、実行時間の上限の監視は、無人運用では欠かせません。

どの方法を選ぶか:使い分けの目安

最初は方法1で作り、困ったら上に上がる、で十分です。

それぞれの乗り換えの合図を表にしました。

いまの方法乗り換えの合図移る先
1. main+OSスケジューラ起動のたびに接続や初期化が重い/常駐アプリから起動したい方法2
2. ScheduledExecutorService暦指定やジョブの永続化が必要になったQuartz
1・2のどちらでも途中再開や件数ごとのコミットを自作するのが重くなった方法3(Spring Batchなど)

数百件から数万件を、夜に1回まとめて処理する程度なら、方法1と冪等な設計で足ります。

フレームワークは、再開位置の管理を自作するのが割に合わなくなったときの道具です。入れるほど安心、という種類のものではありません。

よくある質問

Javaのバッチ処理について、よく出る質問をまとめました。

Q1. Javaでバッチ処理を作る最短の方法は?

A1. mainメソッドに処理を書き、cronやタスクスケジューラから起動する方法が最短です。JDK 11以降ならソースを直接実行できるので、まず java Batch.java で動かして確かめます。

Q2. バッチ処理とバッチファイル(.bat)は同じですか?

A2. 別物です。バッチ処理は一括処理の方式で、バッチファイルはWindowsのコマンドを並べた実行ファイルです。.bat はバッチ処理を起動する手段の1つにすぎません。

Q3. Spring Batchは小規模でも使うべきですか?

A3. 数百件から数万件を1日1回処理する程度なら、必須ではありません。途中再開や件数ごとのコミットを自作するのが重くなったときに検討します。使い方はSpring Batchの解説記事にあります。

Q4. ScheduledExecutorServiceで例外が出るとどうなりますか?

A4. 以降の実行が抑止され、定期実行が止まります。Javadocにも書かれている仕様で、Java 23.0.1で確認しました。タスクの最外周を try-catch で囲み、例外をログに残してください。

Q5. バッチ処理が失敗したときの対処は?

A5. ログで失敗した入力と位置を特定し、一時的な障害ならリトライ、データ不正なら修正してから再実行します。冪等に作ってあれば、再実行で二重登録になる心配はありません。

Q6. バッチ処理のメモリ不足はどう防ぎますか?

A6. 全件をListに読み込まず、1行ずつ、または一定件数ずつ処理します。ファイルなら BufferedReader、DBならカーソルやページングを使い、件数ごとにコミットします。

Q7. バッチの開発で最初に決めることは?

A7. 入力、出力、起動のきっかけ、失敗したときの扱いの4つです。とくに「失敗したらどうやり直すか」を先に決めておくと、冪等性や引数の設計が自然に決まります。

まとめ:まず方法1で作り、再実行の設計を先に決める

Javaのバッチ処理は、mainメソッドとOSのスケジューラで作り始められます。

  • 方法は3つ。main+OSスケジューラ、アプリ内スケジューラ、バッチフレームワーク
  • 選ぶ基準は、途中再開と件数ごとのコミットが要るかどうか
  • 設計の核は、冪等性・再実行の範囲・コミット単位・ログ・失敗時の備えの5つ
  • ScheduledExecutorService は、例外で黙って止まる。最外周に try-catch を置く

コードを書く前に、「失敗したらどうやり直すか」を1行で書いてみてください。それが決まれば、使う道具は自然に絞れます。

公式の情報は、ScheduledExecutorServiceのJavadocと、Jakarta Batchの仕様ページで確認できます。

REVISIONS / 改訂履歴
REV.DATE内容
52026-10-19同じ説明の繰り返しを減らし、読みやすく整えた
42026-10-15図解を描き直し(図面のデザインにそろえた)
32026-08-182026年8月時点の情報で全体を見直し(古い記述を3件更新)
22026-05-092026年5月時点の情報で全体を見直し(古い記述を3件更新)
12025-10-05初版
NEXT READ本棚から1本

AIで開発を組み直すなら、まずこの5冊から

入門・実践・設計まで、Claude Codeの本をレベル別に5冊選んで比べました。

→ おすすめ5冊を見る

  • この記事を書いた人
  • 最新記事

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

𝕏

Java入門