概要

バッチ処理の設計で最も基本的かつ重要なのは、前処理・本処理・後処理の責務を明確に分離することです。前処理で入力ファイルの存在確認や形式チェックを済ませておけば、本処理で初歩的なエラーに悩まされることはありません。後処理で処理件数やエラー件数のサマリを出力すれば、運用担当者が実行結果を即座に判断できます。この記事では、BatchJob インターフェースを CSV 取込ジョブとして実装し、入力ファイルの存在確認(前処理)、1行ずつの読込とバリデーション(本処理)、処理結果のサマリ出力(後処理)を具体的なコードで示します。ヘッダー行のスキップ、カラム数不一致の検出、エラー行の記録も盛り込みます。

使いどころ

取引先から受領した CSV ファイルを日次バッチで取り込み、バリデーション結果をログに出力する

月次の売上データ CSV を読み込み、不正行をスキップしつつ正常データだけを後続処理に渡す

社内システムのマスタ一括更新バッチで、更新前に入力ファイルの形式をチェックする

コード例

CsvImportJob の実装
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

public class CsvImportJob {
    private static final Logger LOGGER = Logger.getLogger(CsvImportJob.class.getName());
    private static final int EXPECTED_COLUMNS = 4;
    private static final int MAX_ERRORS = 100;

    private final File inputFile;
    private BufferedReader reader;
    private int totalCount;
    private int successCount;
    private final List<String> errors = new ArrayList<String>();

    public CsvImportJob(File inputFile) { this.inputFile = inputFile; }

    public void initialize() throws Exception {
        if (!inputFile.exists()) throw new IllegalStateException("ファイルが見つかりません: " + inputFile);
        reader = new BufferedReader(new InputStreamReader(new FileInputStream(inputFile), "UTF-8"));
        String header = reader.readLine();
        if (header == null) throw new IllegalStateException("ファイルが空です");
        LOGGER.info("入力ファイル確認完了: " + inputFile.getName());
    }

    public int execute() throws Exception {
        String line;
        int lineNum = 1;
        while ((line = reader.readLine()) != null) {
            lineNum++;
            totalCount++;
            if (line.trim().isEmpty()) continue;
            String[] cols = line.split(",", -1);
            if (cols.length != EXPECTED_COLUMNS) {
                errors.add("行" + lineNum + ": カラム数不一致(" + cols.length + ")");
                if (errors.size() >= MAX_ERRORS) { LOGGER.warning("エラー上限到達"); return 2; }
                continue;
            }
            if (cols[0].trim().isEmpty() || cols[1].trim().isEmpty()) {
                errors.add("行" + lineNum + ": 必須項目が空");
                continue;
            }
            successCount++;
        }
        return errors.isEmpty() ? 0 : 1;
    }

    public void terminate() {
        if (reader != null) { try { reader.close(); } catch (Exception e) { /* ignore */ } }
        LOGGER.info("=== 処理結果 ===");
        LOGGER.info("総行数: " + totalCount + " / 成功: " + successCount + " / エラー: " + errors.size());
        for (String err : errors) { LOGGER.warning(err); }
    }

    public static void main(String[] args) {
        if (args.length < 1) { System.err.println("使い方: java CsvImportJob <CSVパス>"); return; }
        CsvImportJob job = new CsvImportJob(new File(args[0]));
        int exitCode = 2;
        try { job.initialize(); exitCode = job.execute(); } catch (Exception e) {
            LOGGER.log(Level.SEVERE, "エラー", e);
        } finally { job.terminate(); }
        LOGGER.info("終了コード: " + exitCode);
    }
}

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

Version Coverage

Files.lines() で読込がシンプルになる。var でローカル変数の型宣言を省略できる。

Java 17
// Java 17: Files.lines() でストリーム処理
try (var lines = Files.lines(path, StandardCharsets.UTF_8)) {
    lines.skip(1).map(line -> line.split(",", -1))
         .filter(cols -> cols.length == expected)
         .forEach(this::processRecord);
}

Library Comparison

Pure Java(BufferedReader + split)引用符なしの単純な CSV で外部依存を入れられない場合。RFC 4180 準拠が必要な場合は自前実装の工数が増える。
OpenCSV引用符囲み、エスケープ、Bean バインドが必要な場合。外部依存が増える。単純な CSV には過剰。
Apache Commons CSVRFC 4180 準拠の厳密なパースが必要な場合。Commons 系の依存が増える。

注意点

CSV のカラム区切りにカンマを使う場合、値にカンマが含まれるケースを考慮すること

BufferedReader は finally で必ず閉じること。terminate() で閉じる設計にする場合、execute() が例外で中断しても漏れなく解放されることを確認する

バリデーションエラーが大量に出る場合、全行をメモリに溜めるとヒープを圧迫する。エラー上限を設けること

入力ファイルの文字コードが Shift_JIS の場合、InputStreamReader で明示的にエンコーディングを指定する

FAQ

ヘッダー行の有無はどう判定すればよいですか。

設定値で「ヘッダーあり/なし」を指定する方法が安全です。自動判定は誤判定のリスクがあります。

バリデーションエラーが出たら即座に中断すべきですか。

全行チェックしてエラー一覧を返すほうが、修正→再投入のサイクルが1回で済むため効率的な場合が多いです。

CSV の文字コードが不明な場合はどう対処しますか。

BOM の有無を先頭バイトで確認し、なければ UTF-8 を試してフォールバックする方法が実務では多く使われます。

関連書籍

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

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