«1. Обзор

Process API предоставляет мощный способ выполнения команд операционной системы на языке Java. Однако у него есть несколько опций, которые могут затруднить работу с ним.

В этом руководстве мы рассмотрим, как Java облегчает это с помощью API ProcessBuilder.

2. API ProcessBuilder

Класс ProcessBuilder предоставляет методы для создания и настройки процессов операционной системы. Каждый экземпляр ProcessBuilder позволяет нам управлять набором атрибутов процесса. Затем мы можем начать новый процесс с этими заданными атрибутами.

Вот несколько распространенных сценариев, в которых мы могли бы использовать этот API:

    Найти текущую версию Java. Настроить пользовательскую карту значений ключей для нашей среды. потоки вывода для пользовательских замен Наследовать оба потока текущего процесса JVM Выполнить команду оболочки из кода Java

Мы рассмотрим практические примеры для каждого из них в следующих разделах.

Но прежде чем мы углубимся в работающий код, давайте посмотрим, какие функции предоставляет этот API.

2.1. Резюме методов

В этом разделе мы сделаем шаг назад и кратко рассмотрим наиболее важные методы класса ProcessBuilder. Это поможет нам позже, когда мы будем углубляться в некоторые реальные примеры:

    ProcessBuilder(String… command) Чтобы создать новый построитель процессов с указанной программой операционной системы и аргументами, мы можем использовать этот удобный конструктор. directory (Каталог файлов) Мы можем переопределить рабочий каталог текущего процесса по умолчанию, вызвав метод каталога и передав объект File. По умолчанию в качестве текущего рабочего каталога используется значение, возвращаемое системным свойством user.dir. environment() Если мы хотим получить текущие переменные среды, мы можем просто вызвать метод среды. Он возвращает нам копию текущей среды процесса, используя System.getenv(), но как карту. inheritIO() Если мы хотим указать, что источник и место назначения для стандартного ввода-вывода нашего подпроцесса должны быть такими же, как у текущего процесса Java, мы можем использовать метод inheritIO. redirectInput(Файл-файл), redirectOutput(Файл-файл), redirectError(Файл-файл) Когда мы хотим перенаправить стандартный ввод, вывод и место назначения ошибок построителя процессов в файл, в нашем распоряжении есть эти три похожих метода перенаправления. start() И последнее, но не менее важное: чтобы начать новый процесс с тем, что мы настроили, мы просто вызываем start().

Следует отметить, что этот класс НЕ синхронизирован. Например, если у нас есть несколько потоков, одновременно обращающихся к экземпляру ProcessBuilder, тогда синхронизация должна управляться извне.

3. Примеры

Теперь, когда у нас есть общее представление об API ProcessBuilder, давайте рассмотрим несколько примеров.

3.1. Использование ProcessBuilder для печати версии Java

В этом первом примере мы запустим команду 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\u003cString, String\u003e. На этот раз вместо использования конструктора мы устанавливаем команду и аргументы через command(String… command) напрямую. Затем мы начинаем наш процесс, как в предыдущем примере.

    Чтобы завершить пример, мы проверяем, что вывод содержит наше приветствие:

3.3. Запуск процесса с измененным рабочим каталогом

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")));

Иногда бывает полезно изменить рабочий каталог. В нашем следующем примере мы увидим, как именно это сделать:

В приведенном выше примере мы установили рабочий каталог в каталог src проекта, используя каталог удобного метода (каталог файлов). Затем мы запускаем простую команду списка каталогов и проверяем, что выходные данные содержат подкаталоги main и test.

@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);
}

3.4. Перенаправление стандартного ввода и вывода

В реальном мире мы, вероятно, захотим зафиксировать результаты наших запущенных процессов в файле журнала для дальнейшего анализа. К счастью, API ProcessBuilder имеет встроенную поддержку именно для этого, как мы увидим в этом примере.

По умолчанию наш процесс считывает ввод из канала. Мы можем получить доступ к этому каналу через выходной поток, возвращаемый Process.getOutputStream().

Однако, как мы вскоре увидим, стандартный вывод может быть перенаправлен на другой источник, например файл, с помощью метода redirectOutput. В этом случае getOutputStream() вернет ProcessBuilder.NullOutputStream.

Вернемся к нашему исходному примеру, чтобы распечатать версию Java. Но на этот раз давайте перенаправим вывод в файл журнала вместо стандартного канала вывода:

В приведенном выше примере мы создаем новый временный файл с именем log и сообщаем нашему ProcessBuilder перенаправить вывод в этот файл назначения.

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

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

Process process = processBuilder.start();

В этом последнем фрагменте мы просто проверяем, что 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")));

Также важно упомянуть вызов redirectErrorStream(true). В случае каких-либо ошибок вывод ошибки будет объединен с файлом вывода обычного процесса.

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

Мы можем, конечно, указать отдельные файлы для стандартного вывода и стандартного вывода ошибок:

3.5. Наследование ввода-вывода текущего процесса

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

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

В этом предпоследнем примере мы увидим метод inheritIO() в действии. Мы можем использовать этот метод, когда хотим перенаправить ввод-вывод подпроцесса на стандартный ввод-вывод текущего процесса:

В приведенном выше примере с помощью метода inheritIO() мы видим вывод простой команды в консоли в нашей IDE.

@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);
}

В следующем разделе мы рассмотрим, какие дополнения были внесены в API ProcessBuilder в Java 9.

4. Дополнения Java 9

Java 9 представила концепцию конвейеров для ProcessBuilder API:

С помощью метода startPipeline мы можем передать список объектов ProcessBuilder. Затем этот статический метод запускает процесс для каждого ProcessBuilder. Таким образом, создается конвейер процессов, которые связаны своими стандартными выходными и стандартными входными потоками.

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

Например, если мы хотим запустить что-то вроде этого:

Что мы должны сделать, так это создать построитель процессов для каждой изолированной команды и скомпоновать их в конвейер:

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())));
}

«В этом примере мы ищем все java-файлы в каталоге src и передаем результаты другому процессу для их подсчета.

Чтобы узнать о других улучшениях, внесенных в Process API в Java 9, ознакомьтесь с нашей замечательной статьей об улучшениях Process API в Java 9.

5. Заключение

Подводя итог, в этом руководстве мы подробно изучили API java.lang.ProcessBuilder.

Во-первых, мы начали с объяснения того, что можно сделать с помощью API, и суммировали наиболее важные методы.

Далее мы рассмотрели ряд практических примеров. Наконец, мы рассмотрели, какие новые дополнения были введены в API в Java 9.

Как всегда, полный исходный код статьи доступен на GitHub.