在設計程式的時候偶爾會需要依靠系統指令或是其它的程式來取得一些相關的訊息,或是進行一些特殊的處理,如查看系統的網卡介面,或是呼叫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));
使用Process
的waitFor()
方法,可以將目前的執行緒暫時停止,等待Process結束執行後才會繼續再執行之後的程式。若有使用waitFor()
,而沒有去讀取Precess
的InputStream
,可能會造成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
物件提供run
或runAsync
方法來執行指令,一個Command
物件可以重複呼叫多次的run
或runAsync
方法來執行相同的指令。正常情況下,每呼叫一次都會產生一個命令行程,可用Command
物件的toString
方法來查看目前Command
物件正在進行的命令行程有哪些,每個命令行程都會在該Command
物件下有個獨立的行程ID字串,以利辨識。行程ID建議由設計師自行指定並維護,可在呼叫run
或runAsync
方法時傳入行程ID。run
和runAsync
方法的差別在於:前者會在同一執行緒下執行指令,後者會在新的執行緒下執行指令。
例如,建立出一個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物件。如下圖,成功解決中文亂碼的問題: