1. 程式人生 > >Java呼叫外部程式命令時執行緒阻塞問題分析

Java呼叫外部程式命令時執行緒阻塞問題分析

    今天要寫個遠端重啟服務的功能,為了開發速度,暫時定為Java程式碼+WMIC命令的方法,簡單的說,就是利用Java呼叫本機應用程式的方法。涉及到的 Java類有java.lang包裡面的Runtime、Process、ProcessBuilder三個類,以及wmic中重啟服務的命令。因為之前 也寫過這方面的東西,所以很習慣性的寫出了程式碼:

Process p = Runtime.getRuntime().exec("wmic ...");
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String tmp = null;
while ((tmp = br.readline()) != null) {
  System.out.println(tmp);
}
int exitValue = p.waitfor();
 

  執行,結果發現程式不能退出,Debug發現程式阻塞在br.readline()中了,強制結束程式,發現重啟服務的命令正常下下去了,去掉程式中獲得標準輸出的地方和獲得返回結果的地方,命令也能正常下去,而且正常退出。

  為什麼程式會阻塞呢?Google了一下,發現了大家的解釋,應該也是比較權威的解釋吧:每個程序都有自己的標準輸入、標準輸出、標準錯誤輸出,對於某些 依賴於OS的程序,可能其輸出緩衝區很小,如果不能及時的讀出(標準輸出、標註錯誤輸出),將導致程序不能正常退出。我的程式中標準輸出已經讀了,顯然原因不是這個,難道是錯誤輸出緩衝區中的資料沒有讀出導致的?帶著這個疑問,對程式作了一些更改:

ProcessBuilder pb = new ProcessBuilder("wmic",...);
pb.redirectErrorStream(true);
Process p = pb.start();
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String tmp = null;
while ((tmp = br.readline()) != null) {
  System.out.println(tmp);
}
int exitValue = p.waitfor();
  

  編譯執行,發現還是有問題,依然還是阻塞。又google了一下,大家的評論大多還是關於標準輸出和標準錯誤輸出,那這程式應該是沒有問題了。後來在 cmd中敲入wmic的命令,發現wmic命令敲入以後會進入一個自有的提示符中,難道是因為標準輸入的問題。後來又google了一下,驗證了我的猜 想,果然是因為wmic程序會等待標準輸入,而程式中沒有處理標準輸入的地方,是標準輸入阻塞了程序的退出,修改程式碼:

ProcessBuilder pb = new ProcessBuilder("wmic",...);
pb.redirectErrorStream(true);
Process p = pb.start();
p.getOutputStream().close();
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String tmp = null;
while ((tmp = br.readline()) != null) {
  System.out.println(tmp);
}
int exitValue = p.waitfor();
   編譯執行,程式成功執行。果然是標準輸入的原因。

  後來執行的過程中換了一個服務的名稱,發現執行失敗(能夠正常退出,但是返回的結果是“無效動作”),但是同樣的命令,在命令列中執行成功,而且直接適用 Runtime.exec()方法中寫入整個命令也能夠執行成功,難道是ProcessBuilder的錯誤,ProcessBuilder建構函式有兩 個:

  ProcessBuilder(List<String> command)

  ProcessBuilder(String... command)

  找到ProcessBuilder的原始碼,發現了對List<String>的解析方法:JDK將List中的所有字串用空格連線,對 list中的每個字串JDK先判斷串中是否包含空格,如果包含空格,用雙引號將該字串引起來,再拼到前面字串的後面(應該是為了解決路徑中包含空格 的問題),可恰好Wmic命令的引數中有一段是name="ServiceName",如果ServiceName中包換空格,JDK就會把 name="service name"的外層加一個雙引號,導致wmic不能解析該命令了。

  問題終於全都解決了,耗費了多半天的時間,不過收穫總是有的,這裡總結一下,在使用Java呼叫外部命令的時候,一定要注意對標準輸出、標準輸入和錯誤輸 出的處理。對於一般的命令,只需要將標準輸出和錯誤輸出合併,一起讀出來或者在另外的執行緒中讀出來,而對於一些特殊的命令,還有處理標準輸入。建議即使不 使用標準輸入,先close了,總是不會出錯了。另外,使用ProcessBuilder時要注意它的空格處理方式是否是你想要的,如果不是,就不能用 ProcessBuilder了,直接使用Runtime.exec()就好了。

    另外,如果子程序Process執行的工作目錄與當前主執行緒的工作目錄一相同,則可以用下面兩種方法指定子程序Process執行的工作目錄。

ProcessBuilder.directory(new File("filepath"));

Runtime.getRuntime().exec(command, evn, new File("filepath"));