核心概念
在 Java 中执行系统命令,本质上是启动一个新的操作系统进程来运行指定的命令,Java 通过 java.lang.Process 类来表示这个新进程,我们需要做的就是:

- 启动进程:告诉 JVM 要执行哪个命令。
- 获取输入/输出流:向进程的输入流发送数据,从进程的标准输出流和错误流读取数据。
- 等待进程结束:检查进程的退出状态码,确保命令执行完成。
Runtime.exec() (最基础但不推荐)
这是最传统、最直接的方法。Runtime 类提供了一个 exec() 方法来执行命令。
示例代码
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class RuntimeExecExample {
public static void main(String[] args) {
String command = "ls -l /etc/hosts"; // 示例命令:列出/etc/hosts文件信息
try {
// 1. 获取Runtime实例
Runtime runtime = Runtime.getRuntime();
// 2. 执行命令,返回一个Process对象
Process process = runtime.exec(command);
// 3. 读取命令的标准输出
// 使用InputStreamReader和BufferedReader来高效读取
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
System.out.println("--- Command Output ---");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
// 4. 读取命令的错误输出(非常重要!)
// 如果不读取错误流,当命令输出大量错误信息时,缓冲区可能会满,导致进程阻塞
BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
System.out.println("\n--- Command Error (if any) ---");
while ((line = errorReader.readLine()) != null) {
System.err.println(line);
}
// 5. 等待命令执行完成,并获取退出码
int exitCode = process.waitFor();
System.out.println("\n--- Command Finished with Exit Code: " + exitCode + " ---");
} catch (Exception e) {
e.printStackTrace();
}
}
}
优点
- 简单直接,无需额外依赖。
缺点 (非常关键!)
- 缓冲区阻塞:这是
Runtime.exec()最经典的问题,如果子进程产生的标准输出或错误输出流没有被及时读取,缓冲区可能会被填满,导致子进程阻塞,进而导致主线程也永久阻塞,上面的代码通过BufferedReader循环读取解决了这个问题。 - 命令拼接复杂:当命令参数中包含空格或特殊字符时,直接拼接字符串非常容易出错。
exec("rm -rf /some path/with spaces")会失败,因为JVM会将其解析为三个独立的参数。 - 跨平台性差:
Runtime.exec()的行为在不同操作系统上可能不一致,例如路径分隔符( vs\)、命令格式等。 - 难以处理复杂的交互:如果命令需要交互式输入(如输入密码),处理起来非常麻烦。
ProcessBuilder (推荐,现代标准)
从 Java 1.5 开始,引入了 ProcessBuilder 类,它是对 Runtime.exec() 的改进,是执行系统命令的首选方式。
示例代码
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
public class ProcessBuilderExample {
public static void main(String[] args) {
// 1. 将命令和参数拆分成一个列表,避免字符串拼接问题
// 注意:每个参数都是一个独立的字符串
ProcessBuilder pb = new ProcessBuilder("ls", "-l", "/etc/hosts");
// 2. 可以设置工作目录
// pb.directory(new File("/tmp"));
// 3. 重定向输入、输出和错误流
// 将错误流合并到标准输出流中,方便统一处理
pb.redirectErrorStream(true);
try {
// 4. 启动进程
Process process = pb.start();
// 5. 读取输出(由于redirectErrorStream=true,错误信息也会在这里读到)
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
System.out.println("--- Command Output (with Error) ---");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
// 6. 等待进程结束
int exitCode = process.waitFor();
System.out.println("\n--- Command Finished with Exit Code: " + exitCode + " ---");
} catch (Exception e) {
e.printStackTrace();
}
}
}
ProcessBuilder 的优点
- 更安全:通过列表传递命令和参数,自动处理了空格和特殊字符,避免了命令注入风险。
- 更灵活:
- 可以轻松设置工作目录 (
pb.directory())。 - 可以方便地重定向输入、输出和错误流 (
pb.redirectInput(),pb.redirectOutput(),pb.redirectError(),pb.redirectErrorStream(true))。
- 可以轻松设置工作目录 (
- 更健壮:设计上比
Runtime.exec()更好,避免了部分常见的陷阱。
高级用法:处理交互式命令
有些命令(如 sudo, passwd, ssh)需要与用户进行交互,这需要我们向进程的输入流写入数据,并从输出流读取提示信息。
示例:模拟 echo "hello world" 并通过管道 | wc -c 计算字符数
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
public class InteractiveCommandExample {
public static void main(String[] args) {
try {
// 使用ProcessBuilder构建命令
ProcessBuilder pb = new ProcessBuilder("sh", "-c", "echo \"hello world\" | wc -c");
Process process = pb.start();
// 获取进程的输入流,用于向进程写入数据
// InputStream inputStream = process.getOutputStream(); // 对于echo命令,我们不需要向它写东西
// 获取进程的输出流,用于读取进程的输出
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
System.out.println("--- Interactive Command Output ---");
while ((line = reader.readLine()) != null) {
System.out.println(line); // 应该会输出 "13"
}
int exitCode = process.waitFor();
System.out.println("\n--- Command Finished with Exit Code: " + exitCode + " ---");
} catch (Exception e) {
e.printStackTrace();
}
}
}
第三方库:Apache Commons Exec
如果执行命令的逻辑非常复杂(例如需要超时控制、更复杂的流处理、更好的日志集成等),使用第三方库是一个非常好的选择。Apache Commons Exec 是最流行的库之一。

Maven 依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.3.2</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.StreamHandler;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class CommonsExecExample {
public static void main(String[] args) {
CommandLine cmdLine = CommandLine.parse("ls -l /etc/hosts");
DefaultExecutor executor = new DefaultExecutor();
// 设置超时时间 (毫秒)
executor.setWorkingDirectory(new File("/etc"));
executor.setExitValue(0); // 正常退出的值
// 使用PumpStreamHandler来处理输入输出,可以捕获到String
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
StreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
executor.setStreamHandler(streamHandler);
try {
System.out.println("--- Executing with Apache Commons Exec ---");
int exitCode = executor.execute(cmdLine);
System.out.println("--- Command 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 (IOException e) {
e.printStackTrace();
}
}
}
优点
- 功能强大:内置超时控制、更完善的流处理、命令解析等。
- 代码更简洁:封装了底层细节,提供了更高级的API。
- 可维护性高:对于复杂的命令执行场景,代码更清晰。
总结与最佳实践
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
Runtime.exec() |
无需依赖,最基础 | 有阻塞风险,命令拼接易错,跨平台差 | 简单、一次性、无输出的命令(不推荐用于生产) |
ProcessBuilder |
推荐,安全,灵活,功能齐全 | Java 1.5+ | 绝大多数场景下的首选,是Java执行命令的标准方式。 |
| Apache Commons Exec | 功能强大(超时、流处理等),代码优雅 | 需要引入第三方依赖 | 复杂的命令执行逻辑,需要高级功能(如超时控制)的项目。 |
最佳实践建议:
- 首选
ProcessBuilder:对于99%的情况,直接使用ProcessBuilder就足够了,它安全、灵活且是Java官方推荐的方式。 - 始终处理输出流:无论是标准输出还是错误输出,都要有地方去读取,如果不关心,可以将其重定向到
null或/dev/null,以避免缓冲区阻塞。 - 使用列表传递命令:将命令和参数拆分成一个
List<String>或数组,避免字符串拼接带来的问题。 - 检查退出码:使用
process.waitFor()获取退出码,以判断命令是否成功执行。 - 考虑超时:对于可能长时间运行的命令,务必设置超时,否则你的程序可能会被无限期挂起。
ProcessBuilder本身不直接支持,但可以结合Thread和process.destroy()来实现,或者直接使用Apache Commons Exec。 - 处理异常:妥善处理
IOException和InterruptedException。

