Статьи

Запуск Java-класса в качестве подпроцесса

Запуск Java-класса (не jar-файла) в качестве подпроцесса — это то, что мне нужно было сделать на этой неделе. Точнее, я хотел порождать новый процесс из теста, а не запускать его внутри теста напрямую (в процессе). Я не думаю, что это что-то необычное или сложное занятие. Но это не то, что мне когда-либо приходилось делать раньше, и я не знал точного кода для написания.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class JavaProcess {
 
  private JavaProcess() {
  }
 
  public static int exec(Class clazz, List<String> jvmArgs, List<String> args) throws IOException,
        InterruptedException {
    String javaHome = System.getProperty("java.home");
    String javaBin = javaHome + File.separator + "bin" + File.separator + "java";
    String classpath = System.getProperty("java.class.path");
    String className = clazz.getName();
 
    List<String> command = new ArrayList<>();
    command.add(javaBin);
    command.addAll(jvmArgs);
    command.add("-cp");
    command.add(classpath);
    command.add(className);
    command.addAll(args);
 
    ProcessBuilder builder = new ProcessBuilder(command);
    Process process = builder.inheritIO().start();
    process.waitFor();
    return process.exitValue();
  }
}

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

Обратите внимание, чтобы это работало, класс, который вы хотите выполнить, должен иметь метод main . Это очень важно.

Доступ к пути исполняемого файла Java (хранящегося в javaBin ) позволяет выполнить подпроцесс, используя ту же версию Java, что и основное приложение. Если javaBin был заменен на "java" , то вы рискуете выполнить подпроцесс с версией Java вашего компьютера по умолчанию. Это, вероятно, хорошо в большинстве случаев. Но могут быть ситуации, когда это нежелательно.

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

Подпроцесс запускается с передачей ввода-вывода в процесс, который его выполнил. Это необходимо для того, чтобы увидеть все stdout и stderr он производит. inheritIO является удобным методом и может быть достигнуто путем вызова цепочки следующего кода (также настраивает стандартный stdin подпроцесса):

1
2
3
4
builder
    .redirectInput(ProcessBuilder.Redirect.INHERIT)
    .redirectOutput(ProcessBuilder.Redirect.INHERIT)
    .redirectError(ProcessBuilder.Redirect.INHERIT);

Наконец, waitFor указывает исполняющему потоку дождаться завершения порожденного подпроцесса. Неважно, если процесс завершится успешно или ошибки. Пока подпроцесс как-то заканчивается. Основное исполнение может продолжаться. Как завершился процесс, подробно описано в его exitValue . Например, 0 обычно обозначает успешное выполнение, а 1 — недопустимую синтаксическую ошибку. Существует много других кодов выхода, и они могут различаться в зависимости от приложения.

Вызов метода exec будет выглядеть примерно так:

1
JavaProcess.exec(MyProcess.class, List.of("-Xmx200m"), List.of("argument"))

Который выполняет следующую команду (или что-то близкое к ней):

1
/Library/Java/JavaVirtualMachines/jdk-12.0.1.jdk/Contents/Home/bin/java -cp /playing-around-for-blogs MyProcess "argument"

Я вырезал много путей, включая classpath, чтобы сделать его немного аккуратнее. Ваш, вероятно, будет выглядеть намного дольше, чем этот. Это действительно зависит от вашего приложения на самом деле. Путь в приведенной выше команде — это минимум, необходимый для его запуска (очевидно, настроенный для моей машины).

Метод exec достаточно гибок и полезен в описании того, что происходит. Хотя, если вы хотите сделать его более гибким и применимым в более широком диапазоне ситуаций, я рекомендую вернуть сам ProcessBuilder из метода. Позволяет вам повторно использовать этот фрагмент кода в нескольких местах, обеспечивая при этом гибкость в настройке перенаправлений ввода-вывода, а также возможность решать, следует ли запускать подпроцесс в фоновом режиме или в блоке, и ожидать его завершения. Это будет выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public static ProcessBuilder exec(Class clazz, List<String> jvmArgs, List<String> args) {
  String javaHome = System.getProperty("java.home");
  String javaBin = javaHome + File.separator + "bin" + File.separator + "java";
  String classpath = System.getProperty("java.class.path");
  String className = clazz.getName();
 
  List<String> command = new ArrayList<>();
  command.add(javaBin);
  command.addAll(jvmArgs);
  command.add("-cp");
  command.add(classpath);
  command.add(className);
  command.addAll(args);
 
  return new ProcessBuilder(command);
}

Используя любую из этих функций (или обе), вы теперь сможете запускать любой класс, который существует в пути к классам вашего приложения. В моей ситуации это было очень полезно для порождения подпроцессов внутри интеграционного теста без предварительной сборки каких-либо jar-файлов. Это позволило контролировать аргументы JVM, такие как память подпроцессов, которые не могли бы быть сконфигурированы, если бы выполнялись непосредственно внутри существующего процесса.

Опубликовано на Java Code Geeks с разрешения Дэна Ньютона, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Запуск Java-класса в качестве подпроцесса

Мнения, высказанные участниками Java Code Geeks, являются их собственными.