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 .