概要
業務システムのバッチ処理は、夜間の締め処理やデータ移行、日次集計など多くの場面で必要になります。Spring Batch のようなフレームワークを導入すれば一通りの機能は揃いますが、学習コストや設定の複雑さから、小規模なバッチやフレームワーク導入が難しい現場では Pure Java で組み立てる判断も珍しくありません。この記事では、バッチ処理を「前処理・本処理・後処理」の 3 フェーズに分離する BatchJob インターフェースを軸に、終了コードを表す ExitCode、実行時の共有データを運ぶ JobContext、そしてそれらを束ねる SimpleBatchRunner を定義します。フレームワークの内部構造を理解するうえでも、この骨格設計は有用です。
使いどころ
夜間バッチの共通実行基盤を自前で構築し、ジョブの追加・差し替えを容易にする
既存の手続き型バッチをインターフェースベースにリファクタリングして保守性を高める
Spring Batch を導入する前段階として、バッチの基本構造を社内で共有する
コード例
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());
}
}Version Coverage
sealed interface で BatchJob の実装クラスを制限できる。switch 式で ExitCode ごとの処理分岐を簡潔に書ける。
// 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
注意点
ExitCode を System.exit() で返す場合、JVM シャットダウンフックとの順序に注意すること。finally ブロックが実行されない場合がある
JobContext に何でも入れると依存関係が不透明になる。入れるデータの型と用途を決めておくこと
initialize() で例外が出た場合に terminate() を呼ぶかどうか、呼び出し側の責務を明確にしておくこと
バッチの戻り値を int ではなく enum にすることで、未定義コードの混入を防ぐ
FAQ
execute() は ExitCode を返す設計が実用的です。initialize() と terminate() は void で十分です。
小規模なら HashMap で十分です。キー名の定数化と取得時のキャストをラッパーメソッドに集約すると保守しやすくなります。
前処理でファイル確認やDB接続確認をすることで、本処理前にエラーを検知できます。後処理はリソース解放の保証に有用です。