我本周需要将Java类(而不是jar)作为子进程运行。 更准确地说,我想从测试内部产生一个新进程,而不是直接在测试内部运行(进程内)。 我不认为这是幻想或复杂的事情。 但是,这不是我以前不需要做的事,也不知道要编写的确切代码。

幸运的是,稍后有一个快速的Google和一些Stack Overflow帖子。 我找到了所需的答案 。 尽管有答案,但为了我自己和你自己的利益,我在这里重写了它。

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 。 如果将javaBin替换为"java" ,则存在使用计算机的默认Java版本执行子javaBin的风险。 很多时候那可能很好。 但是,在某些情况下可能不希望这样做。

将所有命令添加到command列表后,它们将传递到ProcessBuilderProcessBuilder获取此列表,并使用其中包含的每个值来生成命令。 command列表中的每个值都由ProcessBuilder用空格分隔。 它的构造函数还有其他重载,其中之一包含一个字符串,您可以在其中手动定义整个命令。 这消除了您手动管理在命令字符串中添加参数的需要。

子进程以其IO传递到执行它的进程开始。 查看生成的所有stdoutstderr都是必需的。 inheritIO是一种便捷方法,也可以通过调用链接以下代码来实现(也配置子inheritIOstdin ):

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

最后, waitFor告诉执行线程等待所生成的子进程完成。 该过程是否成功结束或错误都无关紧要。 只要子流程以某种方式完成。 主要执行可以继续进行。 进程如何完成由其exitValue详细说明。 例如, 0通常表示成功执行,而1详细说明无效语法错误。 还有许多其他退出代码,它们在不同的应用程序中可能会有所不同。

调用exec方法如下所示:

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

它执行以下命令(或其附近的命令):

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

我剪掉了很多包含classpath的路径,以使其更加简洁。 您的外观可能会比这更长。 这实际上取决于您的应用程序。 上面命令中的路径是运行它所需的最低要求(显然是为我的机器定制的)。

exec方法相当灵活,有助于描述发生的情况。 尽管,如果您希望使其具有更大的延展性和适用性,但我建议从该方法返回ProcessBuilder本身。 允许您在多个地方重用这段代码,同时提供配置IO重定向的灵活性,以及决定是在后台还是在块中运行子流程并等待其完成的能力。 这看起来像:

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参数,例如子进程的内存,如果直接在现有进程内部运行,则这些子进程将无法配置。