Java 應用常需調用外部命令(殺毒掃描、圖片處理、視頻轉碼、系統管理指令)並捕獲輸出。本文對比 Runtime.exec 與現代 ProcessBuilder,提供生產級代碼範本,涵蓋輸出捕獲、錯誤流處理、超時控制、編碼處理等關鍵細節。
| 特性 | Runtime.exec | ProcessBuilder |
|---|---|---|
| API 風格 | 舊式、字串命令 | 流式建構器、List<String> 參數 |
| 參數處理 | 空格分割,易出錯 | 每個參數獨立元素,安全可靠 |
| 環境變數 | 繼承父進程,難以定製 | environment() 精確控制 |
| 工作目錄 | 不可變 | directory(File) 指定 |
| 重定向 | 手動處理流 | redirectOutput/Error 直達檔案 |
| 推薦度 | 維舊代碼 | 新代碼首選 |
public class ExternalProcessExecutor {
public static ExecutionResult execute(List<String> command,
Map<String, String> env,
File workDir,
long timeoutMs,
Charset outputCharset) throws IOException, InterruptedException, TimeoutException {
ProcessBuilder pb = new ProcessBuilder(command);
if (env != null) pb.environment().putAll(env);
if (workDir != null) pb.directory(workDir);
// 合併錯誤流到標準輸出,避免雙流閱讀死鎖
pb.redirectErrorStream(true);
Process process = pb.start();
// 異步讀取輸出,防止緩衝區填滿阻塞進程
StringBuilder output = new StringBuilder();
Thread readerThread = new Thread(() -> {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(process.getInputStream(), outputCharset))) {
String line;
while ((line = br.readLine()) != null) {
output.append(line).append('\n');
}
} catch (IOException ignored) {}
});
readerThread.start();
boolean finished = process.waitFor(timeoutMs, TimeUnit.MILLISECONDS);
readerThread.join(timeoutMs); // 等待讀取完成
if (!finished) {
process.destroyForcibly();
throw new TimeoutException("Process timeout after " + timeoutMs + "ms: " + command);
}
return new ExecutionResult(process.exitValue(), output.toString());
}
public record ExecutionResult(int exitCode, String output) {}
}
進程同時產生 stdout/stderr,若 Java 端循序讀取,一個流緩衝區滿會阻塞進程,導致另一流也無法讀取 → 死鎖。
解法:pb.redirectErrorStream(true) 合併流,或雙線程並發讀取兩個流。
外部程式輸出編碼取決於其自身、系統區域設置、終端模擬器。必須顯式指定 Charset(通常為 StandardCharsets.UTF_8 或系統預設 Charset.defaultCharset())。
絕不使用字串拼接命令:exec("cmd " + userInput)。必須使用 List<String> 參數列表,每個參數獨立元素。
process.waitFor(timeout, unit) + process.destroyForcibly()// 正確:參數獨立,自動處理空格路徑
List<String> cmd = List.of("C:\\Program Files\\Tool\\tool.exe", "-i", "input file.txt");
// 錯誤:字串命令需手動處理引號轉義
String bad = "\"C:\\Program Files\\Tool\\tool.exe\" -i \"input file.txt\"";
public class VirusScanner {
private static final String CLAMSCAN = "clamscan";
public ScanResult scan(File file) throws IOException, InterruptedException, TimeoutException {
List<String> cmd = List.of(CLAMSCAN, "--infected", "--no-summary", file.getAbsolutePath());
ExecutionResult result = ExternalProcessExecutor.execute(cmd, null, null, 30_000, StandardCharsets.UTF_8);
// clamscan: exit 0=乾淨, 1=發現病毒, 2=錯誤
boolean infected = result.exitCode() == 1;
String details = result.output().trim();
return new ScanResult(infected, details);
}
}
調用外部程式看似簡單,實則暗藏死鎖、編碼、超時、安全、資源洩漏等坑。統一使用 ProcessBuilder、合併錯誤流、顯式編碼、超時強制銷毀、參數列表化,是生產環境的鐵律。