1. 概要

Process API は、Javaでオペレーティングシステムコマンドを実行するための強力な方法を提供します。 ただし、操作が面倒になる可能性のあるいくつかのオプションがあります。

このチュートリアルでは、JavaがProcessBuilderAPIを使用してそれをどのように軽減するかを見ていきます。

2. ProcessBuilder API

ProcessBuilder クラスは、オペレーティングシステムプロセスを作成および構成するためのメソッドを提供します。 各ProcessBuilderインスタンスを使用すると、プロセス属性のコレクションを管理できます。 次に、これらの指定された属性を使用して、新しいProcessを開始できます。

このAPIを使用できる一般的なシナリオを次に示します。

  • 現在のJavaバージョンを検索する
  • 環境のカスタムキー値マップを設定します
  • シェルコマンドが実行されている場所の作業ディレクトリを変更します
  • 入力ストリームと出力ストリームをカスタム置換にリダイレクトします
  • 現在のJVMプロセスの両方のストリームを継承します
  • Javaコードからシェルコマンドを実行する

これらのそれぞれの実際的な例については、後のセクションで説明します。

ただし、実際のコードに飛び込む前に、このAPIが提供する機能の種類を見てみましょう。

2.1. メソッドの概要

このセクションでは、一歩下がって、ProcessBuilderクラスの最も重要なメソッドについて簡単に説明します。 これは、後でいくつかの実際の例に飛び込むときに役立ちます。

  • ProcessBuilder(String... command)

    指定されたオペレーティングシステムプログラムと引数を使用して新しいプロセスビルダーを作成するには、この便利なコンストラクターを使用できます。

  • directory(File directory)

    directory メソッドを呼び出し、 File オブジェクトを渡すことにより、現在のプロセスのデフォルトの作業ディレクトリをオーバーライドできます。 デフォルトでは、現在の作業ディレクトリは、user.dirシステムプロパティによって返される値に設定されます。

  • environment()

    現在の環境変数を取得したい場合は、単に 環境 方法。 それは私たちにのコピーを返します を使用した現在のプロセス環境 System.getenv() しかし、 地図.

  • inheritIO()

    サブプロセス標準I/Oのソースと宛先を現在のJavaプロセスと同じにするように指定する場合は、inheritIOメソッドを使用できます。

  • redirectInput(File file), redirectOutput(File file), redirectError(File file)

    プロセスビルダーの標準の入力、出力、およびエラーの宛先をファイルにリダイレクトする場合、これら3つの同様のリダイレクト方法を自由に使用できます。

  • start()

    最後になりましたが、構成したもので新しいプロセスを開始するには、 start()を呼び出すだけです。

このクラスは同期されていないことに注意してください。 たとえば、 ProcessBuilder インスタンスに同時にアクセスする複数のスレッドがある場合、同期は外部で管理する必要があります。

3. 例

ProcessBuilder APIの基本を理解したので、いくつかの例を見ていきましょう。

3.1. ProcessBuilderを使用してJavaのバージョンを印刷する

この最初の例では、バージョンを取得するために、1つの引数を指定してjavaコマンドを実行します。

Process process = new ProcessBuilder("java", "-version").start();

まず、 ProcessBuilder オブジェクトを作成して、コマンドと引数の値をコンストラクターに渡します。 次に、 start()メソッドを使用してプロセスを開始し、Processオブジェクトを取得します。

次に、出力を処理する方法を見てみましょう。

List<String> results = readOutput(process.getInputStream());

assertThat("Results should not be empty", results, is(not(empty())));
assertThat("Results should contain java version: ", results, hasItem(containsString("java version")));

int exitCode = process.waitFor();
assertEquals("No errors should be detected", 0, exitCode);

ここでは、プロセス出力を読み取り、内容が期待どおりであることを確認しています。 最後のステップでは、 process.waitFor()を使用してプロセスが終了するのを待ちます。

プロセスが終了すると、戻り値はプロセスが成功したかどうかを示します

覚えておくべきいくつかの重要なポイント:

  • 引数は正しい順序である必要があります
  • さらに、この例では、デフォルトの作業ディレクトリと環境が使用されています
  • 出力バッファがプロセスを停止させる可能性があるため、出力を読み取るまで、意図的に process.waitFor()を呼び出さないでください。
  • javaコマンドはPATH変数を介して使用可能であると想定しています。

3.2. 変更された環境でプロセスを開始する

この次の例では、作業環境を変更する方法を見ていきます。

しかし、その前に、デフォルト環境で見つけることができる情報の種類を見てみましょう

ProcessBuilder processBuilder = new ProcessBuilder();        
Map<String, String> environment = processBuilder.environment();
environment.forEach((key, value) -> System.out.println(key + value));

これにより、デフォルトで提供される各変数エントリが出力されます。

PATH/usr/bin:/bin:/usr/sbin:/sbin
SHELL/bin/bash
...

次に、 ProcessBuilder オブジェクトに新しい環境変数を追加し、コマンドを実行してその値を出力します。

environment.put("GREETING", "Hola Mundo");

processBuilder.command("/bin/bash", "-c", "echo $GREETING");
Process process = processBuilder.start();

手順を分解して、これまでに行ったことを理解しましょう。

  • 標準である「HolaMundo」の値を持つ「GREETING」という変数を環境に追加します地図
  • 今回は、コンストラクターを使用するのではなく、 command(String…command)メソッドを介してコマンドと引数を直接設定しました。
  • 次に、前の例のようにプロセスを開始します。

例を完了するために、出力に挨拶が含まれていることを確認します。

List<String> results = readOutput(process.getInputStream());
assertThat("Results should not be empty", results, is(not(empty())));
assertThat("Results should contain java version: ", results, hasItem(containsString("Hola Mundo")));

3.3. 変更された作業ディレクトリを使用してプロセスを開始する

作業ディレクトリを変更すると便利な場合があります。 次の例では、それを行う方法を見ていきます。

@Test
public void givenProcessBuilder_whenModifyWorkingDir_thenSuccess() 
  throws IOException, InterruptedException {
    ProcessBuilder processBuilder = new ProcessBuilder("/bin/sh", "-c", "ls");

    processBuilder.directory(new File("src"));
    Process process = processBuilder.start();

    List<String> results = readOutput(process.getInputStream());
    assertThat("Results should not be empty", results, is(not(empty())));
    assertThat("Results should contain directory listing: ", results, contains("main", "test"));

    int exitCode = process.waitFor();
    assertEquals("No errors should be detected", 0, exitCode);
}

上記の例では、コンビニエンスメソッドディレクトリ(ファイルディレクトリ)を使用して、作業ディレクトリをプロジェクトのsrcディレクトリに設定します。 次に、単純なディレクトリリストコマンドを実行し、出力にサブディレクトリmainおよびtestが含まれていることを確認します。

3.4. 標準の入力と出力のリダイレクト

現実の世界では、実行中のプロセスの結果をログファイルに記録してさらに分析したいと思うでしょう。 幸い、 ProcessBuilder APIには、この例で示すように、まさにこれに対する組み込みのサポートがあります。

デフォルトでは、プロセスはパイプから入力を読み取ります。 Process.getOutputStream()によって返される出力ストリームを介してこのパイプにアクセスできます

ただし、後で説明するように、 redirectOutput メソッドを使用して、標準出力がファイルなどの別のソースにリダイレクトされる場合があります。 この場合、getOutputStream()ProcessBuilder.NullOutputStream。を返します。

元の例に戻って、Javaのバージョンを印刷してみましょう。 ただし、今回は、出力を標準の出力パイプではなくログファイルにリダイレクトしましょう。

ProcessBuilder processBuilder = new ProcessBuilder("java", "-version");

processBuilder.redirectErrorStream(true);
File log = folder.newFile("java-version.log");
processBuilder.redirectOutput(log);

Process process = processBuilder.start();

上記の例では、 logという新しい一時ファイルを作成し、ProcessBuilderに出力をこのファイルの宛先にリダイレクトするように指示します。

この最後のスニペットでは、 getInputStream()が実際に null であり、ファイルの内容が期待どおりであることを確認するだけです。

assertEquals("If redirected, should be -1 ", -1, process.getInputStream().read());
List<String> lines = Files.lines(log.toPath()).collect(Collectors.toList());
assertThat("Results should contain java version: ", lines, hasItem(containsString("java version")));

次に、この例のわずかなバリエーションを見てみましょう。 たとえば、毎回新しいログファイルを作成するのではなく、ログファイルに追加したい場合

File log = tempFolder.newFile("java-version-append.log");
processBuilder.redirectErrorStream(true);
processBuilder.redirectOutput(Redirect.appendTo(log));

redirectErrorStream(true)の呼び出しについて言及することも重要です。エラーが発生した場合、エラー出力は通常のプロセス出力ファイルにマージされます。

もちろん、標準出力と標準エラー出力に個別のファイルを指定することもできます。

File outputLog = tempFolder.newFile("standard-output.log");
File errorLog = tempFolder.newFile("error.log");

processBuilder.redirectOutput(Redirect.appendTo(outputLog));
processBuilder.redirectError(Redirect.appendTo(errorLog));

3.5. 現在のプロセスのI/Oを継承する

この最後から2番目の例では、 inheritIO()メソッドが動作していることがわかります。 サブプロセスI/Oを現在のプロセスの標準I/Oにリダイレクトする場合は、この方法を使用できます。

@Test
public void givenProcessBuilder_whenInheritIO_thenSuccess() throws IOException, InterruptedException {
    ProcessBuilder processBuilder = new ProcessBuilder("/bin/sh", "-c", "echo hello");

    processBuilder.inheritIO();
    Process process = processBuilder.start();

    int exitCode = process.waitFor();
    assertEquals("No errors should be detected", 0, exitCode);
}

上記の例では、 inheritIO()メソッドを使用すると、IDEのコンソールに簡単なコマンドの出力が表示されます。

次のセクションでは、Java9でProcessBuilderAPIにどのような追加が行われたかを見ていきます。

4. Java9の追加

Java 9は、パイプラインの概念を ProcessBuilderAPIに導入しました。

public static List<Process> startPipeline​(List<ProcessBuilder> builders)

startPipeline メソッドを使用して、ProcessBuilderオブジェクトのリストを渡すことができます。 この静的メソッドは、ProcessBuilderごとにProcessを開始します。 したがって、標準出力ストリームと標準入力ストリームによってリンクされるプロセスのパイプラインを作成します。

たとえば、次のようなものを実行する場合:

find . -name *.java -type f | wc -l

分離されたコマンドごとにプロセスビルダーを作成し、それらをパイプラインに構成します。

@Test
public void givenProcessBuilder_whenStartingPipeline_thenSuccess()
  throws IOException, InterruptedException {
    List builders = Arrays.asList(
      new ProcessBuilder("find", "src", "-name", "*.java", "-type", "f"), 
      new ProcessBuilder("wc", "-l"));

    List processes = ProcessBuilder.startPipeline(builders);
    Process last = processes.get(processes.size() - 1);

    List output = readOutput(last.getInputStream());
    assertThat("Results should not be empty", output, is(not(empty())));
}

この例では、 src ディレクトリ内のすべてのjavaファイルを検索し、結果を別のプロセスにパイプしてカウントします。

Java9でProcessAPIに加えられたその他の改善については、 Java 9ProcessAPIの改善に関するすばらしい記事をご覧ください。

5. 結論

要約すると、このチュートリアルでは、 java.lang.ProcessBuilderAPIについて詳しく説明しました。

まず、APIで何ができるかを説明し、最も重要な方法を要約しました。

次に、いくつかの実際的な例を見てみました。 最後に、Java9のAPIにどのような新しい追加が導入されたかを確認しました。

いつものように、記事の完全なソースコードは、GitHubから入手できます。