杰瑞科技汇

Java Linux如何调用Shell命令?

下面我将详细介绍几种主流的方法,从最简单到最强大,并分析它们的优缺点和适用场景。

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

核心思路

无论使用哪种方法,Java 调用 Shell 的基本流程都是相似的:

  1. 获取 Runtime 实例Runtime.getRuntime() 是一个单例方法,用于与 Java 运行时环境交互。
  2. 执行命令:通过 Runtimeexec() 方法来启动一个新的进程并执行命令。
  3. 处理输入/输出/错误流:这是最关键也最容易出错的一步,子进程有自己的标准输入、标准输出和标准错误流,如果这些流不被及时读取,可能会导致子进程阻塞,甚至死锁。
  4. 等待进程结束:调用 Process.waitFor() 来等待命令执行完成,并获取其退出码。

Runtime.exec() - 基础方法

这是最直接的方法,但也是最容易出现问题的方法。exec() 方法有多个重载版本。

执行单个命令

import java.io.BufferedReader;
import java.io.InputStreamReader;
public class SimpleShellCommand {
    public static void main(String[] args) {
        try {
            // 1. 执行命令
            Process process = Runtime.getRuntime().exec("ls -l");
            // 2. 读取命令的标准输出
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            System.out.println("Command Output:");
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            // 3. 读取命令的标准错误(可选,但推荐)
            BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            System.out.println("\nCommand Error (if any):");
            while ((line = errorReader.readLine()) != null) {
                System.err.println(line);
            }
            // 4. 等待命令执行完成
            int exitCode = process.waitFor();
            System.out.println("\nCommand finished with exit code: " + exitCode);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

⚠️ 重要注意事项:

  • 流处理:上面的代码中,我们使用 BufferedReader 来逐行读取输出流,如果命令输出很大,直接读取 InputStream 而不进行缓冲,可能会导致性能问题,更严重的是,如果不读取子进程的输出流,当缓冲区满时,子进程会阻塞,等待 Java 程序读取,从而导致 Java 程序也卡住,这就是所谓的“死锁”。
  • 命令作为字符串exec("ls -l") 看起来简单,但底层会调用系统的 sh -c 来解析这个字符串,如果命令字符串中包含复杂的 shell 特殊字符(如 >, <, , &),它们可能会被 shell 解释,也可能不会,行为不确定。

执行带参数的命令(推荐方式)

为了避免 shell 解析的歧义,最好将命令和它的参数作为数组传递给 exec()

Java Linux如何调用Shell命令?-图2
(图片来源网络,侵删)
public class CommandWithArgs {
    public static void main(String[] args) {
        try {
            // 将命令和参数分开,更安全、更清晰
            String[] command = { "ls", "-l", "/tmp" };
            Process process = Runtime.getRuntime().exec(command);
            // 合并输出和错误流到一个地方处理
            // 使用 StreamGobbler 是一个好习惯,可以避免死锁
            StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(), System.out::println);
            StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), System.err::println);
            outputGobbler.start();
            errorGobbler.start();
            int exitCode = process.waitFor();
            outputGobbler.join(); // 等待线程结束
            errorGobbler.join();
            System.out.println("\nCommand finished with exit code: " + exitCode);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    // 一个辅助类,用于消费输入流,防止缓冲区满导致阻塞
    private static class StreamGobbler extends Thread {
        private final InputStream inputStream;
        private final java.util.function.Consumer<String> consumer;
        public StreamGobbler(InputStream inputStream, java.util.function.Consumer<String> consumer) {
            this.inputStream = inputStream;
            this.consumer = consumer;
        }
        @Override
        public void run() {
            new BufferedReader(new InputStreamReader(inputStream)).lines()
                .forEach(consumer);
        }
    }
}

使用 ProcessBuilder - 更强大、更推荐

ProcessBuilder 是 Java 5 引入的,它比 Runtime.exec() 更强大、更灵活,是现在执行外部命令的首选方式

主要优势:

  1. 命令和参数分离:构造函数直接接受一个 List<String>String[],无需 shell 解析,更安全。
  2. 轻松重定向流:可以轻松地将子进程的输入、输出、错误流重定向到文件或当前进程的流。
  3. 设置工作目录:可以指定命令执行时的工作目录。
  4. 管理环境变量:可以修改子进程的环境变量。

示例代码:

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.List;
public class ProcessBuilderExample {
    public static void main(String[] args) {
        // 1. 准备命令和参数
        List<String> command = Arrays.asList("sh", "-c", "for i in {1..5}; do echo \"Hello $i\"; sleep 1; done");
        // 2. 创建 ProcessBuilder 实例
        ProcessBuilder pb = new ProcessBuilder(command);
        // 3. (可选) 设置工作目录
        pb.directory(new File("/tmp"));
        // 4. (可选) 合并错误流到输出流,这样只需要读取一个流
        pb.redirectErrorStream(true);
        try {
            // 5. 启动进程
            Process process = pb.start();
            // 6. 读取输出
            // 因为 redirectErrorStream(true),所以这里同时捕获了标准输出和标准错误
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            System.out.println("Command Output:");
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            // 7. 等待进程结束
            int exitCode = process.waitFor();
            System.out.println("\nCommand finished with exit code: " + exitCode);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ProcessBuilder 的流处理:

  • 不处理流(最危险):如果既不读取也不重定向,缓冲区满会阻塞。
  • 读取流:像上面例子一样,使用线程读取。StreamGobbler 类在这里同样适用。
  • 重定向到文件pb.redirectOutput(new File("output.log"));,这是最安全的方式之一,尤其适用于输出量大的命令。
  • 重定向到当前进程的流pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);,这会让子进程的输出直接打印到你的控制台,非常方便用于调试。

使用第三方库(如 Apache Commons Exec)

如果你的需求比较复杂,或者希望代码更简洁、健壮,可以考虑使用第三方库。Apache Commons Exec 是一个非常流行的选择。

优点:

  • 更高级的 API:提供了更直观的 CommandLineExecutor 接口。
  • 超时控制:可以轻松地为命令执行设置超时时间。
  • 更好的流处理:内置了流处理器,可以方便地将输出流重定向到 Logger 或其他目标。
  • 更完善的错误处理

示例代码(需要添加 commons-exec 依赖):

<!-- 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 = CommandLine.parse("df -h");
        DefaultExecutor executor = new DefaultExecutor();
        // 设置超时时间为 10 秒
        executor.setExitValue(0); // 正常退出的状态码
        // executor.setWorkingDirectory(...); // 设置工作目录
        // 使用 PumpStreamHandler 来处理输出和错误流
        // 将输出捕获到 ByteArrayOutputStream
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
        PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
        executor.setStreamHandler(streamHandler);
        try {
            System.out.println("Executing command: " + cmdLine);
            int exitCode = executor.execute(cmdLine);
            System.out.println("\nCommand 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 (ExecuteException e) {
            // 命令执行失败,非正常退出
            System.err.println("Execution failed with exit code: " + e.getExitValue());
            System.err.println("Error output: " + errorStream.toString());
        } catch (IOException e) {
            // IO异常,例如无法启动进程
            e.printStackTrace();
        }
    }
}

总结与对比

特性 Runtime.exec() ProcessBuilder Apache Commons Exec
易用性 简单,但容易出错 中等,API 更清晰 高,API 设计优秀
灵活性 (流重定向、工作目录、环境变量) (超时、流处理器等高级功能)
健壮性 低(易死锁) 中(需要手动处理流) (内置了健壮的流处理和超时机制)
推荐场景 简单、一次性、低风险的命令 大多数场景下的首选,功能全面 复杂脚本、需要超时控制、追求代码健壮性和可维护性的项目
依赖 无(JDK 内置) 无(JDK 内置) 需要添加 commons-exec 依赖

最终建议

  1. 对于简单的命令:如果只是执行一个简单的、无输出的命令,或者能确保输出量很小,可以直接使用 ProcessBuilder 并读取流。
  2. 对于大多数情况强烈推荐使用 ProcessBuilder,它是 Java 标准库的一部分,功能强大,足以应对 95% 的场景,记得使用 StreamGobbler 或重定向到文件来处理输出流,以避免死锁。
  3. 对于复杂项目:如果你的项目已经依赖了 Apache Commons,或者你需要处理复杂的命令行逻辑、超时、日志集成等,那么使用 Apache Commons Exec 会让你的代码更简洁、更可靠。

无论选择哪种方法,请务必记住:处理好子进程的输入、输出和错误流,是成功调用 Shell 命令的关键。

Java Linux如何调用Shell命令?-图3
(图片来源网络,侵删)
分享:
扫描分享到社交APP
上一篇
下一篇