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+)。無限ブロックを防ぎます
サンプルコード
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) - タイムアウトが正しく機能し、長時間かかるコマンドが強制終了されること