1. 程式人生 > >Java魔法堂:呼叫外部程式

Java魔法堂:呼叫外部程式

# 前言 Java雖然五臟俱全但總有軟肋,譬如獲取CPU等硬體資訊,當然我們可以通過JNI呼叫C/C++來獲取,但對於對C/C++和Windows API不熟的碼農是一系列複雜的學習和踩坑過程。那能不能通過簡單一些、學習成本低一些的方式呢?答案是肯定的,在功能實現放在首位的情況下,借他山之石是最簡潔有力的做法。 # 認識`java.lang.Runtime#exec`方法 作用:用於呼叫外部程式,並重定向外部程式的標準輸入、標準輸出和標準錯誤到緩衝池。功能就是和windows的“執行”一樣。 ![](https://img2020.cnblogs.com/blog/347002/202102/347002-20210210164902670-1630726869.png) ## 過載方法說明 ``` Runtime#exec(String command); Runtime#exec(String command, String[] envp); Runtime#exec(String command, String[] envp, File workdir); Runtime#exec(String[] cmdArray); Runtime#exec(String[] cmdArray, String[] envp); Runtime#exec(String[] cmdArray, String[] envp, File workdir); ``` 1. `String[] envp` 作為呼叫命令前設定的會話級環境變數。 1.1. 變數作用域:命令執行結束後,通過該引數設定的環境變數將失效; 1.2. 設定方式:`variableName=variableValue`,如`Process proc = r.exec("cmd /c dir > %dest%", new String[]{"dest=c:\\dir.txt"});` 2. `File workdir` 用於設定當前工作目錄,譬如我們需要執行位於D:\tools下的echo.exe程式,那麼可以這樣呼叫`Process proc = r.exec("echo.exec", null, new File("D:\\tools"));` 3. `String command` 即為需要呼叫的外部程式,以及命令列引數等。Windows下呼叫系統命令,像dir等命令是由cmd解析器解釋執行的,因此若直接寫"dir"則會被認為在當前工作目錄下有一個"dir.exe"檔案,那麼當然會執行失敗;在Linux下呼叫ls等是同樣道理,因此請按如下方式呼叫cmd和shell命令: 3.1. 呼叫CMD命令的方式為`Process proc = r.exec(String.format("cmd /c %s", "cmd命令,如dir、type等"))`,若要啟動一個新的Console執行命令,只需要將`dir`改寫為`start dir`即可; 3.2. 呼叫Shell命令的方式為`Process proc = r.exec(String.format("/bin/sh -c %s", "shell命令,如ls、cat等"))`,若要啟動一個新的Terminal執行命令,只需要將`ls`改寫為`xterm -e ls`即可; 4. `String[] cmdArray` 功能和`String command`一樣,但命令列的每個部分將作被獨立分隔出來作為陣列中的元素。如`cmd /c dir`必須分割為`new String[]{"cmd", "/c", "dir"}`,而不能分割為`new String[]{"cmd /c", "dir"}`。 ## 輸入重定向 廢話少說,直接看程式碼! ``` try { String cmd = "cmd /c start cmd.exe"; Process child = Runtime.getRuntime().exec(cmd); OutputStream output = child.getOutputStream(); output.write("cd C:/ /r/n".getBytes()); output.flush(); output.write("dir /r/n".getBytes()); output.close(); } catch (IOException e) {} ``` ## 輸出重定向 注意:不支援直接使用>或>>執行標準輸出重定向。 ``` String cmd = "/path/to/getipconfig.bat"; // 自己寫的bat指令碼檔案,裡面包含ipconfig /all命令。 Process p; try { p = Runtime.getRunTime().exec(cmd); InputStream input = p.getInputStream(); InputStreamReader streamReader = new InputStreamReader(input); BufferedReader bufReader = new BufferedReader(streamReader); String line = null; while ((line = bufReader.readLine()) != null) { System.out.println(line); } } catch (IOException e) {} ``` ## 為什麼不能使用`>`和`>>`實現輸出重定向呢? 通過`Process例項.getInputStream()`和`Process例項.getErrorStream()`獲取的輸入流和錯誤資訊流是緩衝池是當前Java程式提供的,而不是直接獲取外部程式的標準輸出流和標準錯誤流。 即通過`Runtime#exec`呼叫外部程式時,外部程式的標準輸出流和標準錯誤流已經被Java程式接管。那麼在命令中企圖通過`>`和`>>`實現輸出重定向顯然已無效果。 ![](https://img2020.cnblogs.com/blog/347002/202102/347002-20210210172220504-1922368692.png) 另外,緩衝池的容量是一定的,因此若外部程式在執行過程中不斷向緩衝池輸出內容,當緩衝池填滿,那麼外部程式將暫停執行直到緩衝池有空位可接收外部程式的輸出內容為止。(採用xcopy命令複製大量檔案時將會出現該問題) 解決辦法就是當前的Java程式不斷讀取緩衝池的內容,從而為騰出緩衝池的空間。如: ``` Runtime r = Runtime.getRuntime(); try{ Process proc = r.exec("cmd /c dir"); // 假設該操作為造成大量內容輸出 // 採用字元流讀取緩衝池內容,騰出空間 BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream(), "gbk"))); String line = null; while ((line = reader.readLine()) != null){ System.out.println(line); } // 或採用位元組流讀取緩衝池內容,騰出空間 // ByteArrayOutputStream pool = new ByteArrayOutputStream(); // byte[] buffer = new byte[1024]; // int count = -1; // while ((count = proc.getInputStream().read(buffer)) != -1){ // pool.write(buffer, 0, count); // buffer = new byte[1024]; // } // System.out.println(pool.toString("gbk")); int exitVal = proc.waitFor(); System.out.println(exitVal == 0 ? "成功" : "失敗"); } catch(Exception e){ e.printStackTrace(); } ``` *注意:外部程式在執行結束後將會自動關閉,否則不管是字元流還是位元組流均由於既讀不到資料,又讀不到流結束符而出現阻塞Java程序執行的情況。* # 簡化輸入輸出重定向的`java.lang.ProcessBuilder` 功能和`java.lang.runtime#exec一樣`,只是`java.lang.ProcessBuilder`僅接收命令列以陣列形式傳遞給`java.lang.ProcessBuilder#command()`而已。 ## 基本使用 ``` ProcessBuilder pb = new ProcessBuilder(); // pb.command("cmd /c dir"); 這樣會報錯 // 或者寫到new ProcessBuilder("cmd", "/c", "dir") pb.command("cmd", "/c", "dir"); try { Process process = pb.start(); // 後面都是操作Process例項的方法 } catch (IOException e){} ``` ## 重定向 ``` public ProcessBuilder redirectInput(File file) // 輸入重定向 public ProcessBuilder redirectOutput(File file) // 輸出重定向 public ProcessBuilder redirectError(File file) // 異常重定向 ``` 示例 ``` try { ProcessBuilder pb = new ProcessBuilder("cmd"); // 在標準輸入中,通過換行符分隔多行命令。 // commands.txt的內容為 // javac Demo.java // java Demo File commands = new File("/path/to/commands.txt"); File error = new File("/path/to/error"); File output = new File("/path/to/output"); pd.redirectInput(commands); pd.redirectOutput(output); pd.redirectError(error); pd.start(); } catch(Exception e) { e.printStackTrace(); } ``` # `java.lang.Process`API說明 ``` // 以非阻塞方式獲取子程序執行的返回值(習慣0表示正常結束)。若子程序尚未完成時呼叫該方法,則會報異常`java.lang.IllegalThreadStateException` int exitValue() // 以阻塞方式獲取子程序執行的返回值。若程序尚未完成則會等待子程序完成後才恢復當前執行緒。 // 問題:若子程序無法正常關閉,則會導致Java執行緒一直掛起; // 返回值為子程序的退出碼 int waitFor()。 // 如果超時前子程序結束,那麼返回`true` ,否則返回`false` boolean waitFor(long timeout, TimeUnit unit) // 強行終止子程序,但呼叫後子程序不會馬上被終止,所以立即調`boolean isAlive()`方法可能會返回`true`,因此需配合`waitFor`使用。 void destory() // 預設實現為呼叫`void destory()`方法。從JDK1.8開始提供。 Process destoryForcibly() // 如果子程序還沒結束則返回`true` 。從JDK1.8開始提供。 boolean isAlive() // 獲取子程序的異常輸出流,如果子程序以`ProcessBuilder`建立,且通過`ProcessBuilder.redirectError`設定重定向,那麼該方法返回`null` InputStream getErrorStream() // 獲取子程序的標準輸出流,如果子程序以`ProcessBuilder`建立,且通過`ProcessBuilder.redirectOutput`設定重定向,那麼該方法返回`null` InputStream getInputStream() // 獲取子程序的標準輸入流,如果子程序以`ProcessBuilder`建立,且通過`ProcessBuilder.redirectInput`設定重定向,那麼該方法返回`null` OutputStream getOutputStream() ``` # 總結 尊重原創,轉載請註明來自:https://www.cnblogs.com/fsjohnhuang/p/14396218.html ^_^