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

Java 執行系統命令完全指南:Runtime.exec() 與 ProcessBuilder 實戰

     編輯:Java 程式設計入門

核心 API 對比

特性Runtime.exec()ProcessBuilder (推薦)
API 風格靜態方法,簡單直接建造者模式,靈活配置
參數處理單字符串,需手動分割List,自動處理引號/空格
環境變量繼承父進程,難以定制environment() 精確控制
工作目錄固定為 JVM 啟動目錄directory(File) 任意指定
輸出重定向需手動讀取流redirectOutput() 直接導向文件
管道/鏈式命令極其困難startPipeline() 支持

快速上手:啟動計算器


// 最簡單寫法 (Windows)
Runtime.getRuntime().exec("calc");

// ProcessBuilder 等價寫法
new ProcessBuilder("calc").start();

ProcessBuilder 進階實戰

1. 跨平台命令執行


public static Process executeCommand(String... command) throws IOException {
    return new ProcessBuilder(command)
        .inheritIO()  // 直接繼承父進程標準輸入/輸出/錯誤流
        .start();
}

// 使用示例
// Windows: executeCommand("cmd", "/c", "dir");
// Linux/Mac: executeCommand("ls", "-la");

2. 獲取命令輸出 (核心難點)


public static String captureOutput(String... command) throws IOException, InterruptedException {
    Process process = new ProcessBuilder(command)
        .redirectErrorStream(true)  // 合併 stderr 到 stdout
        .start();
    
    try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
        
        StringBuilder output = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            output.append(line).append(System.lineSeparator());
        }
        
        int exitCode = process.waitFor();  // 等待進程結束
        if (exitCode != 0) {
            throw new RuntimeException("命令執行失敗,退出碼: " + exitCode + "\n輸出: " + output);
        }
        return output.toString();
    }
}

// 使用
String javaVersion = captureOutput("java", "-version");
System.out.println(javaVersion);

3. 處理含空格/特殊字符的路徑


// 錯誤:Runtime.exec("C:\\Program Files\\app.exe") 會被拆分為多個參數
// 正確:ProcessBuilder 自動處理
new ProcessBuilder("C:\\Program Files\\MyApp\\app.exe", "--arg=value").start();

// 或手動構建列表
List cmd = Arrays.asList(
    "C:\\Program Files\\MyApp\\app.exe",
    "--input", "file with spaces.txt",
    "--output", "result.txt"
);
new ProcessBuilder(cmd).start();

4. 設置環境變量與工作目錄


ProcessBuilder pb = new ProcessBuilder("mvn", "clean", "package");
pb.environment().put("JAVA_HOME", "C:\\Java\\jdk-17");
pb.environment().remove("HTTP_PROXY");  // 移除代理
pb.directory(new File("D:\\projects\\my-app"));  // 指定工作目錄
pb.redirectOutput(ProcessBuilder.Redirect.to(new File("build.log")));  // 輸出重定向到文件
pb.redirectError(ProcessBuilder.Redirect.INHERIT);  // 錯誤輸出繼承控制台
Process process = pb.start();

5. 異步非阻塞執行 (Java 9+)


Process process = new ProcessBuilder("long-running-task").start();

// 異步等待完成
CompletableFuture future = process.onExit();
future.thenAccept(p -> {
    System.out.println("進程結束,退出碼: " + p.exitValue());
});

// 或輪詢
while (process.isAlive()) {
    Thread.sleep(1000);
    System.out.println("運行中...");
}

經典坑點與解決方案

❌ 坑 1:緩衝區填滿導致死鎖


// 錯誤:只讀 stdout,stderr 緩衝區滿則進程阻塞
Process p = Runtime.getRuntime().exec("cmd /c dir /s");
BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()));
// 如果 stderr 有大量輸出,進程會卡死在這裡

// 正確:合併流 或 多線程同時讀取 stdout/stderr
ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "dir", "/s");
pb.redirectErrorStream(true);  // 方案一:合併

// 方案二:多線程讀取 (通用)
ExecutorService es = Executors.newFixedThreadPool(2);
es.submit(() -> consumeStream(p.getInputStream(), System.out::print));
es.submit(() -> consumeStream(p.getErrorStream(), System.err::print));
p.waitFor();
es.shutdown();

❌ 坑 2:Windows 命令解析陷阱


// 內部命令 (dir, copy, del) 需 cmd /c
new ProcessBuilder("cmd", "/c", "dir").start();

// 外部程序可直接調用
new ProcessBuilder("notepad.exe").start();

// 路徑含空格時
new ProcessBuilder("cmd", "/c", "\"C:\\Program Files\\app.exe\"").start();

❌ 坑 3:編碼亂碼


// Windows 默認 GBK,Linux 默認 UTF-8
// 明確指定編碼
String os = System.getProperty("os.name").toLowerCase();
Charset charset = os.contains("win") ? Charset.forName("GBK") : StandardCharsets.UTF_8;

new InputStreamReader(process.getInputStream(), charset)

❌ 坑 4:僵尸進程洩漏


// 必須調用 waitFor() 或 onExit() 回收進程資源
// 長期運行的服務中,定期清理已結束但未回收的 Process 對象

實戰案例:手機遠程關機小工具


// 服務端 (電腦端)
@SpringBootApplication
@RestController
public class RemoteShutdownServer {
    @PostMapping("/shutdown")
    public String shutdown(@RequestParam String token) {
        if (!"secret123".equals(token)) return "Unauthorized";
        try {
            // Windows 關機命令
            new ProcessBuilder("shutdown", "/s", "/t", "60", "/c", "遠程定時關機").start();
            return "已發送關機指令,60秒後執行";
        } catch (Exception e) {
            return "失敗: " + e.getMessage();
        }
    }
    public static void main(String[] args) {
        SpringApplication.run(RemoteShutdownServer.class, args);
    }
}

// 客戶端 (手機端發送 HTTP POST 即可)

安全性警示 ⚠️

  • 命令注入風險:絕不可將不可信輸入直接拼接進命令字符串
  • 最小權限原則:服務進程勿使用 root/Administrator 運行
  • 白名單機制:僅允許預定義命令集合
  • 審計日誌:記錄所有命令執行請求與結果

總結

ProcessBuilder 是現代 Java 執行系統命令的標準方式,提供了豐富的配置選項解決了 Runtime.exec() 的諸多痛點。掌握流處理、編碼處理、異步等待、安全防範等核心技能,能構建健壯的系統集成功能。記住:能用 Java API 解決的,絕不調用外部命令——跨平台性、安全性、可維護性均優於命令調用。

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