下面我将详细介绍几种主流的方法,从最简单到最强大,并分析它们的优缺点和适用场景。

核心思路
无论使用哪种方法,Java 调用 Shell 的基本流程都是相似的:
- 获取
Runtime实例:Runtime.getRuntime()是一个单例方法,用于与 Java 运行时环境交互。 - 执行命令:通过
Runtime的exec()方法来启动一个新的进程并执行命令。 - 处理输入/输出/错误流:这是最关键也最容易出错的一步,子进程有自己的标准输入、标准输出和标准错误流,如果这些流不被及时读取,可能会导致子进程阻塞,甚至死锁。
- 等待进程结束:调用
Process.waitFor()来等待命令执行完成,并获取其退出码。
Runtime.exec() - 基础方法
这是最直接的方法,但也是最容易出现问题的方法。exec() 方法有多个重载版本。
执行单个命令
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class SimpleShellCommand {
public static void main(String[] args) {
try {
// 1. 执行命令
Process process = Runtime.getRuntime().exec("ls -l");
// 2. 读取命令的标准输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
System.out.println("Command Output:");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
// 3. 读取命令的标准错误(可选,但推荐)
BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
System.out.println("\nCommand Error (if any):");
while ((line = errorReader.readLine()) != null) {
System.err.println(line);
}
// 4. 等待命令执行完成
int exitCode = process.waitFor();
System.out.println("\nCommand finished with exit code: " + exitCode);
} catch (Exception e) {
e.printStackTrace();
}
}
}
⚠️ 重要注意事项:
- 流处理:上面的代码中,我们使用
BufferedReader来逐行读取输出流,如果命令输出很大,直接读取InputStream而不进行缓冲,可能会导致性能问题,更严重的是,如果不读取子进程的输出流,当缓冲区满时,子进程会阻塞,等待 Java 程序读取,从而导致 Java 程序也卡住,这就是所谓的“死锁”。 - 命令作为字符串:
exec("ls -l")看起来简单,但底层会调用系统的sh -c来解析这个字符串,如果命令字符串中包含复杂的 shell 特殊字符(如>,<, ,&),它们可能会被 shell 解释,也可能不会,行为不确定。
执行带参数的命令(推荐方式)
为了避免 shell 解析的歧义,最好将命令和它的参数作为数组传递给 exec()。

public class CommandWithArgs {
public static void main(String[] args) {
try {
// 将命令和参数分开,更安全、更清晰
String[] command = { "ls", "-l", "/tmp" };
Process process = Runtime.getRuntime().exec(command);
// 合并输出和错误流到一个地方处理
// 使用 StreamGobbler 是一个好习惯,可以避免死锁
StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(), System.out::println);
StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), System.err::println);
outputGobbler.start();
errorGobbler.start();
int exitCode = process.waitFor();
outputGobbler.join(); // 等待线程结束
errorGobbler.join();
System.out.println("\nCommand finished with exit code: " + exitCode);
} catch (Exception e) {
e.printStackTrace();
}
}
// 一个辅助类,用于消费输入流,防止缓冲区满导致阻塞
private static class StreamGobbler extends Thread {
private final InputStream inputStream;
private final java.util.function.Consumer<String> consumer;
public StreamGobbler(InputStream inputStream, java.util.function.Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}
@Override
public void run() {
new BufferedReader(new InputStreamReader(inputStream)).lines()
.forEach(consumer);
}
}
}
使用 ProcessBuilder - 更强大、更推荐
ProcessBuilder 是 Java 5 引入的,它比 Runtime.exec() 更强大、更灵活,是现在执行外部命令的首选方式。
主要优势:
- 命令和参数分离:构造函数直接接受一个
List<String>或String[],无需 shell 解析,更安全。 - 轻松重定向流:可以轻松地将子进程的输入、输出、错误流重定向到文件或当前进程的流。
- 设置工作目录:可以指定命令执行时的工作目录。
- 管理环境变量:可以修改子进程的环境变量。
示例代码:
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.List;
public class ProcessBuilderExample {
public static void main(String[] args) {
// 1. 准备命令和参数
List<String> command = Arrays.asList("sh", "-c", "for i in {1..5}; do echo \"Hello $i\"; sleep 1; done");
// 2. 创建 ProcessBuilder 实例
ProcessBuilder pb = new ProcessBuilder(command);
// 3. (可选) 设置工作目录
pb.directory(new File("/tmp"));
// 4. (可选) 合并错误流到输出流,这样只需要读取一个流
pb.redirectErrorStream(true);
try {
// 5. 启动进程
Process process = pb.start();
// 6. 读取输出
// 因为 redirectErrorStream(true),所以这里同时捕获了标准输出和标准错误
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
System.out.println("Command Output:");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
// 7. 等待进程结束
int exitCode = process.waitFor();
System.out.println("\nCommand finished with exit code: " + exitCode);
} catch (Exception e) {
e.printStackTrace();
}
}
}
ProcessBuilder 的流处理:
- 不处理流(最危险):如果既不读取也不重定向,缓冲区满会阻塞。
- 读取流:像上面例子一样,使用线程读取。
StreamGobbler类在这里同样适用。 - 重定向到文件:
pb.redirectOutput(new File("output.log"));,这是最安全的方式之一,尤其适用于输出量大的命令。 - 重定向到当前进程的流:
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);,这会让子进程的输出直接打印到你的控制台,非常方便用于调试。
使用第三方库(如 Apache Commons Exec)
如果你的需求比较复杂,或者希望代码更简洁、健壮,可以考虑使用第三方库。Apache Commons Exec 是一个非常流行的选择。
优点:
- 更高级的 API:提供了更直观的
CommandLine和Executor接口。 - 超时控制:可以轻松地为命令执行设置超时时间。
- 更好的流处理:内置了流处理器,可以方便地将输出流重定向到
Logger或其他目标。 - 更完善的错误处理。
示例代码(需要添加 commons-exec 依赖):
<!-- Maven 依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.3</version> <!-- 使用最新版本 -->
</dependency>
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.exec.ExecuteException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class CommonsExecExample {
public static void main(String[] args) {
CommandLine cmdLine = CommandLine.parse("df -h");
DefaultExecutor executor = new DefaultExecutor();
// 设置超时时间为 10 秒
executor.setExitValue(0); // 正常退出的状态码
// executor.setWorkingDirectory(...); // 设置工作目录
// 使用 PumpStreamHandler 来处理输出和错误流
// 将输出捕获到 ByteArrayOutputStream
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
executor.setStreamHandler(streamHandler);
try {
System.out.println("Executing command: " + cmdLine);
int exitCode = executor.execute(cmdLine);
System.out.println("\nCommand finished with exit code: " + exitCode);
System.out.println("\n--- Standard Output ---");
System.out.println(outputStream.toString());
System.out.println("\n--- Error Output ---");
System.out.println(errorStream.toString());
} catch (ExecuteException e) {
// 命令执行失败,非正常退出
System.err.println("Execution failed with exit code: " + e.getExitValue());
System.err.println("Error output: " + errorStream.toString());
} catch (IOException e) {
// IO异常,例如无法启动进程
e.printStackTrace();
}
}
}
总结与对比
| 特性 | Runtime.exec() |
ProcessBuilder |
Apache Commons Exec |
|---|---|---|---|
| 易用性 | 简单,但容易出错 | 中等,API 更清晰 | 高,API 设计优秀 |
| 灵活性 | 低 | 高(流重定向、工作目录、环境变量) | 高(超时、流处理器等高级功能) |
| 健壮性 | 低(易死锁) | 中(需要手动处理流) | 高(内置了健壮的流处理和超时机制) |
| 推荐场景 | 简单、一次性、低风险的命令 | 大多数场景下的首选,功能全面 | 复杂脚本、需要超时控制、追求代码健壮性和可维护性的项目 |
| 依赖 | 无(JDK 内置) | 无(JDK 内置) | 需要添加 commons-exec 依赖 |
最终建议
- 对于简单的命令:如果只是执行一个简单的、无输出的命令,或者能确保输出量很小,可以直接使用
ProcessBuilder并读取流。 - 对于大多数情况:强烈推荐使用
ProcessBuilder,它是 Java 标准库的一部分,功能强大,足以应对 95% 的场景,记得使用StreamGobbler或重定向到文件来处理输出流,以避免死锁。 - 对于复杂项目:如果你的项目已经依赖了 Apache Commons,或者你需要处理复杂的命令行逻辑、超时、日志集成等,那么使用
Apache Commons Exec会让你的代码更简洁、更可靠。
无论选择哪种方法,请务必记住:处理好子进程的输入、输出和错误流,是成功调用 Shell 命令的关键。

