首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java 執行外部程式完全指南:Runtime.exec 與 ProcessBuilder 實戰

     編輯:Java 程式設計入門

Java 應用常需啟動外部進程:啟動系統工具 (記事本、計算機)、調用命令列工具 (ffmpeg、pandoc)、啟動子系統、執行腳本。本文系統講解 Runtime.exec() 與現代 ProcessBuilder 兩種 API 的用法差異、參數處理、輸入輸出流管理、常見坑點與最佳實踐。

兩種 API 對比

特性Runtime.exec()ProcessBuilder (Java 5+)
參數傳遞單一字串 (依賴平台解析) 或 字串陣列強制字串陣列 / List<String>,無歧義
工作目錄無法指定 (繼承 JVM 進程)directory(File) 顯式設置
環境變量繼承且難以修改environment() 返回 Map 可增刪改
輸入輸出重定向手動處理 StreamredirectInput/Output/Error 支援文件/管道/繼承
啟動管道鏈極其困難startPipeline(List<ProcessBuilder>) (Java 9+)
推薦度維護舊代碼新代碼首選

基礎範例:啟動系統程式

Runtime.exec() (舊式)

// 啟動記事本
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 (推薦)

// 啟動記事本編輯指定文件
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 特有:啟動 GUI 程式不阻塞主控台

// 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() 的平台解析歧義、流死鎖、環境變量繼承混亂,構建穩健的系統集成層。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved