本文介绍了如何在 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<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` 方法执行参数,通过参数可以完全控制子进程执行过程,例如减小执行时 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 重定向,确定子进程在后台运行或阻塞运行。看起来像下面这样:


```java
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);
}
```


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