概要
業務システムでは、帳票生成ツールの呼び出し、シェルスクリプト経由のデータ連携、外部コマンドによるファイル変換など、Java から OS のコマンドを実行する場面が少なからず存在します。Runtime.getRuntime() は古くから使われてきましたが、出力の取得やエラーハンドリングに難があり、ProcessBuilder に移行するのが現在の標準です。この記事では、ProcessBuilder の基本的な使い方から、標準出力の取得、標準エラーとのマージ、タイムアウト付き実行、終了コードによる成功・失敗判定までを整理します。Java 17 では record で実行結果を型安全に表現し、Java 21 では sealed interface と switch パターンマッチングで成功・失敗の分岐をさらに明確に書けます。
使いどころ
帳票 PDF 生成ツール(wkhtmltopdf 等)を Java から呼び出し、生成結果のパスと終了コードを取得する
夜間バッチでシェルスクリプトを起動し、外部システムとのファイル連携処理を行い、終了コードで後続処理を分岐する
開発ツールや CI スクリプトから git コマンドや DB マイグレーションコマンドを実行し、結果をログに出力する
コード例
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.stream.Collectors;
public class ExternalProcessDemo {
/** プロセスの実行結果を保持する record */
record ProcessResult(int exitCode, String output) {
public boolean isSuccess() {
return exitCode == 0;
}
}
/** 外部コマンドを実行し、結果を ProcessResult で返す */
public static ProcessResult run(String... command)
throws IOException, InterruptedException {
var pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
var process = pb.start();
String output;
try (var reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
output = reader.lines().collect(Collectors.joining("\n"));
}
int exitCode = process.waitFor();
return new ProcessResult(exitCode, output);
}
public static void main(String[] args) throws Exception {
// Java バージョン確認
System.out.println("=== Java バージョン確認 ===");
var result = run("java", "-version");
System.out.println("終了コード: " + result.exitCode());
System.out.println("出力:\n" + result.output());
if (result.isSuccess()) {
System.out.println("コマンド成功");
} else {
System.out.println("コマンド失敗");
}
// OS に応じた echo コマンド
System.out.println("\n=== echo コマンド ===");
var os = System.getProperty("os.name").toLowerCase();
ProcessResult echoResult;
if (os.contains("win")) {
echoResult = run("cmd", "/c", "echo", "Hello from Java");
} else {
echoResult = run("echo", "Hello from Java");
}
System.out.println("出力: " + echoResult.output());
}
}Version Coverage
record で ProcessResult(exitCode, output) を定義し、実行結果を構造化して返せる。var と try-with-resources で記述量が減る。
// Java 17: record で実行結果を構造化
record ProcessResult(int exitCode, String output) {
boolean isSuccess() { return exitCode == 0; }
}
var pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
var process = pb.start();
String output;
try (var reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
output = reader.lines()
.collect(Collectors.joining("\n"));
}
return new ProcessResult(process.waitFor(), output);Library Comparison
注意点
Process の標準出力を読まずに waitFor() を呼ぶと、バッファが一杯になってプロセスがハングする。必ず出力を読み取ってから waitFor() を呼ぶこと
redirectErrorStream(true) を使わない場合、標準出力と標準エラーを別スレッドで読む必要がある。片方だけ読むともう片方のバッファ詰まりでデッドロックする
タイムアウトなしの waitFor() は永久に待ち続ける可能性がある。Java 9 以降では waitFor(long, TimeUnit) でタイムアウトを設定し、超過時は destroyForcibly() で強制終了すること
command 配列の先頭にシェル(bash, cmd)を指定しないと、パイプやリダイレクトは使えない。OS 間の移植性にも注意が必要
外部コマンドの引数にユーザー入力を含める場合、コマンドインジェクションのリスクがある。引数は文字列連結ではなく配列で個別に渡すこと
FAQ
ProcessBuilder を使います。Runtime の exec は内部で ProcessBuilder を呼んでいるだけで、リダイレクトやディレクトリ指定などの柔軟性がありません。
InputStreamReader の第2引数に文字コードを指定します。Windows では Shift_JIS、Linux では UTF-8 が一般的です。Charset.forName で明示すると安全です。
ProcessBuilder 単体ではパイプは使えません。Java 9 以降の ProcessBuilder.startPipeline() を使うか、シェル経由で実行します。