Как выполнить shell-команду из Java
Системные вызовы из Java часто приводят к зависаниям, потерям вывода и проблемам на разных ОС: ошибки при работе с потоками stdin/stdout/stderr и игнорирование кодов возврата ломают автоматизацию. В статье даны сравнение Runtime.exec и ProcessBuilder, показаны приёмы работы с потоками, обработка ошибок и нюансы кроссплатформенности.
Вы получите алгоритм безопасного запуска и контроля процесса: чтение/запись потоков, корректное закрытие ресурсов и проверка кодов возврата. В результате сможете надёжно интегрировать shell‑вызовы в серверное приложение и избежать утечек и блокировок.
Зависимость операционной системы
Сначала нужно определить операционную систему, на которой работает наша JVM . В Windows необходимо запустить команду в качестве аргумента оболочки cmd.exe, а в остальных ОС мы будем использовать стандартную оболочку sh:
boolean isWindows = System.getProperty("os.name")
.toLowerCase().startsWith("windows");Ввод и вывод
Также нужно подключиться к входным и выходным потокам нашего процесса. По крайней мере, нужно получить выходные данные, иначе процесс зависнет.
Реализуем класс StreamGobbler, который использует InputStream:
private static class StreamGobbler implements Runnable {
private InputStream inputStream;
private Consumer<String> consumer;
public StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}
@Override
public void run() {
new BufferedReader(new InputStreamReader(inputStream)).lines()
.forEach(consumer);
}
}Примечание. Этот класс реализует интерфейс Runnable, а это означает, что он может быть выполнен любым исполнителем.
Runtime.exec()
Метод Runtime.exec() - это простой, но недостаточно гибкий способ создания нового подпроцесса.
В следующем примере мы запросим список пользователей из локальной директории и выведем его в консоли:
String homeDirectory = System.getProperty("user.home");
Process process;
if (isWindows) {
process = Runtime.getRuntime()
.exec(String.format("cmd.exe /c dir %s", homeDirectory));
} else {
process = Runtime.getRuntime()
.exec(String.format("sh -c ls %s", homeDirectory));
}
StreamGobbler streamGobbler =
new StreamGobbler(process.getInputStream(), System.out::println);
Executors.newSingleThreadExecutor().submit(streamGobbler);
int exitCode = process.waitFor();
assert exitCode == 0;ProcessBuilder
Класс ProcessBuilder является более гибким в использовании, чем Runtime. Он позволяет настраивать целый ряд параметров.
Например, можно:
- изменить рабочий каталог, в котором работает shell-команда,
- перенаправить потоки ввода и вывода;
- наследовать их в потоках текущего процесса JVM, используя builder.inheritIO().
ProcessBuilder builder = new ProcessBuilder();
if (isWindows) {
builder.command("cmd.exe", "/c", "dir");
} else {
builder.command("sh", "-c", "ls");
}
builder.directory(new File(System.getProperty("user.home")));
Process process = builder.start();
StreamGobbler streamGobbler =
new StreamGobbler(process.getInputStream(), System.out::println);
Executors.newSingleThreadExecutor().submit(streamGobbler);
int exitCode = process.waitFor();
assert exitCode == 0;
Заключение
Shell команды в Java можно выполнять двумя различными способами. Но если нужно настроить выполнение созданного процесса, то используйте класс ProcessBuilder.
Программные исходники примеров, приведенных в этой статье, доступны на GitHub .
Комментарии