杰瑞科技汇

java 执行 shell命令

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

java 执行 shell命令-图1
(图片来源网络,侵删)

使用 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();
        }
    }
}

重要注意事项

  1. 命令阻塞问题:上面的代码有一个潜在问题,如果子进程产生的输出流(InputStream)没有被及时读取,缓冲区可能会被填满,导致子进程阻塞,进而导致主程序也阻塞,上面的例子中,我们立即启动了一个线程来读取输出,就是为了避免这个问题。
  2. 命令拼接与安全性:直接拼接字符串来构建命令是非常危险的,容易引发命令注入攻击,如果命令的参数来自用户输入:
    // 危险!
    String userInput = "malicious; rm -rf /";
    Process process = runtime.exec("ls -l " + userInput);

    这将导致 rm -rf / 被执行,造成灾难性后果。永远不要这样做,正确的做法是使用字符串数组,如上例所示,这样参数会被当作独立的参数处理,而不是命令的一部分。


使用 ProcessBuilder (推荐)

ProcessBuilder 是自 Java 5 引入的,它比 Runtime.exec() 更加强大和灵活,是目前推荐使用的方式。

基本用法

ProcessBuilder 将命令和参数作为列表(List<String>)传递,这更安全,也更清晰。

java 执行 shell命令-图2
(图片来源网络,侵删)
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 的优势

  1. 更安全的命令构建:强制使用列表来传递命令和参数,避免了字符串拼接带来的注入风险。

  2. 更灵活的I/O重定向:可以轻松地重定向进程的输入、输出和错误流。

    // 将进程的输出重定向到一个文件
    processBuilder.redirectOutput(new File("output.txt"));
    // 将进程的错误重定向到另一个文件
    processBuilder.redirectError(new File("error.txt"));
    // 合并标准输出和错误流
    processBuilder.redirectErrorStream(true);
  3. 设置工作目录:可以指定进程运行在哪个目录下。

  4. 环境变量管理:可以获取和修改进程的环境变量。

    java 执行 shell命令-图3
    (图片来源网络,侵删)
    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 的优势

  1. 超时控制:可以轻松地为命令执行设置超时。
  2. 流处理更灵活:提供了 PumpStreamHandler 等工具,可以方便地将进程的输出/错误流泵送到任何地方(如文件、内存、控制台)。
  3. 更丰富的API:提供了更多高级功能,如异步执行、观察者模式来监控进程状态等。

总结与最佳实践

特性 Runtime.exec() ProcessBuilder Apache Commons Exec
易用性 简单直接 简单,比Runtime更清晰 稍复杂,需要引入库
安全性 差,易受注入攻击 ,强制使用列表 ,强制使用列表
功能 基础 强大,支持重定向、环境变量等 非常强大,支持超时、异步、高级流处理
阻塞风险 高,需手动管理线程 中,需手动读取InputStreamErrorStream 低,库内部处理得更好
推荐度 不推荐,仅用于极简场景 强烈推荐,是Java标准库的最佳实践 复杂场景下的首选

最佳实践

  1. 首选 ProcessBuilder:对于绝大多数在Java中执行Shell命令的需求,ProcessBuilder 是最佳选择,它安全、灵活且是标准库的一部分。
  2. 始终读取错误流:在 ProcessBuilder 中,除了读取 InputStream(标准输出),一定要读取 ErrorStream,否则缓冲区满时会导致进程挂起。
  3. 处理线程:如果命令执行时间较长或者输出量很大,最好在一个单独的线程中读取输入流和错误流,以避免阻塞主线程。
  4. 考虑超时:对于可能长时间运行的命令,一定要设置超时机制,防止程序无限期等待。ProcessBuilder 本身不直接支持,需要结合 FutureExecutorService,或者直接使用 Commons Exec
  5. 处理平台差异:注意不同操作系统(Windows vs. Linux/macOS)命令和参数的差异,代码中需要做判断或使用跨平台工具(如shcmd)。
分享:
扫描分享到社交APP
上一篇
下一篇