Java 應用常需啟動外部進程:啟動系統工具 (記事本、計算機)、調用命令列工具 (ffmpeg、pandoc)、啟動子系統、執行腳本。本文系統講解 Runtime.exec() 與現代 ProcessBuilder 兩種 API 的用法差異、參數處理、輸入輸出流管理、常見坑點與最佳實踐。
| 特性 | Runtime.exec() | ProcessBuilder (Java 5+) |
|---|---|---|
| 參數傳遞 | 單一字串 (依賴平台解析) 或 字串陣列 | 強制字串陣列 / List<String>,無歧義 |
| 工作目錄 | 無法指定 (繼承 JVM 進程) | directory(File) 顯式設置 |
| 環境變量 | 繼承且難以修改 | environment() 返回 Map 可增刪改 |
| 輸入輸出重定向 | 手動處理 Stream | redirectInput/Output/Error 支援文件/管道/繼承 |
| 啟動管道鏈 | 極其困難 | startPipeline(List<ProcessBuilder>) (Java 9+) |
| 推薦度 | 維護舊代碼 | 新代碼首選 |
// 啟動記事本
Runtime rt = Runtime.getRuntime();
Process p = rt.exec("notepad.exe"); // Windows
// Process p = rt.exec("gedit"); // Linux
// 帶參數:路徑含空格需引號,但 Runtime.exec 解析不可靠
// p = rt.exec("\"C:\\Program Files\\MyApp\\app.exe\" -arg1"); // 危險!
// 啟動記事本編輯指定文件
ProcessBuilder pb = new ProcessBuilder("notepad.exe", "D:\\readme.txt");
pb.start();
// 自定義路徑含空格:陣列元素自動處理,無需手動引號
ProcessBuilder pb2 = new ProcessBuilder(
"C:\\Program Files\\MyApp\\app.exe", "-arg1", "--option=value");
pb2.directory(new File("C:\\WorkDir")); // 指定工作目錄
Map env = pb2.environment();
env.put("MY_VAR", "custom_value"); // 設置環境變量
Process p = pb2.start();
必須消費子進程的 stdout/stderr,否則緩衝區滿導致進程掛起死鎖!
ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/c", "dir C:\\Windows");
pb.redirectErrorStream(true); // 合併 stderr 到 stdout,簡化處理
Process process = pb.start();
// 異步讀取輸出 (避免主線程阻塞)
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), "GBK"))) { // Windows 中文編碼
String line;
while ((line = reader.readLine()) != null) {
System.out.println("[子進程] " + line);
}
} catch (IOException e) { e.printStackTrace(); }
}).start();
// 等待結束並獲取退出碼
int exitCode = process.waitFor(); // 阻塞直到結束
System.out.println("進程退出碼: " + exitCode);
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
public class CommandExecutor {
public static class Result {
public final int exitCode;
public final String output;
public final String error;
public Result(int exitCode, String output, String error) {
this.exitCode = exitCode; this.output = output; this.error = error;
}
}
public static Result execute(List command, File workDir, Map env, long timeoutMs)
throws IOException, InterruptedException, TimeoutException {
ProcessBuilder pb = new ProcessBuilder(command);
if (workDir != null) pb.directory(workDir);
if (env != null) pb.environment().putAll(env);
pb.redirectErrorStream(false); // 分離 stdout/stderr
Process process = pb.start();
// 並發讀取雙流
ExecutorService executor = Executors.newFixedThreadPool(2);
Future outFuture = executor.submit(() -> readStream(process.getInputStream()));
Future errFuture = executor.submit(() -> readStream(process.getErrorStream()));
boolean finished = process.waitFor(timeoutMs, TimeUnit.MILLISECONDS);
if (!finished) {
process.destroyForcibly();
throw new TimeoutException("命令執行超時: " + command);
}
executor.shutdown();
return new Result(process.exitValue(), outFuture.get(), errFuture.get());
}
private static String readStream(InputStream is) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) sb.append(line).append('\n');
return sb.toString();
}
}
// 使用範例
public static void main(String[] args) throws Exception {
Result r = execute(Arrays.asList("java", "-version"), null, null, 5000);
System.out.println("Exit: " + r.exitCode);
System.out.println("Out:\n" + r.output);
System.out.println("Err:\n" + r.error);
}
}
| 坑點 | 現象 | 解決 |
|---|---|---|
| 空格路徑解析失敗 | Runtime.exec("C:\Program Files\App\app.exe") 當成多參數 | 用 ProcessBuilder 陣列 / List,每個參數獨立元素 |
| 中文亂碼 | Windows 預設 GBK,Linux UTF-8 | 明確指定 InputStreamReader(stream, Charset.forName("GBK")) 或 StandardCharsets.UTF_8 |
| 進程掛起不返回 | 未讀取 stdout/stderr,緩衝區滿 (通常 4KB-64KB) | 必須 並發讀取雙流,或 redirectErrorStream(true) 合併 |
| 僵屍進程 | Java 進程結束,子進程仍在運行 | JVM shutdown hook 中 process.destroy(),或守護線程監控 |
| 權限不足 | Linux Permission denied | 確保可執行位 chmod +x、路徑正確、用戶權限 |
| 環境變量丟失 | 子進程找不到 PATH 中的命令 | pb.inheritIO() 繼承 IO、或顯式設置 env.put("PATH", ...) |
// Windows: 使用 cmd /c start "" "path" 啟動獨立視窗
ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/c", "start", "", "\"C:\\Program Files\\App\\app.exe\"");
pb.start(); // 立即返回,不等待 app 結束
Java 執行外部程式的黃金法則:ProcessBuilder + 陣列參數 + 並發讀取雙流 + 顯式編碼 + 超時控制 + 資源清理。封裝為工具類後,業務代碼簡潔為 CommandExecutor.execute(cmdList, workDir, env, timeout)。避開 Runtime.exec() 的平台解析歧義、流死鎖、環境變量繼承混亂,構建穩健的系統集成層。