java.lang.ProcessBuilder APIのガイド

1. 概要

link:/java-process-api[Process API]は、Javaでオペレーティングシステムコマンドを実行する強力な方法を提供します。 ただし、操作が面倒になるいくつかのオプションがあります。
このチュートリアルでは、Javaが_ProcessBuilder_ APIを使用してそれをどのように緩和するかを見ていきます。*

*2. ProcessBuilder API *

_ProcessBuilder_クラスは、オペレーティングシステムプロセスを作成および構成するためのメソッドを提供します。 *各_ProcessBuilder_インスタンスにより、プロセス属性のコレクションを管理できます*。 次に、指定された属性を使用して新しい_Process_を開始できます。
このAPIを使用できる一般的なシナリオを次に示します。
  • 現在のJavaバージョンを見つける

  • 環境に合わせてカスタムKey-Valueマップを設定します

  • シェルコマンドが実行されている作業ディレクトリを変更します

  • 入力および出力ストリームをカスタム置換にリダイレクトします

  • 現在の「JVM」プロセスの両方のストリームを継承する

  • link:/run-shell-command-in-java [シェルを実行
    Javaコードからのコマンド]

    これらについては、後のセクションで実際の例を見ていきます。
    しかし、作業コードに飛び込む前に、このAPIが提供する機能の種類を見てみましょう。

* 2.1。 メソッドの概要*

このセクションでは、*一歩戻って、_ProcessBuilder_クラスの最も重要なメソッドを簡単に見ていきます*。 これは、後で実際の例に飛び込むときに役立ちます。
  • [ソース、java、gutter:、false]

ProcessBuilder(String... command)
 指定されたオペレーティングシステムプログラムと引数を使用して新しいプロセスビルダーを作成するには、この便利なコンストラクターを使用できます。
*  [ソース、java、gutter:、false]
directory(File directory)
 + _directory_メソッドを呼び出して_File_オブジェクトを渡すことにより、現在のプロセスのデフォルトの作業ディレクトリをオーバーライドできます。 *デフォルトでは、現在の作業ディレクトリは_user.dir_システムプロパティによって返される値に設定されます*。
*  [ソース、java、gutter:、false]
environment()
 _environmentSystem.getenv()Map_
*  [ソース、java、gutter:、false]
inheritIO()
 +サブプロセスの標準I / Oのソースと宛先を現在のJavaプロセスのソースと宛先に指定する場合は、_inheritIO_メソッドを使用できます。
*  [ソース、java、gutter:、false]
redirectInput(File file), redirectOutput(File file), redirectError(File file)
 +プロセスビルダーの標準入力、出力、およびエラーの宛先をファイルにリダイレクトする場合、これら3つの同様のリダイレクトメソッドを自由に使用できます。
*  [ソース、java、gutter:、false]
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();
手順を分解して、何をしたかを理解しましょう。
  • 「GREETING」という変数を「Hola Mundo」という値で追加します
    標準である環境_Map <String、String> _

  • 今回は、コンストラクタを使用するのではなく、コマンドと
    _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);
}
*上の例では、便利なメソッド_directory(File directory)_ *を使用して、作業ディレクトリをプロジェクトの_src_ dirに設定します。 次に、単純なディレクトリ一覧コマンドを実行し、出力にサブディレクトリ_main_と_test_が含まれていることを確認します。

* 3.4。 標準入出力のリダイレクト*

*現実の世界では、実行中のプロセスの結果をログファイルにキャプチャして、さらに分析することをお勧めします*。 幸いなことに、_ProcessBuilder_ APIには、この例で示すように、まさにこれに対する組み込みのサポートがあります。
*デフォルトでは、プロセスはパイプから入力を読み取ります。 _Process.getOutputStream()_ *によって返される出力ストリームを介してこのパイプにアクセスできます。
ただし、すぐにわかるように、標準出力はmethod__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のコンソールに単純なコマンドの出力が表示されます。
次のセクションでは、Java 9の_ProcessBuilder_ APIにどのような追加が行われたかを見ていきます。

4. Java 9の追加

  • Java 9は、ProcessBuilder APIにパイプラインの概念を導入しました:*

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ファイルを検索し、結果を別のプロセスにパイプしてカウントします。*
Java 9のProcess APIに加えられたその他の改善点については、https://www.baeldung.com/java-9-process-api [Java 9 Process APIの改善]にある素晴らしい記事をご覧ください。

5. 結論

要約すると、このチュートリアルでは、_java.lang.ProcessBuilder_ APIを詳細に調査しました。
最初に、APIで何ができるかを説明することから始め、最も重要なメソッドをまとめました。
次に、いくつかの実用的な例を見てみました。 最後に、Java 9のAPIに新たに追加された機能を調べました。
いつものように、記事の完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-os[GitHub上]で入手できます。