Процессы. Process и ProcessBuilder

Последнее обновление: 29.10.2025

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

Создание процесса

Для создания и настройки объекта Process применяется класс ProcessBuilder. Этот класс имеет два похожих конструктора

ProcessBuilder(String... command)
ProcessBuilder(List<String> command)

Оба этих конструктора принимают путь к запускаемой программе и набор ее аргументов в виде строк. Например:

var builder = new ProcessBuilder("java", "--version");

Первый аргумент - "java" представляет имя приложения, которае будет запускаться в процессе. Второй параметр - "--version" - аргумент, который передается приложению. То есть в данном случае будем запускать команду "java --version", пытаясь получить текущую версию Java.

Запуск процесса

Для запуска процесса в классе ProcessBuilder определен метод start()

Process start()

Этот метод возвращает объект запущенного процесса. Например:

class Program{
 
    public static void main(String[] args) throws Exception{
          
        var builder = new ProcessBuilder("java", "--version");
        Process javac = builder.start();
    }
}

Однако после компиляции и запуска программы мы ничего не увидим. Для этого нам надо дополнительно выполнить некоторую настройку.

Настройка создания процесса

С помощью ряда методов ProcessBuilder можно настроить создание и выполнение процесса. Все эти методы возвращают объект ProcessBuilder:

  • ProcessBuilder command(String... command) / command(List command): устанавливает запускаемую программу и аргументы для ее запускав.

  • ProcessBuilder directory(File directory): устанавливает рабочий каталог для построителя процессов.

  • ProcessBuilder inheritIO(): устанавливает источник ввода и назначение вывода подпроцесса такими же, как у текущего процесса Java.

  • ProcessBuilder redirectError(File file): устанавливает файл для вывода ошибок.

  • ProcessBuilder redirectError(ProcessBuilder.Redirect destination): устанавливает стандартный вывод ошибок.

  • ProcessBuilder redirectInput(File file): устанавливает файл в качестве стандартного источника ввода.

  • ProcessBuilder redirectInput(ProcessBuilder.Redirect source): устанавливает источник стандартного ввода.

  • ProcessBuilder redirectOutput(File file): устанавливает файл в качестве стандартного вывода.

  • ProcessBuilder redirectOutput(ProcessBuilder.Redirect destination): устанавливает место вывода.

  • ProcessBuilder redirectErrorStream(boolean redirectErrorStream): устанавливает свойство redirectErrorStream - если оно равно true, то стандартный вывод и вывод ошибок производятся в одно место.

  • Здесь надо отметить следующие важные аспекты:

    • Вывод: куда будет идти вывод запускаемого процесса. Задается методом redirectOutput()

    • Ввод: откуда будет идти ввод данных для запускаемого процесса. Задается методом redirectInput()

    • Вывод ошибок: куда будет идти вывод ошибок процесса. Задается методом redirectError() и redirectErrorStream()

    Мы также можем получить устанавливаемые параметры с помощью одноименных методов:

    • List<String>> command()

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

    • File directory()

      Возвращает рабочий каталог для построителя процессов.

    • boolean redirectErrorStream(): Указывает, объединяет ли этот построитель процессов стандартный вывод ошибок и стандартный вывод.

    • ProcessBuilder.Redirect redirectError(): Возвращает стандартный приёмник ошибок этого построителя процессов.

    • ProcessBuilder.Redirect redirectInput(): Возвращает стандартный источник ввода.

    • ProcessBuilder.Redirect redirectOutput(): Возвращает стандартный вывод.

    Можете указать, что потоки ввода, вывода и ошибок нового процесса должны совпадать с потоками JVM. Если пользователь запускает JVM в консоли, любой пользовательский ввод перенаправляется в процесс, а вывод процесса отображается в консоли. Для этого применяется метод inheritIO(). Например, наша основная программа запускается в консоли, и мы хотим, чтобы запускаемый процесс также выводил данные на консоль:

    class Program{
     
        public static void main(String[] args) throws Exception{
              
            var builder = new ProcessBuilder("java", "--version");
            Process javac = builder
                                .inheritIO()    // запускаемый процесс наследует параметры ввода/вывода текущего процесса
                                .start();       // запускаем процесс
        }
    }
    

    В итоге при запуске мы увидим версию Java - по сути вывод запущенного подпроцесса:

    eugene@Eugene:/workspace/java/hello$ javac Program.java && java Program
    java 25 2025-09-16 LTS
    Java(TM) SE Runtime Environment (build 25+37-LTS-3491)
    Java HotSpot(TM) 64-Bit Server VM (build 25+37-LTS-3491, mixed mode, sharing)
    

    Также можно по отдельности направить потоки подпроцесса на текущие процессы JVM. Для этого используется константа ProcessBuilder.Redirect.INHERIT

    class Program{
     
        public static void main(String[] args) throws Exception{
              
            var builder = new ProcessBuilder("java", "--version");
            builder
                .redirectOutput(ProcessBuilder.Redirect.INHERIT)
                .start();
        }
    }
    

    Еще один распространенный способ вывода - вывод в файл. Для этого в метод redirectOutput() передается объект File:

    import java.nio.file.Path;
    
    class Program{
     
        public static void main(String[] args) throws Exception{
              
            var builder = new ProcessBuilder("java", "--version");
            builder
                .redirectOutput(Path.of("output.txt").toFile())
                .start();
        }
    }
    

    Здесь для создания пути применяется встроенный интерфейс java.nio.file.Path. С помощью статического метода Path.of() определяем путь к файлу. В данном случае будет использоваться файл "output.txt" из текущего каталога (если такого файла нет, то он создается). А метод .toFile() преобразует путь Path в объект File. В итоге вывод команды "java --version" будет записан в файл "output.txt"

    Нередо потоки вывода и ошибок пишут свои данные в один поток. Чтобы объединить потоки вывода и ошибок можно выполнить вызов

    builder.redirectErrorStream(true);

    Рабочий каталог

    У каждого процесса есть рабочий каталог (working directory), который используется для вычисления относительных имен каталогов. По умолчанию рабочий каталог процесса совпадает с рабочим каталогом виртуальной машины - обычно это каталог текущей программы на Java. С помощью метода directory() можно получить данный каталог в виде объекта File:

    var builder = new ProcessBuilder("java", "--version");
    System.out.println(builder.directory());  // null - текущий каталог
    

    Если метод возвращает null, то в качестве рабочего каталога установлен каталог текуй программы на Java.

    Но также можно изменить рабочий каталог с помощью одноименного метода directory(), который принимает объенкт File.

    Например, создадим в текущей папке программы новый каталог с именем "test". А в этом каталоге определим файл с именем "Main.java" и следующим содержимым:

    class Main{
     
        public static void main(String[] args) throws Exception{
              
            System.out.println("Hello METANIT.COM");
        }
    }
    

    То есть файл Main.java из папки "test" содержит простейшую программу на Java. И допустим, мы хотим, чтобы наша программа запускала на выполнение этот файл. Для этого определеим следующую программу:

    import java.nio.file.Path;
    
    class Program{
     
        public static void main(String[] args) throws Exception{
              
            var builder = new ProcessBuilder("java", "Main.java");
            builder
                .directory(Path.of("test").toFile())                // устанавливаем рабочий каталог на подкаталог "test"
                .redirectOutput(ProcessBuilder.Redirect.INHERIT)    // устанавливаем вывод на консоль
                .redirectErrorStream(true)                          // вывод ошибок также идет на консоль
                .start();
        }
    }
    

    То есть фактически наша программу будет запускать команду "java Main.java". И поскольку мы установили в качестве рабочего каталога подпадку "test", то процесс сможет найти файл "Main.java". Запустим данную программу, и в отдельном подпроцессе будет выполняться команда java Main.java:

    eugene@Eugene:/workspace/java$ java Program.java
    Hello METANIT.COM
    eugene@Eugene:/workspace/java$ 
    

    Управление процессами

    Класс Process предоставляет ряд методов для управления процессом:

    • abstract InputStream getErrorStream()

      Возвращает входной поток, подключенный к потоку ошибок процесса.

    • abstract InputStream getInputStream()

      Возвращает поток ввода, подключенный к обычному выводу процесса.

    • abstract OutputStream getOutputStream()

      Возвращает поток вывода, подключенный к обычному вводу процесса.

    • final BufferedReader errorReader()

      Возвращает объект BufferedReader, подключенный к стандартному потоку ошибок процесса.

    • final BufferedReader inputReader()

      Возвращает объект BufferedReader, подключенный к стандартному выводу процесса.

    • final BufferedWriter outputWriter()

      Возвращает объект BufferedWriter, подключенный к обычному входу процесса, используя собственную кодировку.

    • Stream<ProcessHandle> children()

      Возвращает поток прямых дочерних процессов процесса.

    • Stream<ProcessHandle> descendants()

      Возвращает поток потомков процесса.

    • abstract void destroy()

      Завершает процесс.

    • Process destroyForcibly()

      Принудительно завершает процесс.

    • boolean isAlive()

      Проверяет, активен ли запущенный процесс.

    • long pid()

      Возвращает идентификатор запущенного процесса.

    • ProcessHandle toHandle()

      Возвращает объект ProcessHandle для процесса.

    • CompletableFuture onExit()

      Возвращает объект CompletableFuture для завершения процесса.

    • boolean supportsNormalTermination()

      Возвращает true, если метод destroy() завершает процесс нормально. Возвращает false, если метод destroy завершает процесс принудительно и немедленно.

    • abstract int waitFor()

      При необходимости заставляет текущий поток ожидать завершения запущенного процесса. Модификации метода позволяют передать период ожидания до завершения процесса

      boolean waitFor(long timeout, TimeUnit unit)

      boolean waitFor(Duration duration)

    Получение потоков ввода-вывода процесса

    Ряд методов позволяют получить потоки ввода-вывода процесса:

    • abstract InputStream getErrorStream()

      Возвращает входной поток, подключенный к потоку ошибок процесса.

    • abstract InputStream getInputStream()

      Возвращает поток ввода, подключенный к обычному выводу процесса.

    • abstract OutputStream getOutputStream()

      Возвращает поток вывода, подключенный к обычному вводу процесса.

    • final BufferedReader errorReader()

      Возвращает объект BufferedReader, подключенный к стандартному потоку ошибок процесса.

    • final BufferedReader inputReader()

      Возвращает объект BufferedReader, подключенный к стандартному выводу процесса.

    • final BufferedWriter outputWriter()

      Возвращает объект BufferedWriter, подключенный к обычному входу процесса, используя собственную кодировку.

    Конечно, мы можем в целом перенаправить поток вывода или ошибок в заданное место (например, в файл или на консоль). Однако потоки ввода-вывода позволяют вручную настроить взаимодействие с потоком. Например:

    import java.util.Scanner;
    
    class Program{
     
        public static void main(String[] args) throws Exception{
              
            var builder = new ProcessBuilder("java", "--version");
            Process process = builder.start();
            try (var in = new Scanner(process.getInputStream()))
            {
                int i = 0;
                // проходим по каждой строке из потока
                while (in.hasNextLine()){
                    // при выводе на консоль добавляем перед строкой ее номер
                    System.out.println(++i + ". " + in.nextLine());
                }
            }
        }
    }
    

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

    1. java 25 2025-09-16 LTS
    2. Java(TM) SE Runtime Environment (build 25+37-LTS-3491)
    3. Java HotSpot(TM) 64-Bit Server VM (build 25+37-LTS-3491, mixed mode, sharing)
    

    Аналогично можно было бы получить поток ввода с помощью другого метода:

    class Program{
     
        public static void main(String[] args) throws Exception{
              
            var builder = new ProcessBuilder("java", "--version");
            Process process = builder.start();
            try (var in = process.inputReader();)
            {
                int i = 0;
                String s;
                // считываем построчно
                while ((s = in.readLine())!=null){
                    System.out.println(++i + ". " + s);
                }
            }
        }
    }
    

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

    Завершение процесса

    destroy и destroyForcibly

    Для завершения процесса применяются методы destroy() либо destroyForcibly().

    process.destroy();
    // или
    process.destroyForcibly();
    

    Разница между ними зависит от платформы. В UNIX-подобных системах (например, в Linux) метод destroy() завершает процесс с помощью сигнала SIGTERM, а destroyForcibly() - с помощью сигнала SIGKILL. (Метод supportsNormalTermination() возвращает true, если метод destroy() может завершить процесс нормально.)

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

    Ожидание завершения процесса и waitFor

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

    int result = process.waitFor();

    Этот метод возвращает числовой код завершения процесса (по соглашению, 0 в случае успешного завершения или ненулевой код ошибки).

    Однако этот метод неограничен по времени. Чтобы ограничить время ожидания, можно применить другую версию метода, которая принимает временной интервал:

    long delay = 10; // период ожидания
    if (process.waitFor(delay, TimeUnit.SECONDS)){
    
        int result = process.exitValue();
    }
    else{
    
        process.destroyForcibly();
    }
    

    Здесь вызов process.waitFor() возвращает true, если время ожидания процесса не истекло. В этом случае с помощью метода exitValue() получаем статусный код завершения.

    Асинхронное уведомление о завершении процесса и onExit()

    С помощью метода onExit() можно получить асинхронное уведомление о завершении процесса. Этот метод возвращает объект CompletableFuture<Process>, который можно использовать для планирования любого действия по завершении процесса:

    class Program{
     
        public static void main(String[] args) throws Exception{
              
            var builder = new ProcessBuilder("java", "--version");
            Process process = builder.redirectOutput(ProcessBuilder.Redirect.INHERIT).start();
            process.onExit().thenAccept(p -> System.out.println("Процесс завершился с кодом: " + p.exitValue()));
            Thread.sleep(500); // задержка, что успел отработать
        }
    }
    
    Помощь сайту
    Юмани:
    410011174743222
    Номер карты:
    4048415020898850