Перейти к основному содержимому

Руководство по API java.lang.ProcessBuilder

· 8 мин. чтения

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 directory)

Мы можем переопределить рабочий каталог по умолчанию для текущего процесса, вызвав метод каталога и передав объект File . По умолчанию в качестве текущего рабочего каталога используется значение, возвращаемое системным свойством user.dir .

environment()

Если мы хотим получить текущие переменные среды, мы можем просто вызвать метод среды . Он возвращает нам копию текущей среды процесса, используя System.getenv() , но как Map .

inheritIO()

Если мы хотим указать, что источник и место назначения для стандартного ввода-вывода нашего подпроцесса должны быть такими же, как у текущего процесса Java, мы можем использовать метод inheritIO .

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

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

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

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

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

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

По умолчанию наш процесс считывает ввод из канала. Мы можем получить доступ к этому каналу через выходной поток, возвращаемый 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. Наследование ввода/вывода текущего процесса

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

@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.

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

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

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

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

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

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

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, ознакомьтесь с нашей отличной статьей об улучшениях в Java 9 Process API .

5. Вывод

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

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

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

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