本文介绍了如何在 Java 中运行子进程(非 jar)。确切地说,要求从测试程序内部启动一个新进程,而非直接在测试(进程)内部运行。尽管不是什么炫酷的技术,但以前没有做过类似的事情,不清楚如何下手。

经过一番搜索,在 Stack Overflow 中找到了[解答][1]。为了更好地解决问题,重写了答案。

[1]:https://stackoverflow.com/questions/636367/executing-a-java-application-in-a-separate-process

```java
class JavaProcess {
private JavaProcess() {
  }

public static int exec(Class clazz, List jvmArgs, List 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 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` 方法执行参数,通过参数可以完全控制子进程执行过程,例如减小执行时 JVM 堆空间(本文目标之一)。

注意:实际运行时,需要提供 `main` 方法。

为了让主程序与子进程使用同一个 Java 版本,可以使用 `javaBin` 中的路径调用 Java 可执行程序。如果把 `javaBin` 替换为 `java`,会调用本机默认安装的 Java 版本。大多数情况不会有问题,但很可能出现意外。

所有 command 加载到 `command` 列表后,都会传给 `ProcessBuilder` 生成命令。`ProcessBuilder` 对 `command` 中每个参数用空格分隔。也可以调用重载的构造函数,直接手工传入整个命令字符串,无需手工添加字符串参数。

一般在 IO 传递给执行的主进程后会启动子进程。通常期望看到 `stdout` 和 `stderr` 输出。可以直接调用 `inheritIO`,也可以使用以下方法(额外配置了 `stdin` 子进程):

```java
builder
    .redirectInput(ProcessBuilder.Redirect.INHERIT)
    .redirectOutput(ProcessBuilder.Redirect.INHERIT)
    .redirectError(ProcessBuilder.Redirect.INHERIT);
```

最后,`waitFor` 会通知执行线程等待产生的子进程执行结束,无论执行成功或还是出现错误,只要子进程以某种方式结束就可以了。主程序会继续执行。子进程的结束状态可以通过进程结束时 `exitValue` 判断。例如,通常 0 代表执行成功,1 代表语法无效错误。根据每个应用不同,还有其他 exit code。

调用 `exec` 方法:

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

> 译注:List.of 是 Java 9 引入的方法,之前的版本可以使用 Arrays.asList。

将执行下面的命令(或类似的命令):

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

为了整洁起见,已经移除了包括 classpath 在内的许多路径。实际执行结果可能看起来要长许多,与具体应用相关。上面展示的命令是可运行最短路径(为本文的示例定制)。

`exec` 方法很灵活,非常有助于描述正在进行的工作。不仅如此,还可以返回 `ProcessBuilder` 对象,增强扩展性、适用更多场景。不但可以在多个地方重用代码段,还能灵活配置 IO 重定向,确定子进程在后台运行或阻塞运行。看起来像下面这样:

```javapublic static ProcessBuilder exec(Class clazz, List jvmArgs, List 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 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);
}
```

利用上面的函数,可以运行 classpath 上的任意 class。这篇文章中,用来在集成测试中启动新的子进程,无需提前构建 jar。这种方法可以控制 JVM 参数,比如子进程内存大小。传统的直接调用无法对此进行配置。