【轉】Java中Runtime.exec的一些事 

  • 2019 年 10 月 4 日
  • 筆記

0 預備知識

Runtime類是一個與JVM運行時環境有關的Singleton類,有以下幾個值得注意的地方:

0.1 Runtime.getRuntime()可以取得當前JVM的運行時環境,這也是在Java中唯一得到運行時環境的方法。

0.2 Runtime上其他大部分的方法都是實例方法,也就是說每次進行運行時調用時都要用到getRuntime方法。

0.3 Runtime中的exit方法是退出當前JVM的方法,估計也是唯一的。System類中的exit實際上也是通過調用Runtime.exit()來退出JVM的。

Java對Runtime返回值的一般規則,0代表正常退出,非0代表異常中止,這只是Java的規則,在各個作業系統中總會發生一些小的混淆。

0.4 Runtime.addShutdownHook()方法可以註冊一個hook在JVM執行shutdown的過程中,方法的參數只要是一個初始化過但是沒有執行的Thread實例就可以。(注意,Java中的Thread都是執行過了就不值錢的哦)

0.5說到addShutdownHook這個方法就要說一下JVM運行環境是在什麼情況下shutdown或者abort的。

Shutdown:當最後一個非精靈進程退出或者收到了一個用戶中斷訊號、用戶登出、系統shutdown、Runtime的exit方法被調用時JVM會啟動shutdown的過程,在這個過程開始後,他會並行啟動所有登記的shutdown hook(注意是並行啟動,這就需要執行緒安全和防止死鎖)。當shutdown過程啟動後,只有通過調用halt方法才能中止shutdown的過程並退出JVM。

Abort: abort退出時JVM就是停止運行但並不一定進行shutdown。這隻有JVM在遇到SIGKILL訊號或者windows中止進程的訊號、本地方法發生類似於訪問非法地址一類的內部錯誤時會出現。這種情況下並不能保證shutdown hook是否被執行。

0.6 Runtime.exec()方法的所有重載。這裡要注意的是:

public Process exec(String[] cmdarray, String[] envp, File dir);

這個方法中cmdArray是一個執行的命令和參數的字元串數組,數組的第一個元素是要執行的命令往後依次都是命令的參數,envp中是name=value形式的環境變數設置,如果子進程要繼承當前進程的環境時是null。

1 不正確的調用exitValue

Java程式碼

public class BadExecJavac {   public static void main(String args[]) {   try {              Runtime rt = Runtime.getRuntime();              Process proc = rt.exec("java");   int exitVal = proc.exitValue();              System.out.println("Process exitValue: " + exitVal);          } catch (Throwable t) {              t.printStackTrace();          }      }  }  

輸出

java.lang.IllegalThreadStateException: process has not exited      at java.lang.ProcessImpl.exitValue(Native Method)      at BadExecJavac.main(BadExecJavac.java:26)  

錯誤分析:

主要問題就是錯誤的調用了exitValue來取得外部命令的返回值。因為exitValue方法是非阻塞的,在調用這個方法時外部命令並沒有返回所以引起異常。阻塞形式的方法是waitFor,它會一直等待外部命令執行完畢,然後返回執行的結果。

當你在一個Process上調用waitFor方法時,當前執行緒是阻塞的,如果外部命令無法執行結束,那麼你的執行緒就會一直阻塞下去,這種意外會影響我們程式的執行。所以在我們不能判斷外部命令什麼時候執行完畢而我們的程式還需要繼續執行的情況下,我們就應該循環的使用exitValue來取得外部命令的返回狀態,並在外部命令返回時作出相應的處理。

2不正確的調用waitFor

Java程式碼

public class BadExecJavac2 {   public static void main(String args[]) {   try {              Runtime rt = Runtime.getRuntime();              Process proc = rt.exec("javac");   int exitVal = proc.waitFor();              System.out.println("Process exitValue: " + exitVal);          } catch (Throwable t) {             t.printStackTrace();          }      }  }  

不幸的是,這個程式也無法執行完成,它沒有輸出但卻一直懸在那裡!這是為什麼那?

JDK文檔中的解釋:因為本地的系統對標準輸入和輸出所提供的緩衝池有效,所以錯誤的對標準輸出快速的寫入和從標準輸入快速的讀入都有可能造成子進程的鎖,甚至死鎖。

JDK僅僅說明為什麼問題會發生,卻並沒有說明這個問題怎麼解決。

解決方法就是:執行完外部命令後我們要控制好Process的所有輸入和輸出(視情況而定),//在這個例子裡邊因為調用的是Javac,而他在沒有參數的情況下會將提示資訊輸出到標準出錯,所以在下面的程式中我們要對此進行處理。

3 一種可接受的調用方式

Java程式碼

public class MediocreExecJavac {   public static void main(String args[]) {   try {              Runtime rt = Runtime.getRuntime();              Process proc = rt.exec("javac");              InputStream stderr = proc.getErrorStream();              InputStreamReader isr = new InputStreamReader(stderr);              BufferedReader br = new BufferedReader(isr);              String line = null;              System.out.println("<error></error>");   while ((line = br.readLine()) != null)                  System.out.println(line);              System.out.println("");   int exitVal = proc.waitFor();              System.out.println("Process exitValue: " + exitVal);          } catch (Throwable t) {              t.printStackTrace();          }      }  }  

輸出:

<error></error>  Usage: javac <options></options> <source files=""></source>    ...    Process exitValue: 2 

結果分析:

出來了結果。

為了處理好外部命令大量輸出的情況,你要確保你的程式處理好外部命令所需要的輸入或者輸出。

4 調用認為是可執行程式的時候容易發生的錯誤

Java程式碼

public class BadExecWinDir {   public static void main(String args[]) {   try {              Runtime rt = Runtime.getRuntime();              Process proc = rt.exec("dir");              InputStream stdin = proc.getInputStream();              InputStreamReader isr = new InputStreamReader(stdin);              BufferedReader br = new BufferedReader(isr);              String line = null;              System.out.println("<output></output>");   while ((line = br.readLine()) != null)                  System.out.println(line);              System.out.println("");   int exitVal = proc.waitFor();              System.out.println("Process exitValue: " + exitVal);          } catch (Throwable t) {              t.printStackTrace();          }      }  }  

輸出:

java.io.IOException: Cannot run program "dir": CreateProcess error=2, ...      at java.lang.ProcessBuilder.start(ProcessBuilder.java:460)      at java.lang.Runtime.exec(Runtime.java:593)      at java.lang.Runtime.exec(Runtime.java:431)      at java.lang.Runtime.exec(Runtime.java:328)      at BadExecWinDir.main(BadExecWinDir.java:29)  

原因分析:

因為dir命令是由windows中的解釋器解釋的,直接執行dir時無法找到dir.exe這個命令,所以會出現文件未找到這個2的錯誤。如果我們要執行這樣的命令,就要先根據作業系統的不同執行不同的解釋程式command.com 或者cmd.exe。

5 window執行的良好示例

Java程式碼

public class StreamGobbler extends Thread {      InputStream is;      String      type;        StreamGobbler(InputStream is, String type) {   this.is = is;   this.type = type;      }     public void run() {   try {              InputStreamReader isr = new InputStreamReader(is);              BufferedReader br = new BufferedReader(isr);              String line = null;   while ((line = br.readLine()) != null)                  System.out.println(type + ">" + line);          } catch (IOException ioe) {              ioe.printStackTrace();          }      }  }  

Java程式碼

public class GoodWindowsExec {   public static void main(String args[]) {   if (args.length < 1) {              System.out.println("USAGE: java GoodWindowsExec <cmd></cmd>");              System.exit(1);          }   try {              String osName = System.getProperty("os.name");              String[] cmd = new String[3];   if (osName.equals("Windows NT")) {                  cmd[0] = "cmd.exe";                  cmd[1] = "/C";                  cmd[2] = args[0];              } else if (osName.equals("Windows 95")) {                  cmd[0] = "command.com";                  cmd[1] = "/C";                  cmd[2] = args[0];              }              Runtime rt = Runtime.getRuntime();              System.out.println("Execing " + cmd[0] + " " + cmd[1] + " " + cmd[2]);              Process proc = rt.exec(cmd);   // any error message?              StreamGobbler errorGobbler = new StreamGobbler(proc.getErrorStream(), "ERROR");   // any output?              StreamGobbler outputGobbler = new StreamGobbler(proc.getInputStream(), "OUTPUT");   // kick them off              errorGobbler.start();              outputGobbler.start();   // any error???   int exitVal = proc.waitFor();              System.out.println("ExitValue: " + exitVal);          } catch (Throwable t) {              t.printStackTrace();          }      }  }  

輸出:

…  ExitValue: 0

原因分析:

就是cmd.exe /C +一個windows中註冊了後綴的文檔名,windows會自動地調用相關的程式來打開這個文檔。

不要假設你執行的程式是可執行的程式,要清楚自己的程式是單獨可執行的還是被解釋的, 這裡還有一點,就是得到process的輸出的方式是getInputStream,這是因為我們要從Java 程式的角度來看,外部程式的輸出對於Java來說就是輸入,反之亦然。

6 不良好的重定向命令輸出

錯誤的認為exec方法會接受所有你在命令行或者Shell中輸入並接受的字元串。這些錯誤主要出現在命令作為參數的情況下,程式設計師錯誤的將所有命令行中可以輸入的參數命令加入到exec中。下面的例子中就是一個程式設計師想重定向一個命令的輸出。

Java程式碼

public class BadWinRedirect {   public static void main(String args[]) {   try {              Runtime rt = Runtime.getRuntime();              Process proc = rt.exec("java jecho 'Hello World' > test.txt");   // any error message?              StreamGobbler errorGobbler = new StreamGobbler(proc.getErrorStream(), "ERROR");   // any output?              StreamGobbler outputGobbler = new StreamGobbler(proc.getInputStream(), "OUTPUT");   // kick them off              errorGobbler.start();              outputGobbler.start();   // any error???   int exitVal = proc.waitFor();              System.out.println("ExitValue: " + exitVal);          } catch (Throwable t) {              t.printStackTrace();          }      }  }  

程式設計師的本意是將Hello World這個輸入重訂向到一個文本文件中,但是這個文件並沒有生成,jecho僅僅是將命令行中的參數輸出到標準輸出中,用戶覺得可以像dos中重定向一樣將輸出重定向到一個文件中,但這並不能實現,用戶錯誤的將exec認為是一個shell解釋器,但它並不是,如果你想將一個程式的輸出重定向到其他的程式中,你必須用程式來實現他。可用java.io中的包。

7 良好的重定向輸出示例

Java程式碼

public class StreamGobbler extends Thread {      InputStream is;      String      type;      OutputStream os;      StreamGobbler(InputStream is, String type) {   this(is, type, null);      }      StreamGobbler(InputStream is, String type, OutputStream redirect) {   this.is = is;   this.type = type;   this.os = redirect;      }   public void run() {   try {              PrintWriter pw = null;   if (os != null)                  pw = new PrintWriter(os);              InputStreamReader isr = new InputStreamReader(is);              BufferedReader br = new BufferedReader(isr);              String line = null;   while ((line = br.readLine()) != null) {   if (pw != null)                      pw.println(line);                  System.out.println(type + ">" + line);              }   if (pw != null)                  pw.flush();          } catch (IOException ioe) {              ioe.printStackTrace();          }      }  }  

Java程式碼

public class GoodWinRedirect {   public static void main(String args[]) {          args = new String[1];          args[0]="g:\out.txt";   if (args.length < 1) {              System.out.println("USAGE java GoodWinRedirect <outputfile></outputfile>");              System.exit(1);          }   try {              FileOutputStream fos = new FileOutputStream(args[0]);              Runtime rt = Runtime.getRuntime();              Process proc = rt.exec("java jecho 'Hello World'");   // any error message?              StreamGobbler errorGobbler = new StreamGobbler(proc.getErrorStream(), "ERROR");   // any output?              StreamGobbler outputGobbler = new StreamGobbler(proc.getInputStream(), "OUTPUT", fos);   // kick them off              errorGobbler.start();              outputGobbler.start();   // any error???   int exitVal = proc.waitFor();              System.out.println("ExitValue: " + exitVal);              fos.flush();              fos.close();          } catch (Throwable t) {              t.printStackTrace();          }      }  }  

8 總結

總結了幾條規則,防止我們在進行Runtime.exec()調用時出現錯誤。

  • 在一個外部進程執行完之前你不能得到他的退出狀態
  • 在你的外部程式開始執行的時候你必須馬上控制輸入、輸出、出錯這些流。
  • 你必須用Runtime.exec()去執行程式
  • 你不能象命令行一樣使用Runtime.exec()。

9 問答

問:為什麼Runtime.exec("ls")沒有任何輸出?

答:調用Runtime.exec方法將產生一個本地的進程,並返回一個Process子類的實例,該實例可用於控制進程或取得進程的相關資訊。

由於調用Runtime.exec方法所創建的子進程沒有自己的終端或控制台,因此該子進程的標準IO(如stdin,stdou,stderr)都通過Process.getOutputStream(),Process.getInputStream(),Process.getErrorStream()方法重定向給它的父進程了。用戶需要用這些stream來向子進程輸入數據或獲取子進程的輸出。所以正確執行Runtime.exec("ls")的常式如下:

Java程式碼

try {      Process process = Runtime.getRuntime().exec(command);      InputStreamReader ir = new InputStreamReader(process.getInputStream());      LineNumberReader input = new LineNumberReader(ir);      String line;   while ((line = input.readLine()) != null)          System.out.println(line);  } catch (java.io.IOException e) {      System.err.println("IOException " + e.getMessage());

原文鏈接如下:http://jiangshuiy.iteye.com/blog/1674235