概要
バッチ処理でエラーが発生したとき、どう振る舞うかは業務要件によって大きく異なります。決済データの取込なら1件でもエラーがあれば全体を止めるべきですし、大量のログ集計であればエラー行をスキップして残りを処理するほうが現実的です。この記事では、エラー時の振る舞いを「異常終了(即停止)」「継続(ログ記録して次へ)」「スキップ(N件まで許容、超過で停止)」の3つの戦略として定義し、ErrorPolicy enum と ErrorHandler クラスで切り替え可能に実装します。CSV行単位の処理を例に、レコードごとのエラーハンドリングパターンを示します。
使いどころ
決済データ取込バッチで不正レコードを検出した場合に全件ロールバックして即停止する
アクセスログ集計バッチで解析できない行をスキップし残りの集計を継続する
顧客マスタ同期バッチでエラーが100件を超えたらデータ品質に問題ありとして停止する
コード例
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
public class BatchErrorHandling {
public enum ErrorPolicy { FAIL_FAST, CONTINUE, SKIP_LIMIT }
public interface RecordProcessor { void process(String record) throws Exception; }
public static class ErrorHandler {
private static final Logger LOGGER = Logger.getLogger(ErrorHandler.class.getName());
private final ErrorPolicy policy;
private final int maxSkips;
private int skipCount;
private final List<String> skipped = new ArrayList<String>();
public ErrorHandler(ErrorPolicy policy, int maxSkips) {
this.policy = policy; this.maxSkips = maxSkips;
}
public void handle(int lineNum, String record, Exception e) {
switch (policy) {
case FAIL_FAST:
LOGGER.severe("行" + lineNum + "で中断: " + e.getMessage());
throw new RuntimeException("行" + lineNum, e);
case CONTINUE:
LOGGER.warning("行" + lineNum + "スキップ: " + e.getMessage());
skipped.add(lineNum + ":" + record);
break;
case SKIP_LIMIT:
skipCount++;
skipped.add(lineNum + ":" + record);
if (skipCount > maxSkips) throw new RuntimeException("スキップ上限超過", e);
LOGGER.warning("行" + lineNum + "スキップ(" + skipCount + "/" + maxSkips + ")");
break;
}
}
public int getSkipCount() { return skipCount; }
public List<String> getSkipped() { return skipped; }
}
public static int processRecords(List<String> records, RecordProcessor proc, ErrorHandler handler) {
int success = 0;
for (int i = 0; i < records.size(); i++) {
try { proc.process(records.get(i)); success++; }
catch (Exception e) { handler.handle(i + 1, records.get(i), e); }
}
return success;
}
public static void main(String[] args) {
List<String> lines = Arrays.asList("1001,田中,50000", "1002,鈴木,INVALID", "1003,佐藤,30000", "1004,山田,-500", "1005,高橋,45000");
RecordProcessor proc = new RecordProcessor() {
@Override public void process(String record) throws Exception {
String[] f = record.split(",");
int amount = Integer.parseInt(f[2]);
if (amount < 0) throw new IllegalArgumentException("金額が負数: " + amount);
System.out.println("処理: " + f[1] + " / " + amount + "円");
}
};
ErrorHandler handler = new ErrorHandler(ErrorPolicy.SKIP_LIMIT, 3);
int ok = processRecords(lines, proc, handler);
System.out.println("成功: " + ok + " / スキップ: " + handler.getSkipCount());
}
}Version Coverage
sealed interface でエラー結果を型安全に分類できる。switch 式で分岐を式として書ける。
// Java 17: switch 式で分岐結果を返す
boolean shouldContinue = switch (policy) {
case FAIL_FAST -> throw new RuntimeException("中断");
case CONTINUE -> { yield true; }
case SKIP_LIMIT -> { skipCount++; yield skipCount <= maxSkips; }
};Library Comparison
注意点
異常終了戦略でも終了前にエラー内容とどこまで処理したかをログに記録すること
スキップ上限を設定しない継続戦略はデータ全体が壊れていても走りきるリスクがある
エラーハンドリングのテストでは先頭行・末尾行・連続エラー・全行エラーを網羅する
スキップしたレコードの一覧を別ファイルに出力しておくと再処理に活用できる
FAQ
データの正確性が最優先なら FAIL_FAST、一部不備が許容されるなら SKIP_LIMIT です。
スキップしたレコードをエラーCSVに書き出し、修正後に同じバッチで再投入する構成が一般的です。
正常のみ・先頭行エラー・末尾行エラー・上限ちょうど・上限超過・全行エラーの6パターンです。