核心思想
Java 调用 Linux 命令的本质是:在 Java 程序中创建一个新的操作系统进程,然后在这个进程中执行指定的命令,Java 提供了多种方式来实现这一操作。

使用 Runtime.getRuntime().exec()
这是最基础、最直接的方法,但也是最容易出问题的方法。
1. 基础用法
Runtime 类提供了一个 exec() 方法,可以用来执行命令。
示例代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class BasicCommandExec {
public static void main(String[] args) {
// 要执行的命令
String command = "ls -l /tmp";
try {
// 1. 获取 Runtime 实例
Process process = Runtime.getRuntime().exec(command);
// 2. 获取命令的输入流(即命令的输出结果)
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
// 3. 读取输出
String line;
System.out.println("Command output:");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
// 4. 等待命令执行完成,并获取退出码
int exitCode = process.waitFor();
System.out.println("\nExited with code: " + exitCode);
} catch (IOException e) {
System.err.println("Error executing command: " + e.getMessage());
} catch (InterruptedException e) {
System.err.println("Process was interrupted: " + e.getMessage());
// 恢复中断状态
Thread.currentThread().interrupt();
}
}
}
代码解析:

Runtime.getRuntime().exec(command): 这行代码会启动一个新的进程来执行ls -l /tmp命令,并返回一个Process对象。process.getInputStream(): 命令的输出(标准输出 STDOUT)会通过这个输入流返回给 Java 程序。注意:如果命令输出量很大,不及时读取可能会导致缓冲区满,从而阻塞进程的执行。process.waitFor(): 这个方法会阻塞当前线程,直到命令执行完毕,并返回命令的退出码(exit code),0 通常表示成功,非 0 表示失败。InterruptedException:waitFor()方法会抛出InterruptedException,所以必须处理,最佳实践是捕获它并调用Thread.currentThread().interrupt()来恢复线程的中断状态。
2. Runtime.exec() 的常见陷阱
陷阱 1:命令中带空格
如果你直接拼接命令字符串,带空格的参数可能会被错误解析。
// 错误示范 String command = "ls -l /My Documents"; // 这会被当成三个参数,而不是两个 Process process = Runtime.getRuntime().exec(command);
解决方案: 将命令和参数拆分成一个字符串数组。
// 正确示范
String[] command = {"ls", "-l", "/My Documents"};
Process process = Runtime.getRuntime().exec(command);
陷阱 2:忘记读取错误流
如果命令执行失败,错误信息会输出到标准错误流(process.getErrorStream()),如果你不读取它,错误缓冲区可能会被填满,导致进程挂起。
// 错误示范:只读取了标准输出 String command = "ls /non_existent_dir"; Process process = Runtime.getRuntime().exec(command); // ... 只读取 process.getInputStream() ... // 如果目录不存在,错误信息会堆积,可能导致进程无法结束
解决方案: 必须同时读取标准输出和标准错误流,最简单的方式是使用两个不同的线程来分别读取它们。
// 改进版:同时读取输出和错误流
// ... 创建 process ...
// 读取标准输出
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("[STDOUT] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// 读取标准错误
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.err.println("[STDERR] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// 等待进程结束
int exitCode = process.waitFor();
System.out.println("Exited with code: " + exitCode);
使用 ProcessBuilder (推荐)
ProcessBuilder 是 Java 5 引入的类,它提供了比 Runtime.exec() 更强大、更灵活的控制,是目前官方推荐的方式。
1. 基础用法
ProcessBuilder 允许你设置命令、工作目录、环境变量等。
示例代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class ProcessBuilderExample {
public static void main(String[] args) {
// 1. 创建 ProcessBuilder 实例,命令和参数作为列表传入
// 推荐使用这种方式,可以自动处理带空格的参数
ProcessBuilder pb = new ProcessBuilder("ls", "-l", "/var/log");
// (可选) 设置工作目录
// pb.directory(new File("/path/to/working/directory"));
// (可选) 设置环境变量
// Map<String, String> env = pb.environment();
// env.put("MY_VAR", "my_value");
try {
// 2. 启动进程
Process process = pb.start();
// 3. 读取输出 (同样建议多线程读取)
// 为了简洁,这里只读取标准输出,实际项目中请务必同时读取错误流
try (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. 等待进程结束
int exitCode = process.waitFor();
System.out.println("\nExited with code: " + exitCode);
} catch (IOException e) {
System.err.println("Error executing command: " + e.getMessage());
} catch (InterruptedException e) {
System.err.println("Process was interrupted: " + e.getMessage());
Thread.currentThread().interrupt();
}
}
}
2. ProcessBuilder 的优势
- 更安全的命令解析:将命令和参数作为列表传递,由
ProcessBuilder内部处理,避免了Runtime.exec()的空格陷阱。 - 重定向流:这是
ProcessBuilder的杀手级功能,你可以轻松地将进程的标准输入、输出、错误流重定向到文件或彼此合并。
示例:合并标准输出和标准错误流
ProcessBuilder pb = new ProcessBuilder("sh", "-c", "ls /non_existent_dir; echo 'This is a success message'");
pb.redirectErrorStream(true); // 将错误流合并到输出流中
Process process = pb.start();
// 现在只需要读取一个输入流即可
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
// ... 等待进程结束 ...
示例:将输出重定向到文件
ProcessBuilder pb = new ProcessBuilder("ls", "-l", "/tmp");
pb.redirectOutput(new File("output.txt")); // 标准输出重定向到文件
pb.redirectError(new File("error.txt")); // 标准错误重定向到文件
Process process = pb.start();
// 不需要再读取输入流了,因为输出已经写入文件
int exitCode = process.waitFor();
System.out.println("Command finished. Output written to output.txt");
使用第三方库 (Apache Commons Exec)
对于更复杂的场景,比如需要处理超时、更复杂的命令解析和更健壮的流处理,使用第三方库是更好的选择,Apache Commons Exec 是一个非常流行和强大的库。
添加依赖 (Maven):
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.3.0</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 ApacheCommonsExecExample {
public static void main(String[] args) {
// 1. 创建 CommandLine 对象,可以更智能地处理参数
CommandLine cmdLine = new CommandLine("sh");
cmdLine.addArgument("-c"); // 使用 shell 来执行,可以支持管道等复杂操作
cmdLine.addArgument("ls -l /tmp | grep 'log'");
// 2. 创建 Executor
DefaultExecutor executor = new DefaultExecutor();
// (可选) 设置超时时间 (毫秒)
executor.setWorkingDirectory(new java.io.File("/tmp"));
executor.setExitValue(0); // 正常退出的值
// 3. 创建流处理器,用于捕获输出
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
executor.setStreamHandler(streamHandler);
try {
System.out.println("Executing command: " + cmdLine);
// 4. 执行命令
int exitCode = executor.execute(cmdLine);
System.out.println("\n--- Command Output ---");
System.out.println(outputStream.toString());
if (errorStream.size() > 0) {
System.out.println("\n--- Command Error ---");
System.err.println(errorStream.toString());
}
System.out.println("\nExited with code: " + exitCode);
} catch (ExecuteException e) {
System.err.println("Execution failed with exit code: " + e.getExitValue());
System.err.println("Error output: " + errorStream.toString());
} catch (IOException e) {
System.err.println("IO Error: " + e.getMessage());
}
}
}
Apache Commons Exec 的优势:
- 超时控制:内置的超时机制,可以防止进程无限期挂起。
- 命令构建:
CommandLine类提供了更友好的 API 来构建命令。 - 强大的流处理:
PumpStreamHandler简化了输出和错误的捕获。 - 更健壮的错误处理:对
ExecuteException的处理更细致。
总结与最佳实践
| 特性 | Runtime.exec() |
ProcessBuilder |
Apache Commons Exec |
|---|---|---|---|
| 易用性 | 简单,但容易出错 | 简单直观 | 需要学习 API,但功能强大 |
| 健壮性 | 低,易死锁 | 高,支持流重定向 | 非常高,内置超时等机制 |
| 灵活性 | 低 | 高,可设置目录、环境变量、重定向流 | 非常高,支持复杂命令构建和高级特性 |
| 推荐场景 | 快速原型、简单命令 | 大多数生产环境场景 | 复杂命令、需要超时控制、企业级应用 |
最佳实践建议:
- 首选
ProcessBuilder:对于绝大多数 Java 调用 Linux 命令的场景,ProcessBuilder是最佳选择,它比Runtime.exec()更安全、更灵活。 - 务必处理流:无论使用哪种方法,都要同时读取标准输出流 (
getInputStream()) 和标准错误流 (getErrorStream()),或者使用redirectErrorStream(true)将它们合并,这是防止进程因缓冲区满而挂起的关键。 - 考虑使用 Shell:当命令比较复杂,包含管道 、重定向
>、通配符 等时,最好通过sh -c "your complex command"的方式来执行,让 Shell 来解析这些特殊字符。 - 处理超时:如果命令执行时间不确定,一定要设置超时,避免程序无限期等待。
ProcessBuilder本身不直接支持,需要结合Future或Timer,而Apache Commons Exec则提供了现成的超时设置。 - 错误处理:始终检查进程的退出码 (
exitCode),以判断命令是否成功执行。 - 对于复杂需求,使用第三方库:如果项目对命令执行的健壮性、超时控制等有较高要求,不要犹豫,直接使用
Apache Commons Exec这样的成熟库。
