在 Java 中执行 Shell 命令,主要有以下几种方式,我将从最基础、最常用的方法开始,逐步介绍更高级和更健壮的实现方式。

使用 Runtime.exec() (最基础)
这是最直接的方法,通过 java.lang.Runtime 类的 exec() 方法来启动一个进程执行命令。
基本用法
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class SimpleShellCommand {
public static void main(String[] args) {
try {
// 1. 创建一个Runtime实例
Runtime runtime = Runtime.getRuntime();
// 2. 执行命令 (这里以Linux/macOS的 'ls -l' 和Windows的 'dir' 为例)
// 注意:命令是一个字符串数组,第一个元素是命令,后面是参数
String[] command = {"/bin/sh", "-c", "ls -l"}; // Linux/macOS
// String[] command = {"cmd", "/c", "dir"}; // Windows
// 3. 启动进程
Process process = runtime.exec(command);
// 4. 获取命令的输出流 (标准输出)
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
System.out.println("Command output:");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
// 5. 等待命令执行完成,并获取退出码
int exitCode = process.waitFor();
System.out.println("\nCommand finished with exit code: " + exitCode);
} catch (Exception e) {
e.printStackTrace();
}
}
}
重要注意事项
- 命令阻塞问题:上面的代码有一个潜在问题,如果子进程产生的输出流(
InputStream)没有被及时读取,缓冲区可能会被填满,导致子进程阻塞,进而导致主程序也阻塞,上面的例子中,我们立即启动了一个线程来读取输出,就是为了避免这个问题。 - 命令拼接与安全性:直接拼接字符串来构建命令是非常危险的,容易引发命令注入攻击,如果命令的参数来自用户输入:
// 危险! String userInput = "malicious; rm -rf /"; Process process = runtime.exec("ls -l " + userInput);这将导致
rm -rf /被执行,造成灾难性后果。永远不要这样做,正确的做法是使用字符串数组,如上例所示,这样参数会被当作独立的参数处理,而不是命令的一部分。
使用 ProcessBuilder (推荐)
ProcessBuilder 是自 Java 5 引入的,它比 Runtime.exec() 更加强大和灵活,是目前推荐使用的方式。
基本用法
ProcessBuilder 将命令和参数作为列表(List<String>)传递,这更安全,也更清晰。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
public class ProcessBuilderExample {
public static void main(String[] args) {
// 1. 创建ProcessBuilder实例,并设置命令和参数
// List<String> command = List.of("/bin/sh", "-c", "ls -l /etc/hosts"); // Java 9+
List<String> command = new ArrayList<>();
command.add("/bin/sh");
command.add("-c");
command.add("ls -l /etc/hosts");
// Windows 示例
// List<String> command = List.of("cmd", "/c", "dir", "C:\\Windows");
ProcessBuilder processBuilder = new ProcessBuilder(command);
// 2. 可以设置工作目录 (可选)
// processBuilder.directory(new File("/path/to/your/directory"));
try {
// 3. 启动进程
Process process = processBuilder.start();
// 4. 读取命令输出
// 使用try-with-resources确保流被正确关闭
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);
}
}
// 5. 读取错误输出 (非常重要!)
// 错误流如果不读取,同样会导致进程阻塞
try (BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
System.out.println("\nCommand errors:");
while ((line = errorReader.readLine()) != null) {
System.err.println(line);
}
}
// 6. 等待进程结束
int exitCode = process.waitFor();
System.out.println("\nCommand finished with exit code: " + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
ProcessBuilder 的优势
-
更安全的命令构建:强制使用列表来传递命令和参数,避免了字符串拼接带来的注入风险。
-
更灵活的I/O重定向:可以轻松地重定向进程的输入、输出和错误流。
// 将进程的输出重定向到一个文件 processBuilder.redirectOutput(new File("output.txt")); // 将进程的错误重定向到另一个文件 processBuilder.redirectError(new File("error.txt")); // 合并标准输出和错误流 processBuilder.redirectErrorStream(true); -
设置工作目录:可以指定进程运行在哪个目录下。
-
环境变量管理:可以获取和修改进程的环境变量。
(图片来源网络,侵删)Map<String, String> env = processBuilder.environment(); env.put("MY_VAR", "my_value");
使用 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.Executor;
import org.apache.commons.exec.LogOutputStream;
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");
// Windows: CommandLine.parse("cmd /c dir C:\\Windows");
DefaultExecutor executor = new DefaultExecutor();
// 设置超时时间 (毫秒)
executor.setWorkingDirectory(new java.io.File("."));
executor.setExitValue(0); // 成功的退出码
// 使用自定义的流处理器来捕获输出
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
executor.setStreamHandler(streamHandler);
try {
System.out.println("Executing command...");
int exitValue = executor.execute(cmdLine);
System.out.println("Command finished with exit code: " + exitValue);
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) {
System.err.println("Error executing command: " + e.getMessage());
e.printStackTrace();
}
}
}
Commons Exec 的优势
- 超时控制:可以轻松地为命令执行设置超时。
- 流处理更灵活:提供了
PumpStreamHandler等工具,可以方便地将进程的输出/错误流泵送到任何地方(如文件、内存、控制台)。 - 更丰富的API:提供了更多高级功能,如异步执行、观察者模式来监控进程状态等。
总结与最佳实践
| 特性 | Runtime.exec() |
ProcessBuilder |
Apache Commons Exec |
|---|---|---|---|
| 易用性 | 简单直接 | 简单,比Runtime更清晰 |
稍复杂,需要引入库 |
| 安全性 | 差,易受注入攻击 | 好,强制使用列表 | 好,强制使用列表 |
| 功能 | 基础 | 强大,支持重定向、环境变量等 | 非常强大,支持超时、异步、高级流处理 |
| 阻塞风险 | 高,需手动管理线程 | 中,需手动读取InputStream和ErrorStream |
低,库内部处理得更好 |
| 推荐度 | 不推荐,仅用于极简场景 | 强烈推荐,是Java标准库的最佳实践 | 复杂场景下的首选 |
最佳实践
- 首选
ProcessBuilder:对于绝大多数在Java中执行Shell命令的需求,ProcessBuilder是最佳选择,它安全、灵活且是标准库的一部分。 - 始终读取错误流:在
ProcessBuilder中,除了读取InputStream(标准输出),一定要读取ErrorStream,否则缓冲区满时会导致进程挂起。 - 处理线程:如果命令执行时间较长或者输出量很大,最好在一个单独的线程中读取输入流和错误流,以避免阻塞主线程。
- 考虑超时:对于可能长时间运行的命令,一定要设置超时机制,防止程序无限期等待。
ProcessBuilder本身不直接支持,需要结合Future和ExecutorService,或者直接使用Commons Exec。 - 处理平台差异:注意不同操作系统(Windows vs. Linux/macOS)命令和参数的差异,代码中需要做判断或使用跨平台工具(如
sh或cmd)。
