Runtime.exec() 的陷阱

原文地址:http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html?page=4

作爲Java語言的一部分。java.lang包被隱藏的導入到每一個Java程序。這個包的表面陷阱,經常影響到大多數程序員。這個月,我將討論運行時exec()方法時的潛伏陷阱。

陷阱4:當運行exec()不會


java.lang.Runtime類,突出了靜態方法calledgetRuntime(),,它會檢索當前的Java運行時環境。這是唯一的方法來獲取Runtime對象的引用。獲取該引用,您通過可以調用Runtime類的exec()方法運行外部程序。開發人員經常調用這個方法來啓動瀏覽器顯示一個HTML幫助頁面。

exec()有四個重載:

public Process exec(String command);
public Process exec(String [] cmdArray);
public Process exec(String command, String [] envp);
public Process exec(String [] cmdArray, String [] envp); 

對於每個這樣的方法,都會產生一個命令,並可能攜帶一組參數——被傳遞給一個特定操作系統的函數調用。這隨後創建一個特定操作系統的進程(一個運行着的程序),procss類將持有該程序返回Java VM的引用。這個procss類是一個抽象類,具體子類的實現依賴於不同的底層操作系統。

你可以通過三種可能的輸入參數到這些方法:

1、一個字符串,表示程序執行和程序的任何參數。

2、一個字符串數組,通過參數來區分出程序的實現功能。

3、一個環境變量的數組

傳遞環境變量是,使用格式化的方式:名稱=值。如果你使用單個字符串和它的參數的方式調用exec()的重載,,注意字符串是通過StringTokenizer類被解析,使用空格作爲分隔符。

陷入 IllegalThreadStateException

運行exec()的第一個陷阱,是theIllegalThreadStateException。 普遍上,第一次對api的嘗試,都是基於一些最常用的方法。例如,執行一個java vm的外部過程,我們使用exec()方法。查看外部過程的返回值,我們使用process類的exitValue()方法。看到的值外部過程的回報,我們使用exitValue()方法在過程類。在我們的第一個示例中,我們將嘗試執行Java編譯器(javac exe)。

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

運行的BadExecJavac產生:
E:\classes\com\javaworld\jpitfalls\article2>java BadExecJavac
java.lang.IllegalThreadStateException: process has not exited
        at java.lang.Win32Process.exitValue(Native Method)
        at BadExecJavac.main(BadExecJavac.java:13)

如果一個外部進程尚未完成,exitValue()方法將拋出IllegalThreadStateException。這就是程序失敗的原因。儘管文檔聲明瞭這個事實,爲什麼這個方法不能等待一個有效性的結果返回?

更徹底的看看process的可用方法,我們發現waitFor()方法,能準確地完成這一工作。事實上,waitFor()也返回退出值,這意味着你不用同時使用exitValue()和waitFor(),而是選擇其中之一。唯一可能的情況,你會使用exitValue()而不是waitFor(),是當你不希望你的程序塊等待一個外部的過程,而這個外部過程可能永遠不會完成。取代waitFor()方法,我寧願在exitValue()方法內部,傳遞一個布爾參數稱爲waitFor,來確定是否當前線程應該等待。一個布爾變量會更好,因爲exitValue()是一個更合適的名稱,,也沒有必要讓兩個方法在在不同條件下來執行相同的功能。這種簡單的通過輸入參數傳遞,來區分條件,執行不同的功能。

    因此,爲了避免這個陷阱,要麼抓住theIllegalThreadStateException或等待進程完成。

    現在,讓我們在清單4.1的基礎,通過等待進程完成來解決這個問題。清單4.2,程序會再次execute javac.exx,然後等待外部過程來完成。

清單4.2 BadExecJavac2.java
import java.util.*;
import java.io.*;
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();
          }
    }
}

不幸的是,一個運行的BadExecJavac2不產生任何輸出。程序掛起、一直未完成。爲什麼javac進程一直沒有完成?

爲什麼 Runtime.exec() 掛起


    JDK的Javadoc文檔提供了這個問題的答案:

因爲一些本機平臺只提供有限的緩衝區大小爲標準輸入和輸出流,未能及時寫輸入流或讀取輸出流的子流程可能會導致子流程阻止,甚至死鎖。

    這只是程序員不閱讀文檔一個案例。隱含常聽到的建議:讀好手冊(RTFM)?答案是部分是的。在這種情況下,閱讀Javadoc將讓你停在半途;它解釋說,你需要處理流到你的外部過程,但是它沒有告訴你怎樣做。
    
    這個問題,原因明顯是大量程序員的問題和誤解這個API有關信息:儘管運行exec()和流程API看起來非常簡單,那簡單是欺騙,因爲簡單,或者說是明顯,使用的API是容易出錯。這裏給API設計師的建議是,爲簡單的操作保留簡單的API。容易產生複雜性操作和具有特定平臺依賴性應該準確反映問題域。有可能某個抽象進行的太深。這個JConfig庫提供了一個示例的一個更完整的API來處理文件和流程操作(請參閱下面的Resources參考資料以獲得更多信息)。


    現在,讓我們遵循JDK文檔和處理輸出的javac過程。當您運行javac不帶任何參數,它產生一組使用語句,描述瞭如何運行這個程序及其意義的所有可用的程序的選項。瞭解這些信息會到stderr(標準錯誤)流,您可以很容易地編寫一個程序,在等待進程退出前檢測這個輸出流。

    清單4.3完成這個任務。雖然這種方法可以運行,但這不是一個好的通用解決方案。因此,清單4.3的程序被命名爲MediocreExecJavac;它只提供了一個平庸的解決方案。一個更好的解決方案將不輸出或者清空標準錯誤流和標準輸出流。最好的解決方案將清空這些流(我之後將證明)。

清單 4.3 MediocreExecJavac.java
import java.util.*;
import java.io.*;
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>");
            while ( (line = br.readLine()) != null)
                System.out.println(line);
            System.out.println("</ERROR>");
            int exitVal = proc.waitFor();
            System.out.println("Process exitValue: " + exitVal);
        } catch (Throwable t)
          {
            t.printStackTrace();
          }
    }
}

運行MediocreExecJava產生:
E:\classes\com\javaworld\jpitfalls\article2>java MediocreExecJavac
<ERROR>
Usage: javac <options> <source files>
where <options> includes:
  -g                     Generate all debugging info
  -g:none                Generate no debugging info
  -g:{lines,vars,source} Generate only some debugging info
  -O                     Optimize; may hinder debugging or enlarge class files
  -nowarn                Generate no warnings
  -verbose               Output messages about what the compiler is doing
  -deprecation           Output source locations where deprecated APIs are used
  -classpath <path>      Specify where to find user class files
  -sourcepath <path>     Specify where to find input source files
  -bootclasspath <path>  Override location of bootstrap class files
  -extdirs <dirs>        Override location of installed extensions
  -d <directory>         Specify where to place generated class files
  -encoding <encoding>   Specify character encoding used by source files
  -target <release>      Generate class files for specific VM version
</ERROR>
Process exitValue: 2

    所以,MediocreExecJavac運行產生一個退出值2。通常,一個退出值0表示成功,任何非零值表示一個錯誤。這些退出值的含義取決於特定的操作系統。一個Win32錯誤值爲2是一個“未找到文件”錯誤。這是有道理的,因爲javac期望我們遵循的程序源代碼文件進行編譯。

    因此,繞過第二個陷阱——永遠掛在運行時exec()——如果你運行的程序產生輸出或期望的輸入,確保程序的輸入和輸出流。

假設一個命令是一個可執行程序


    在Windows操作系統,許多新程序員運行exec(),當試圖使用它來完成爲非執行命令,像dir和複製,他們就掉進了運行exec()的第三陷阱。清單4.4展示的正是這種情況:

清單 4.4 BadExecWinDir.java
import java.util.*;
import java.io.*;
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>");
            while ( (line = br.readLine()) != null)
                System.out.println(line);
            System.out.println("</OUTPUT>");
            int exitVal = proc.waitFor();            
            System.out.println("Process exitValue: " + exitVal);
        } catch (Throwable t)
          {
            t.printStackTrace();
          }
    }
}

運行BadExecWinDir輸出:

E:\classes\com\javaworld\jpitfalls\article2>java BadExecWinDir
java.io.IOException: CreateProcess: dir error=2
        at java.lang.Win32Process.create(Native Method)
        at java.lang.Win32Process.<init>(Unknown Source)
        at java.lang.Runtime.execInternal(Native Method)
        at java.lang.Runtime.exec(Unknown Source)
        at java.lang.Runtime.exec(Unknown Source)
        at java.lang.Runtime.exec(Unknown Source)
        at java.lang.Runtime.exec(Unknown Source)
        at BadExecWinDir.main(BadExecWinDir.java:12)


   如前所述,誤差值爲2的意思是“未找到文件”,在這種情況下,意味着可執行文件名爲dir.exe不能被發現。這是因爲目錄命令是Windows命令解釋器的一部分,而不是一個單獨的可執行文件。要運行Windows命令解釋器,執行orcmd.exe或者command.com,這取決於您使用的Windows操作系統。清單4.5運行的一個Windows命令解釋器,然後執行用戶提供的命令(如。,dir)。

清單 4.5 GoodWindowsExec.java
import java.util.*;
import java.io.*;
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();  
              }
    }
}
public class GoodWindowsExec
{
    public static void main(String args[])
    {
        if (args.length < 1)
        {
            System.out.println("USAGE: java GoodWindowsExec <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();
          }
    }
}

使用dir命令,運行GoodWindowsExec產生:
E:\classes\com\javaworld\jpitfalls\article2>java GoodWindowsExec "dir *.java"
Execing cmd.exe /C dir *.java
OUTPUT> Volume in drive E has no label.
OUTPUT> Volume Serial Number is 5C5F-0CC9
OUTPUT>
OUTPUT> Directory of E:\classes\com\javaworld\jpitfalls\article2
OUTPUT>
OUTPUT>10/23/00  09:01p                   805 BadExecBrowser.java
OUTPUT>10/22/00  09:35a                   770 BadExecBrowser1.java
OUTPUT>10/24/00  08:45p                   488 BadExecJavac.java
OUTPUT>10/24/00  08:46p                   519 BadExecJavac2.java
OUTPUT>10/24/00  09:13p                   930 BadExecWinDir.java
OUTPUT>10/22/00  09:21a                 2,282 BadURLPost.java
OUTPUT>10/22/00  09:20a                 2,273 BadURLPost1.java
... (some output omitted for brevity)
OUTPUT>10/12/00  09:29p                   151 SuperFrame.java
OUTPUT>10/24/00  09:23p                 1,814 TestExec.java
OUTPUT>10/09/00  05:47p                23,543 TestStringReplace.java
OUTPUT>10/12/00  08:55p                   228 TopLevel.java
OUTPUT>              22 File(s)         46,661 bytes
OUTPUT>                         19,678,420,992 bytes free
ExitValue: 0

      GoodWindowsExec運行與任何相關的文檔類型將啓動與之關聯文檔類型的應用程序。例如,啓動Microsoft Word來顯示一個Word文檔(即一個帶doc擴展名的文件)。
>java GoodWindowsExec "yourdoc.doc"

    注意,GoodWindowsExec使用操作系統的系統名字屬性來確定,您運行的是哪種Windows操作系統,從而確定適當的命令解釋器。

    在執行命令解釋器,使用StreamGobbler類來處理標準錯誤和標準輸入流。StreamGobbler通過獨立線程,來清空任何傳遞到它的流。這個類使用一個簡單的字符串類型來表示流清空,在當它將打印行輸出到控制檯時起作用

    因此,爲了避免運行exec()的第三個陷阱,首先不要認爲一個命令是一個可執行程序;其次要了解你是否正在執行一個獨立的可執行文件或一種解釋命令。在結束這一節中,我將演示一個簡單的命令行工具,可以幫你分析。

    值得注意的是,用於獲取一個進程的輸出流的方法叫做getInputStream()。要記住的是,API看待對象的角度,是從Java程序內部,而不是外部的過程。因此,外部程序的輸出是Java程序的輸入。這種邏輯,也表明外部程序的輸入流,是一個Java程序的輸出流。

Runtime.exec()不是命令行



      最後一個陷阱是,錯誤的假設命令行(shell)能接受的字符串,在Runtime.exec()內也能接受。運行exec()會受到更多的限制,而且不能跨平臺。這個陷阱是由於用戶試圖使用exec()方法,接受一個符合命令行可執行的字符串。混亂可能是因爲command是exec()方法的參數名。因此,程序員錯誤地將參數command與他可以輸入命令行的東西聯繫起來,而不是將它看做單個程序及其參數。下面的清單4.6中,一個用戶嘗試執行命令和重定向輸出在一個調用exec():

清單 4.6 BadWinRedirect.java
import java.util.*;
import java.io.*;
// StreamGobbler omitted for brevity
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();
          }
    }
}

運行BadWinRedirect 產生:
E:\classes\com\javaworld\jpitfalls\article2>java BadWinRedirect
OUTPUT>'Hello World' > test.txt
ExitValue: 0

    該程序BadWinRedirect試圖重定向Java版本的echo程序輸出到文件test.txt。 然而,我們發現文件test.txt並不存在。jecho的程序只需要它的命令行參數,並將其寫入到標準輸出流。(你會發現jecho的源代碼在源代碼可供下載inResources.)清單4.6,用戶認爲你可以重定向標準輸出到一個文件中,就像你可能在一個DOS命令行中執行一樣。然而,你不能通過這種方法重定向輸出。這裏不正確的假設是,exec()方法會像一個shell解析器一樣,但是它不會。相反,exec()執行一個單一可執行文件(一個程序或腳本)。如果你想處理流重定向或管道,要麼用另一個程序實現,你必須通過編程,使用java。io包。清單4.7 完成重定向標準輸出流的jecho流程到一個文件中。

清單4.7 GoodWinRedirect.java
import java.util.*;
import java.io.*;
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();  
            }
    }
}
public class GoodWinRedirect
{
    public static void main(String args[])
    {
        if (args.length < 1)
        {
            System.out.println("USAGE java GoodWinRedirect <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();
          }
    }
}

運行 GoodWinRedirect 產生:
E:\classes\com\javaworld\jpitfalls\article2>java GoodWinRedirect test.txt
OUTPUT>'Hello World'
ExitValue: 0

    在運行GoodWinRedirect、test.txt確實存在。解決這個陷阱很簡單,就是通過外部進程控制重定向的標準輸出流,獨立於exec()方法。我們創建一個單獨的OutputStream、讀取的文件名,打開文件,我們將從派生進程的標準輸出到文件。清單4.7通過對StreamGobbler類添加一個新的構造器來完成這個任務。新構造函數有三個參數:讀入輸入流,String類型標記我們是否正在使用讀入輸出流,重定向輸入。這個新版本的StreamGobbler並不違反任何代碼之前使用的,因爲我們沒有改變現有的公共API——我們只擴展它。
    因爲參數來運行exec()是依賴於操作系統、不同系統之間的適用命令會有所變化。所以,在最終確定Runtime.exec()參數和編寫代碼,快速測試。清單4.8是一個簡單的命令行工具,允許你這樣做。
    這是一個有用的練習:嘗試修改TestExec重定向標準輸入和標準輸出到一個文件。當在Windows 95或Windows 98上執行javac編譯器,這將解決錯誤消息超出命令行有限緩存的問題

清單4.8 TestExec.java
import java.util.*;
import java.io.*;
// class StreamGobbler omitted for brevity
public class TestExec
{
    public static void main(String args[])
    {
        if (args.length < 1)
        {
            System.out.println("USAGE: java TestExec \"cmd\"");
            System.exit(1);
        }
        
        try
        {
            String cmd = args[0];
            Runtime rt = Runtime.getRuntime();
            Process proc = rt.exec(cmd);
            
            // any error message?
            StreamGobbler errorGobbler = new 
                StreamGobbler(proc.getErrorStream(), "ERR");            
            
            // any output?
            StreamGobbler outputGobbler = new 
                StreamGobbler(proc.getInputStream(), "OUT");
                
            // kick them off
            errorGobbler.start();
            outputGobbler.start();
                                    
            // any error???
            int exitVal = proc.waitFor();
            System.out.println("ExitValue: " + exitVal);
        } catch (Throwable t)
          {
            t.printStackTrace();
          }
    }
}



運行TestExec 啓動Netscape瀏覽器和加載Java幫助文檔  產生:

E:\classes\com\javaworld\jpitfalls\article2>java TestExec "e:\java\docs\index.html"
java.io.IOException: CreateProcess: e:\java\docs\index.html error=193
        at java.lang.Win32Process.create(Native Method)
        at java.lang.Win32Process.<init>(Unknown Source)
        at java.lang.Runtime.execInternal(Native Method)
        at java.lang.Runtime.exec(Unknown Source)
        at java.lang.Runtime.exec(Unknown Source)
        at java.lang.Runtime.exec(Unknown Source)
        at java.lang.Runtime.exec(Unknown Source)
        at TestExec.main(TestExec.java:45)

    我們的第一個測試失敗,錯誤193。Win32誤差值193“不是一個有效的Win32應用程序。“這個錯誤告訴我們,沒有找到關聯的應用程序(如網景瀏覽器)存在,而且這一流程不能運行一個HTML文件沒有關聯的應用程序。
因此,我們嘗試測試,這次又給它一個完整路徑網景。(或者,我們可以添加到我們的PATH  environment網景變量)。第二次運行的TestExec產生:

E:\classes\com\javaworld\jpitfalls\article2>java TestExec 
"e:\program files\netscape\program\netscape.exe e:\java\docs\index.html"
ExitValue: 0


ok!網景瀏覽器的打開,然後裝入Java幫助文檔。
一個額外的改進包括一個命令行開關,使TestExec接受從標準輸入的輸入流。然後您將使用Process.getOutputStream()方法通過輸入派生外部程序。

總之,遵循這些法則,以避免的陷阱在運行時執行():

你不能從外部過程獲得一個退出狀態,直到它已經退出
你必須從你外部程序立即處理輸入、輸出和錯誤流
您必須使用運行時exec()來執行程序
你不能使用運行時執行()就像一個命令行



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章