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 关键点解析
- 路径处理:通过类加载器获取资源路径,避免硬编码
- 命令构建:
ProcessBuilder
接收String...
参数,模拟命令行调用 - 流合并:
redirectErrorStream(true)
将错误流重定向到输出流,避免信息丢失 - 结果校验:
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执行的核心要点:
- 命令构造:
- 可执行JAR:
java -jar <路径>
- 普通JAR:
java -cp <路径> <主类> [参数]
- 可执行JAR:
- 流处理:
- 始终合并错误流(
redirectErrorStream(true)
) - 及时读取输出流避免阻塞
- 始终合并错误流(
- 资源管理:
- 必须调用
destroy()
释放进程 - 使用try-with-resources关闭流
- 必须调用
✅ 最佳实践:封装工具类时建议提供超时控制,避免进程长时间阻塞
掌握这些技巧,就能在Java程序中灵活调用外部JAR,实现模块化解耦和功能扩展。