java-recipes

ホーム その他(Misc) › M-01

M-01: 外部プロセス実行(ProcessBuilder)

Java から OS のコマンドを実行する方法を解説します。 古い Runtime.exec() の罠を避け、ProcessBuilder を使った安全な実装パターンを学びましょう。

なぜ ProcessBuilder を使うのか

Java から外部プログラムを実行するには、古くから Runtime.exec() が使われてきましたが、 この API はスペースを含むコマンドのパース挙動や、標準出力・エラーを読み取らないとプロセスがブロックするといった罠があります。 Java 5 以降に追加された ProcessBuilder を使うと、 より明確でミスが少ない実装が可能です。

ProcessBuilder の主な機能

  • コマンドをリストで指定: スペースを含むパスも安全に扱えます(文字列分割の誤りがない)
  • redirectErrorStream(true): 標準エラーを標準出力にまとめると、出力の読み取りが1本で済みます
  • inheritIO(): 親プロセス(ターミナル)の入出力を継承します。対話型コマンドに便利です
  • waitFor(): プロセスの終了を待ちます。必ず呼ばないと終了コードを取得できません
  • waitFor(long, TimeUnit): タイムアウト付き待機(Java 9+)。無限ブロックを防ぎます

サンプルコード

ExternalProcessSample.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class ExternalProcessSample {

    // ✅ パターン1: 戻り値を使う(推奨)
    public static int runWithOutput(String[] command) throws IOException, InterruptedException {
        ProcessBuilder pb = new ProcessBuilder(command);
        pb.redirectErrorStream(true); // 標準エラーを標準出力にマージ

        Process process = pb.start();
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println("[出力] " + line);
            }
        }

        int exitCode = process.waitFor();
        System.out.println("終了コード: " + exitCode);
        return exitCode;
    }

    // ✅ 標準出力と標準エラーを分けて処理
    public static void runWithSeparateStreams(String[] command) throws IOException, InterruptedException {
        ProcessBuilder pb = new ProcessBuilder(command);
        Process process = pb.start();

        try (BufferedReader stdout = new BufferedReader(
                new InputStreamReader(process.getInputStream()));
             BufferedReader stderr = new BufferedReader(
                new InputStreamReader(process.getErrorStream()))) {

            String line;
            while ((line = stdout.readLine()) != null) {
                System.out.println("[stdout] " + line);
            }
            while ((line = stderr.readLine()) != null) {
                System.out.println("[stderr] " + line);
            }
        }

        process.waitFor();
    }

    public static void main(String[] args) throws Exception {
        System.out.println("=== Java バージョン確認 ===");
        runWithOutput(new String[]{"java", "-version"});

        System.out.println("\n=== echo コマンド ===");
        String os = System.getProperty("os.name").toLowerCase();
        if (os.contains("win")) {
            runWithOutput(new String[]{"cmd", "/c", "echo", "Hello from Process"});
        } else {
            runWithOutput(new String[]{"echo", "Hello from Process"});
        }

        System.out.println("\n=== 終了コードで成功・失敗を判断 ===");
        int exitCode = runWithOutput(new String[]{"java", "-version"});
        if (exitCode == 0) {
            System.out.println("コマンド成功");
        } else {
            System.out.println("コマンド失敗: exitCode=" + exitCode);
        }
    }
}

Java 8 以降で ProcessBuilder を使った外部プロセス実行が可能です。redirectErrorStream(true) で標準エラーを標準出力にまとめると読み取りが簡単になります。必ず waitFor() を呼んで終了を待ちましょう。

よくあるミス・注意点

⚠️ 出力を読み取らないとプロセスがブロックする(バッファフル)

外部プロセスの標準出力と標準エラーはOSのバッファに書き込まれます。 このバッファがいっぱいになると、プロセスは書き込みを待ってブロック(停止)します。 Java 側が出力を読み取らないまま waitFor() を呼ぶとデッドロックが発生します。 必ず start() の後にgetInputStream() を読み取るか、redirectErrorStream(true) と組み合わせて出力を消費してください。

⚠️ waitFor() を呼ばないと終了を待てない

process.start() を呼んだだけでは、 プロセスの終了を待ちません。すぐに Java のコードが次の行に進んでしまいます。 終了コードを取得するには必ず process.waitFor() を呼ぶか、 Java 9 以降なら waitFor(30, TimeUnit.SECONDS) でタイムアウトも設定しましょう。

テストする観点

  • 正常なコマンド(java -version)の終了コードが 0 であること
  • 標準出力が空でなく、期待する文字列を含むこと
  • 存在しないコマンドを実行したときに適切にエラーが処理されること(IOException をキャッチ)
  • exitCode が 0 以外のとき isSuccess()false を返すこと(境界値: exitCode=1)
  • タイムアウトが正しく機能し、長時間かかるコマンドが強制終了されること

GitHub でソースコードを見る →