Javaで外部プロセスを実行する方法についてご紹介いたします。
例えばgzipコマンドやwget、curlコマンド、nkfコマンド等の外部プロセスを実行する方法になります。
これらの外部プロセスをJavaから利用する際の注意点としましては、実装方法次第でお見合い(デッドロック)状態となってしまう可能性があることです。
外部プロセスの標準出力、標準エラーがバッファサイズを超えてしまう可能性があり、そのような状況になると外部プロセスは標準出力への出力ができず、待ち状態となってしまいます。
また、Javaプログラム上で外部プロセスの終了を待つようにしていると、外部プロセスはバッファが空くのを待っており、Javaプログラム側はプロセス終了を待っているという状態になってしまう可能性があります。
それらの状況を回避することを考慮した実装方法をご紹介いたします。
実装方法概要
Java1.4以前はRuntimeを利用しておりましたが、1.5以降からはRuntimeクラスではなくProcessBuilderクラスが推奨されます。
外部プロセスを実行する場合、外部プロセスの標準出力、標準エラーはProcessクラスのInputStreamクラスが受け手になります。
このInputStreamクラスを読み込まずにいると、外部プロセスの標準出力、標準エラーがバッファサイズを超えてしまう可能性があり、そのような状況になると外部プロセスは標準出力への出力ができずに待ち状態が続く原因となります。
さらにJavaプログラム上で外部プロセスの終了を待つようにしていると、外部プロセスはバッファが空くのを待っており、Javaプログラム側はプロセス終了を待っているというデッドロックが発生します。
従って、外部プロセスを実行する場合は標準出力と標準エラーをJava側で読み込むようにするか、そもそもファイルへ出力するなど、外部プロセスの標準出力、標準エラー出力が待ち状態とならないように工夫する必要があります。
Javaで外部プロセスを実行するサンプル(1)
外部プロセスを実行するサンプルコードです。
標準出力、標準エラーをJavaと同じ標準出力、標準エラーに出力する場合は以下のようになります。
外部プロセスの実行内容がJavaのログに残るため、外部プロセスの実行内容を取得する必要が無い場合はこちらが良いかもしれません。
public static void main(String[] args) {
java.io.File dir = new File("/tmp");
String[] Command = {"tar","cvzf", "test.tar.gz", "test"};
long timeOutSec = 5 * 60; // 5分でタイムアウト
boolean isFinished = processDone(Command, dir, timeOutSec);
}
public static boolean processDone(String[] Command, File dir, long timeOutSec) {
List<String> cmdArray = Arrays.asList(Command);
ProcessBuilder builder = new ProcessBuilder(cmdArray);
// 作業ディレクトリ
builder.directory(dir);
// 標準エラーを標準出力へ統合
builder.redirectErrorStream(true);
// Javaと同じ標準出力に統合
builder.inheritIO();
Process p = null;
boolean isFinished = false;
try {
p = builder.start();
isFinished = p.waitFor(timeOutSec, TimeUnit.SECONDS); // プロセスが正常終了するまで待機
} catch (InterruptedException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println(e.getMessage());
} finally {
if (Objects.nonNull(p) && p.isAlive()) {
p.destroy();
}
}
return isFinished;
}
Javaで外部プロセスを実行するサンプル(2)
外部プロセスを実行するサンプルコードです。
標準出力、標準エラーをファイルに出力する場合は以下のようになります。
外部プロセスの実行内容がファイルに残るため、外部プロセスの実行内容を取得する場合やJavaのログとは分離して管理したい場合はこちらが良いと思います。
public static void main(String[] args) {
java.io.File dir = new File("/tmp");
String[] Command = {"tar","cvzf", "test.tar.gz", "test"};
long timeOutSec = 5 * 60; // 5分でタイムアウト
boolean isFinished = processDone(Command, dir, timeOutSec);
}
public static boolean processDone(String[] Command, File dir, long timeOutSec) {
List<String> cmdArray = Arrays.asList(Command);
ProcessBuilder builder = new ProcessBuilder(cmdArray);
// 作業ディレクトリ
builder.directory(dir);
// 標準エラーを標準出力へ統合
builder.redirectErrorStream(true);
// logファイルに出力
File log = new File("command.log");
builder.redirectOutput(log);
// builder.redirectOutput(Redirect.appendTo(log)); // ファイルへ追記の場合
Process p = null;
boolean isFinished = false;
try {
p = builder.start();
isFinished = p.waitFor(timeOutSec, TimeUnit.SECONDS); // プロセスが正常終了するまで待機
} catch (InterruptedException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println(e.getMessage());
} finally {
if (Objects.nonNull(p) && p.isAlive()) {
p.destroy();
}
}
return isFinished;
}
Javaで外部プロセスを実行するサンプル(3)
外部プロセスを実行するサンプルコードです。
以下のサンプルは標準出力と標準エラーの出力先をProcessのInputStreamに設定し、読み込み内容を破棄しています。
外部プロセスが実行できればよく、出力内容は全く不要の場合はこちらでも良いと思います。
しかしながら、出力内容が不要な場合、上記(1)、(2)のいずれかでも問題無く、わざわざ読み込んで破棄する方法を選択する必要は無いと思います。外部プロセスの出力結果をJavaプログラム上で利用したい場合も直接Streamを読み込むより、ファイルに出力した内容を読み込むほうが外部プロセスの結果が残るので良いと思います。
public static void main(String[] args) {
java.io.File dir = new File("/tmp");
String[] Command = {"tar","cvzf", "test.tar.gz", "test"};
long timeOutSec = 5 * 60; // 5分でタイムアウト
boolean isFinished = processDone(Command, dir, timeOutSec);
}
public static boolean processDone(String[] Command, File dir, long timeOutSec) {
List<String> cmdArray = Arrays.asList(Command);
ProcessBuilder builder = new ProcessBuilder(cmdArray);
// 作業ディレクトリ
builder.directory(dir);
// 標準エラーを標準出力へ統合
builder.redirectErrorStream(true);
boolean isFinished = false;
try {
Process p = builder.start();
try {
new Thread(() -> {
try (InputStream is = p.getInputStream()) {
// 読み込むだけで何もしない
while (is.read() >= 0);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
} ).start();
isFinished = p.waitFor(timeOutSec, TimeUnit.SECONDS); // プロセスが正常終了するまで待機
} catch (InterruptedException e) {
System.out.println(e.getMessage());
} finally {
if (p.isAlive()) {
p.destroy();
}
}
} catch (IOException e) {
System.out.println(e.getMessage());
}
return isFinished;
}
まとめ
Javaで外部プロセスを実行する場合はProcessBuilderクラスを利用します。
外部プロセスの標準出力、標準エラーの出力先をJavaの標準出力へ統合するか、個別のlogファイルに出力するようにして、外部プロセスが標準出力、標準エラーを出力できるようにします。
ProcessBuilder builder = new ProcessBuilder(cmdArray);
// まずは標準エラーを標準出力へ統合する
builder.redirectErrorStream(true);
// ファイルへ出力する場合はログファイルを指定する
File log = new File("command.log");
builder.redirectOutput(log);
ProcessBuilder builder = new ProcessBuilder(cmdArray);
// まずは標準エラーを標準出力へ統合する
builder.redirectErrorStream(true);
// Javaの標準出力へ統合する場合
builder.inheritIO();
以上