杰瑞科技汇

Java如何执行Linux命令?

Java 提供了多种方式来实现这一功能,每种方式都有其适用场景,下面我将从最基础到最推荐的方式,为你详细讲解。

Java如何执行Linux命令?-图1
(图片来源网络,侵删)

核心思想

无论使用哪种方式,其基本原理都是相同的:

  1. 获取运行时环境:通过 Runtime 类获取与当前 Java 进程关联的操作系统运行时环境。
  2. 创建进程:调用 exec() 方法,并传入要执行的命令字符串,这将启动一个新的进程(子进程)来运行该命令。
  3. 管理进程:获取代表这个新进程的 Process 对象,并处理它的输入流、输出流和错误流。
  4. 获取结果:等待进程执行完毕,并获取其返回值(退出码)以判断命令是否成功执行。

使用 Runtime.exec() (最基础但不推荐直接使用)

这是最传统、最直接的方法,但直接使用 Runtime.exec() 存在严重的陷阱,不推荐在生产环境中直接使用,除非你非常清楚其所有细节。

最简单的用法 (极易出错)

public class SimpleCommand {
    public static void main(String[] args) {
        try {
            // 执行一个简单的命令,如列出当前目录文件
            Process process = Runtime.getRuntime().exec("ls -l");
            // 等待命令执行完成
            int exitCode = process.waitFor();
            System.out.println("命令执行完毕,退出码: " + exitCode);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

问题所在: 上面的代码几乎肯定无法正常工作,或者会卡住,原因在于子进程的输出流没有被读取,如果子进程产生了大量输出,这些数据会填满其内部的缓冲区,当缓冲区满时,子进程会等待,直到父进程(Java 程序)读取输出,而父进程又在 waitFor() 中等待子进程结束,从而形成了死锁

正确的用法 (必须处理输入/输出流)

为了避免死锁,你必须并发地读取子进程的标准输出流错误输出流

Java如何执行Linux命令?-图2
(图片来源网络,侵删)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class CorrectRuntimeExec {
    public static void main(String[] args) {
        // 要执行的命令
        String[] command = {
            "/bin/sh",  // 指定 shell
            "-c",       // -c 表示后面跟着的是命令字符串
            "ls -l /nonexistent" // 这个命令会出错,以便测试错误流
        };
        try {
            // 启动进程
            Process process = Runtime.getRuntime().exec(command);
            // --- 关键部分:并发读取输出流和错误流 ---
            // 读取标准输出
            InputStream inputStream = process.getInputStream();
            BufferedReader inputReader = new BufferedReader(new InputStreamReader(inputStream));
            // 读取错误输出
            InputStream errorStream = process.getErrorStream();
            BufferedReader errorReader = new BufferedReader(new InputStreamReader(errorStream));
            // 使用线程来分别读取,避免死锁
            Thread outputThread = new Thread(() -> {
                String line;
                try {
                    while ((line = inputReader.readLine()) != null) {
                        System.out.println("[OUTPUT] " + line);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            Thread errorThread = new Thread(() -> {
                String line;
                try {
                    while ((line = errorReader.readLine()) != null) {
                        System.err.println("[ERROR] " + line);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            outputThread.start();
            errorThread.start();
            // 等待命令执行完成
            int exitCode = process.waitFor();
            // 等待输出和错误读取线程完成
            outputThread.join();
            errorThread.join();
            System.out.println("\n命令执行完毕,退出码: " + exitCode);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

代码解析

  1. String[] command:将命令拆分成字符串数组是更健壮的方式,它可以正确处理包含空格或特殊字符的参数,并避免了 shell 注入的风险。
  2. /bin/sh -c:我们显式地调用 shell (/bin/sh),并使用 -c 参数来执行一个命令字符串,这样做的好处是,可以使用 shell 的特性,如管道 ()、重定向 (>)、通配符 () 等。
  3. InputStreamBufferedReader:子进程的输出通过流(InputStream)返回,使用 BufferedReader 可以高效地按行读取。
  4. 多线程读取:这是避免死锁的核心,我们创建两个独立的线程,一个读取标准输出,一个读取标准错误,这样就不会因为一个流阻塞而影响另一个。
  5. process.waitFor():此方法会阻塞当前线程,直到子进程执行完毕。
  6. thread.join():确保在主线程继续执行之前,输出和错误读取线程已经完成工作。

使用 ProcessBuilder (强烈推荐)

ProcessBuilder 是 Java 5 引入的,是 Runtime.exec() 的现代替代品,它提供了更强大、更灵活且更易于管理的功能。

优点

  • 更灵活:可以方便地设置工作目录、环境变量。
  • 更安全:直接接受命令和参数列表,减少了 shell 注入的风险。
  • 流管理更方便:可以合并错误流到输出流,简化了代码。

示例代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class ProcessBuilderExample {
    public static void main(String[] args) {
        // 1. 创建 ProcessBuilder 实例,并设置命令
        // 同样推荐使用数组形式
        ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", "ls -l /etc/hosts");
        // 2. (可选) 设置工作目录
        // pb.directory(new File("/path/to/your/directory"));
        // 3. (可选) 合并错误流到输出流
        // 这样你只需要读取一个流即可,简化了多线程逻辑
        pb.redirectErrorStream(true);
        try {
            // 4. 启动进程
            Process process = pb.start();
            // 5. 读取输出 (因为错误流已合并,所以这里读取的是所有输出)
            InputStream inputStream = process.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            System.out.println("--- 命令输出 ---");
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            // 6. 等待进程结束并获取退出码
            int exitCode = process.waitFor();
            System.out.println("\n--- 命令执行完毕,退出码: " + exitCode);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

代码解析

Java如何执行Linux命令?-图3
(图片来源网络,侵删)
  1. new ProcessBuilder(...):直接传入命令和参数列表。
  2. pb.redirectErrorStream(true):这是 ProcessBuilder 的一个巨大优势,将标准错误流重定向到标准输出流,这样,无论是命令的正常输出还是错误信息,都会被发送到同一个 InputStream 中,你只需要一个线程来读取,大大简化了代码,并从根本上避免了死锁问题。
  3. pb.start():启动进程,返回 Process 对象。

使用第三方库 (更高级的封装)

如果你需要更高级的功能,比如更优雅的 API、超时控制、异步执行等,可以考虑使用第三方库。

Apache Commons Exec

这是一个非常流行且功能强大的库,专门用于在 Java 中执行外部进程。

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 = new CommandLine("/bin/sh");
        cmdLine.addArgument("-c");
        cmdLine.addArgument("ls -l /tmp && echo 'Command finished successfully'");
        DefaultExecutor executor = new DefaultExecutor();
        // 设置超时时间 (5秒)
        executor.setWorkingDirectory(new java.io.File("/tmp"));
        executor.setExitValue(0); // 认为退出码为 0 的命令是成功的
        // 使用 PumpStreamHandler 来捕获输出
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
        PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
        executor.setStreamHandler(streamHandler);
        try {
            System.out.println("执行命令: " + cmdLine);
            int exitCode = executor.execute(cmdLine);
            System.out.println("\n--- 标准输出 ---");
            System.out.println(outputStream.toString());
            System.out.println("\n--- 标准错误 ---");
            System.out.println(errorStream.toString());
            System.out.println("\n命令执行成功,退出码: " + exitCode);
        } catch (ExecuteException e) {
            // 命令执行失败 (退出码不为0)
            System.err.println("命令执行失败,退出码: " + e.getExitValue());
            System.err.println("错误信息: " + errorStream.toString());
        } catch (IOException e) {
            // 启动进程或 I/O 时出错
            e.printStackTrace();
        }
    }
}

优点

  • API 友好CommandLine 类让构建命令变得非常直观。
  • 超时控制Executor 可以轻松设置超时。
  • 流处理封装PumpStreamHandler 简化了输入/输出的处理。
  • 更丰富的功能:支持进程销毁、监控等。

总结与最佳实践

特性 Runtime.exec() ProcessBuilder Apache Commons Exec
易用性 低,陷阱多 高,API 清晰 非常高,功能强大
灵活性 高,可设置目录、环境变量 非常高
安全性 低,易受 shell 注入 较高,推荐使用数组形式 高,API 设计安全
流管理 复杂,必须手动多线程读取 简单,redirectErrorStream(true) 一键解决 简单,PumpStreamHandler 封装得很好
额外功能 超时控制、异步执行等
推荐场景 不推荐,除非在极简环境或旧代码维护 强烈推荐,适用于绝大多数标准 Java 项目 企业级应用,需要高级功能和健壮性

最佳实践建议

  1. 首选 ProcessBuilder:对于 99% 的场景,ProcessBuilder 是最佳选择,它解决了 Runtime.exec() 的主要痛点,并且是 Java 标准库的一部分,无需引入额外依赖。
  2. 始终处理输出流:无论使用哪种方法,都要确保读取子进程的输出流和错误流,否则可能导致死锁或内存泄漏。
  3. 使用命令数组:将命令拆分为字符串数组 (String[]),而不是一个长字符串,这样可以避免 shell 注入和参数解析问题。
  4. 考虑第三方库:如果你的应用对进程执行有严格要求(如必须设置超时、需要异步回调等),那么投入精力学习和使用 Apache Commons Exec 是完全值得的。
分享:
扫描分享到社交APP
上一篇
下一篇