Javaで外部プロセスを操作するには?
Javaでプログラムを書いている時に、一部の挙動をシェルアウトしたいと思ったら、以下の2通りの方法がある。
- Runtime
- ProcessBuilder
ProcessBuilderはRuntimeの後継であり、Java1.5以降では外部プロセスを使用する際にはProcessBuilderが推奨されている。
なお、調べた限りでは、Javaが実行されているプロセスでbashコマンドを実行する方法は存在しなそうだった。(これがあればコンテキストスイッチなしでbashを実行できるのだが、おそらく、Javaプログラムはシェルをロードしないので、OSに頼るしかないということなのだと思う)
Runtime
RuntimeはStringにしたコマンドを1行そのまままたはリストとして受け取って実行する。以下のコードでは、Runtime.getRuntime().exec()メソッドに「ls」「-l」 「/home」をStringのリストとして渡して実行することで、
- プロセスの起動
- ls -l /home の実行
を行なっている。
try {
Process process = Runtime.getRuntime().exec(new String[]{"ls", "-l", "/home");
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
ProcessBuilder
一方で、ProcessBuilderはより可読性と柔軟性が高い方法で実行インターフェースをユーザに提供している。
try {
ProcessBuilder builder = new ProcessBuilder("ls", "-l", "/home");
Process process = builder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
このように、ProcessBuilderはRuntimeと違ってインスタンス化して利用し、builder.start()メソッドによって明示的にコマンドを実行している。
RuntimeとProcessBuilderの差
- コマンド指定の柔軟性
RuntimeはStringのリストまたは文字列として渡す一方、ProcessBuilderは任意数の引数を取れるため、柔軟にコマンドを指定できる。 - 環境変数の制御
ProcessBuilderでは、environment()メソッドを利用することで環境変数を制御できる。 - 入出力のリダイレクト
ProcessBuilderは入出力をリダイレクトする機能をredirectInput()、redirectOutput()等のメソッドで提供しているが、Runtimeには存在しない。 - エラーハンドリング
ProcessBuilderのstart()メソッドはIOExceptionをスローするため、エラーを適切にハンドリングすることができる。一方で、Runtime.getRuntime.exec()はプロセスの起動に失敗しても例外を送出しないので、ハンドリングが難しい。
Processクラス
Runtime.getRuntime().exec
ProcessBuilder
は両方とも、Processクラスを返す。
Processクラスは、Javaアプリケーションの中でネイティブプロセスの制御を行うためのクラスである。Javaで起動された外部プロセスには標準出力・標準入力の設定がされておらず、このProcessクラスを使ってstdin, stdoutを制御する必要がある。提供されているメソッド例としては、
- getOutputStream(出力ストリームの取得)
- getInputStream(入力ストリームの取得)
- destroy(プロセスの強制終了)
などがある
https://docs.oracle.com/javase/jp/9/docs/api/java/lang/Process.html
単一のプロセスにいくつもの処理を実行させるには?
例えば、以下のように、コマンドを実行するクラスを作る。
public class CommandExecutor {
public String executeCommand(String command) {
StringBuffer output = new StringBuffer();
Process p;
try {
p = Runtime.getRuntime().exec(command);
p.waitFor();
BufferedReader reader =
new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = "";
while ((line = reader.readLine())!= null) {
output.append(line + "\n");
}
} catch (Exception e) {
e.printStackTrace();
}
return output.toString();
}
}
このメソッドexecuteCommandを使用して、以下のようにいくつかのコマンドを実行する。
public static void main (String[] args){
CommandExecutor commandExecutor = new CommandExecutor();
System.out.println(commandExecutor.executeCommand("ls"));
System.out.println(commandExecutor.executeCommand("cd bin"));
System.out.println(commandExecuter.executeCommand("ls"));
}
これは、lsした後でbinに移動してlsをしているように見えるが、それぞれのコマンドは別のプロセスで実行されるため、1回目のlsと2回目のlsは同じ結果を返してしまう。
これを回避するためには、bashコマンドでプロセスを起動し、このプロセスにFileで.shファイルの中身を渡して全て実行させる必要がある。以下がコード例。
public void executeCommands() throws IOException {
File tempScript = createTempScript();
try {
ProcessBuilder pb = new ProcessBuilder("bash", tempScript.toString());
pb.inheritIO();
Process process = pb.start();
process.waitFor();
} finally {
tempScript.delete();
}
}
public File createTempScript() throws IOException {
File tempScript = File.createTempFile("script", null);
Writer streamWriter = new OutputStreamWriter(new FileOutputStream(
tempScript));
PrintWriter printWriter = new PrintWriter(streamWriter);
printWriter.println("#!/bin/bash");
printWriter.println("cd bin");
printWriter.println("ls");
printWriter.close();
return tempScript;
}
こうすることで、
#!/bin/bash
cd bin
ls
と書かれたbash scriptを実行できる。
(参考)https://stackoverflow.com/questions/26830617/running-bash-commands-in-java