1. 简介

在Java项目开发中,我们常遇到需要在程序内以独立进程运行外部JAR文件的情况。具体分两种场景:

  • ✅ 执行可执行JAR(包含清单文件且已配置Main-Class属性)
  • ❌ 执行普通JAR(需手动指定主类)

本文将通过实战代码演示两种场景的实现方案,重点讲解ProcessBuilder的用法及流处理技巧。

2. 执行可执行JAR

可执行JAR的核心特征:清单文件(MANIFEST.MF)中包含Main-Class属性,指向启动类。命令行可通过java -jar <example.jar>直接运行。

2.1 实现代码

@Test
public void givenRunnableJar_whenExecuted_thenShouldRunSuccessfully() {
    Process process = null;
    try {
        // 获取JAR文件绝对路径
        String jarFile = new File(Objects.requireNonNull(getClass().getClassLoader()
          .getResource(RUNNABLE_JAR_PATH))
          .toURI()).getAbsolutePath();

        // 构建进程命令
        ProcessBuilder processBuilder = new ProcessBuilder("java", "-jar", jarFile);
        processBuilder.redirectErrorStream(true);  // 合并错误流到输出流

        process = processBuilder.start();
        try (InputStream inputStream = process.getInputStream()) {
            byte[] output = inputStream.readAllBytes();
            System.out.println("Output: " + new String(output));
        }

        // 检查退出状态码
        int exitCode = process.waitFor();
        Assert.assertEquals("Process exited with an unexpected exit code", 0, exitCode);
    } catch (IOException | InterruptedException | URISyntaxException e) {
        Assert.fail("Test failed due to exception: " + e.getMessage());
    } finally {
        if (process != null) {
            process.destroy();  // 确保进程销毁
        }
    }
}

2.2 关键点解析

  1. 路径处理:通过类加载器获取资源路径,避免硬编码
  2. 命令构建ProcessBuilder接收String...参数,模拟命令行调用
  3. 流合并redirectErrorStream(true)将错误流重定向到输出流,避免信息丢失
  4. 结果校验
    • process.waitFor()阻塞等待进程结束
    • 退出码0表示成功,非0表示异常

⚠️ 踩坑提示:未调用destroy()可能导致进程残留,尤其在Windows系统下

3. 执行不可执行JAR文件

普通JAR特征:清单文件无Main-Class属性,需显式指定主类全限定名。

3.1 实现代码

@Test
public void givenNonRunnableJar_whenExecutedWithMainClass_thenShouldRunSuccessfully() {
    Process process = null;
    try {
        String jarFile = new File(Objects.requireNonNull(getClass().getClassLoader()
          .getResource(NON_RUNNABLE_JAR_PATH))
          .toURI()).getAbsolutePath();

        // 构建命令:java -cp <jar路径> <主类> [参数...]
        String[] command = { 
            "java", 
            "-cp", jarFile, 
            "com.company.HelloWorld",  // 主类全限定名
            "arg1", "arg2"            // 程序参数
        };
        ProcessBuilder processBuilder = new ProcessBuilder(command);
        processBuilder.redirectErrorStream(true);

        process = processBuilder.start();
        try (InputStream inputStream = process.getInputStream()) {
            byte[] output = inputStream.readAllBytes();
            System.out.println("Output: " + new String(output));
        }

        int exitCode = process.waitFor();
        Assert.assertEquals("Process exited with an unexpected exit code", 0, exitCode);
    } catch (IOException | InterruptedException | URISyntaxException e) {
        Assert.fail("Test failed due to exception: " + e.getMessage());
    } finally {
        if (process != null) {
            process.destroy();
        }
    }
}

3.2 与可执行JAR的核心差异

特性 可执行JAR 不可执行JAR
命令格式 java -jar <文件路径> java -cp <文件路径> <主类>
主类指定方式 清单文件自动指定 命令行显式指定
参数传递位置 JAR文件后追加 主类名后追加

💡 简单粗暴记忆:可执行JAR像"一键启动",普通JAR像"手动挡操作"

4. 总结

通过ProcessBuilder实现JAR执行的核心要点:

  1. 命令构造
    • 可执行JAR:java -jar <路径>
    • 普通JAR:java -cp <路径> <主类> [参数]
  2. 流处理
    • 始终合并错误流(redirectErrorStream(true)
    • 及时读取输出流避免阻塞
  3. 资源管理
    • 必须调用destroy()释放进程
    • 使用try-with-resources关闭流

✅ 最佳实践:封装工具类时建议提供超时控制,避免进程长时间阻塞

掌握这些技巧,就能在Java程序中灵活调用外部JAR,实现模块化解耦和功能扩展。