在設計程式的時候偶爾會需要依靠系統指令或是其它的程式來取得一些相關的訊息,或是進行一些特殊的處理,如查看系統的網卡介面,或是呼叫FFmpeg來實現影音編碼的格式轉換。雖然這些工作基本上都可以靠Java原生程式或是JNI來自行實作,但既然有已存在的指令和執行檔可以用,能省下許多開發時間,何不去用呢?



使用Java去執行系統指令或是其它可執行檔案,可以用Runtime.getRuntime().exec或是ProcessBuilder,兩種方式執行指令後的結果均相同。Runtime類別物件所提供的exec方法可對傳入的參數指令使用StringTokenizer將指令字串切割成陣列後,再使用ProcessBuilder建立出Process,算是ProcessBuilder的外殼。以下是JDK內exec方法的部分實作程式:

public Process exec(String command, String[] envp, File dir)
    throws IOException {
    if (command.length() == 0)
        throw new IllegalArgumentException("Empty command");

    StringTokenizer st = new StringTokenizer(command);
    String[] cmdarray = new String[st.countTokens()];
    for (int i = 0; st.hasMoreTokens(); i++)
        cmdarray[i] = st.nextToken();
    return exec(cmdarray, envp, dir);
}

public Process exec(String[] cmdarray, String[] envp, File dir)
    throws IOException {
    return new ProcessBuilder(cmdarray)
        .environment(envp)
        .directory(dir)
        .start();
}

從上面程式可以知道,使用Runtime.getRuntime().exec來執行系統指令或是其它可執行檔案,會比直接使用ProcessBuilder還要方便。然而,如果是要一直重複執行同樣的指令的話,自行實體化出ProcessBuilder來重複產生Process物件會比較有效率。

如果要執行ping 8.8.8.8這個指令,可寫成如下程式:

Process process = Runtime.getRuntime().exec("ping 8.8.8.8");

或是

ProcessBuilder processBuilder = new ProcessBuilder("ping", "8.8.8.8");
Process process = processBuilder.start();

如果要知道Process執行過程所吐出來的訊息與結果回傳值,可以使用Process物件的getInputStream搭配Process物件的waitFor方法。如果要提供資料給Process所執行的指令,可以使用Process物件的getOutputStream。這裡有個觀念要注意的是,Process物件所執行的指令,其輸出資料對於Process物件來說是輸入(將指令輸出的資料傳入至Java程式中),因此是InputStream;而Process物件所執行的指令,其輸入資料對於Process物件來說是輸出(用Java程式輸出資料供指令使用),因此是OutputStream

取得執行過程與回傳結果的程式如下:

BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
	System.out.println(line);
}
int returnValue = process.waitFor();
System.out.println(String.format("Return: %d", returnValue));

使用ProcesswaitFor()方法,可以將目前的執行緒暫時停止,等待Process結束執行後才會繼續再執行之後的程式。若有使用waitFor(),而沒有去讀取PrecessInputStream,可能會造成Process暫停的情形。

如果需要提供資料給Process所執行的指令,可以寫成如下程式:

BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
writer.write(inputString);
writer.flush();

通常我們提供的資料是一行一行輸入進去的,此時就要丟換行字元\n進去,分隔每一行的資料。程式如下:

writer.write(line1);
writer.write("\n");
writer.write(line2);
writer.write("\n");
...

上面是使用Java原生提供的類別來呼叫系統指令和外部執行檔案,實作起來較為繁瑣,以下提供一個更快的作法。

MagicCommand

MagicCommand套件可以用來快速地執行系統指令或是其它可執行檔案,並提供CallBack方法回傳指令的執行狀況。

取得MagicCommand

GitHub:

建立Command物件

使用MagicCommand套件,必須實體化出Command物件,並在建構子內傳入要執行的指令參數。

final String commandString = "ping 8.8.8.8";
final Command command = new Command(commandString); //實體化出Command物件

設定CommandListener

為了得知指令的執行狀況,需要設定一個CommandListener物件給Command物件,並實作出CommandListener介面所定義的以下幾種方法:

  • commandStart(String id):指令執行時會呼叫這個方法。
  • commandRunning(String id, String message, boolean isError):執行指令中,每輸出一行訊息,就會呼叫這個方法。
  • commandException(String id, exception):執行指令時若出現例外,會呼叫這個方法。
  • commandEnd(String id, int returnValue):指令執行結束後,會呼叫這個方法。

程式如下:

command.setCommandListener(new CommandListener() {

	    @Override
	    public void commandStart(final String id) {
	    }

	    @Override
	    public void commandRunning(final String id, final String message, final boolean isError) {
	        System.out.println(message);
	    }

	    @Override
	    public void commandException(final String id, final Exception exception) {
	        exception.printStackTrace(System.out);
	    }

	    @Override
	    public void commandEnd(final String id, final int returnValue) {
	    }
	});

執行指令

Command物件提供runrunAsync方法來執行指令,一個Command物件可以重複呼叫多次的runrunAsync方法來執行相同的指令。正常情況下,每呼叫一次都會產生一個命令行程,可用Command物件的toString方法來查看目前Command物件正在進行的命令行程有哪些,每個命令行程都會在該Command物件下有個獨立的行程ID字串,以利辨識。行程ID建議由設計師自行指定並維護,可在呼叫runrunAsync方法時傳入行程ID。runrunAsync方法的差別在於:前者會在同一執行緒下執行指令,後者會在新的執行緒下執行指令。

例如,建立出一個ID為first的命令行程,並在新的執行緒上執行,程式如下:

command.runAsync("first");

提供資料給指定ID的命令行程

有些指令執行的時候可能會需要讓使用者填入資料進去,可以使用Command物件提供的inputStringToRunningProcess方法。

輸入exit字串與換行字元給ID為first的命令行程,程式如下:

command.inputStringToRunningProcess("first", "exit\n"); // 注意那個「\n」

停止命令行程

停止ID為first的命令行程。

command.stop("first");

停止Command物件下所有的命令行程。

command.stopAll();

可能會遇到的編碼問題

不同系統、不同語系、不同的指令與程式,可能會使用不同的編碼方式來輸入和輸出文字。像是在繁體中文的Windows下,預設的文字編碼方式是「Big5」,如果直接使用預設的方式來建立Command物件,可能會遇到文字亂碼的問題。

如要指定編碼方式,可以在Command的建構子上傳入正確的Charset物件。如下圖,成功解決中文亂碼的問題: