杰瑞科技汇

Java如何执行Linux命令行?

核心概念

在 Java 中执行系统命令,本质上是启动一个新的操作系统进程来运行指定的命令,Java 通过 java.lang.Process 类来表示这个新进程,我们需要做的就是:

Java如何执行Linux命令行?-图1
(图片来源网络,侵删)
  1. 启动进程:告诉 JVM 要执行哪个命令。
  2. 获取输入/输出流:向进程的输入流发送数据,从进程的标准输出流和错误流读取数据。
  3. 等待进程结束:检查进程的退出状态码,确保命令执行完成。

Runtime.exec() (最基础但不推荐)

这是最传统、最直接的方法。Runtime 类提供了一个 exec() 方法来执行命令。

示例代码

import java.io.BufferedReader;
import java.io.InputStreamReader;
public class RuntimeExecExample {
    public static void main(String[] args) {
        String command = "ls -l /etc/hosts"; // 示例命令:列出/etc/hosts文件信息
        try {
            // 1. 获取Runtime实例
            Runtime runtime = Runtime.getRuntime();
            // 2. 执行命令,返回一个Process对象
            Process process = runtime.exec(command);
            // 3. 读取命令的标准输出
            // 使用InputStreamReader和BufferedReader来高效读取
            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. 读取命令的错误输出(非常重要!)
            // 如果不读取错误流,当命令输出大量错误信息时,缓冲区可能会满,导致进程阻塞
            BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            System.out.println("\n--- Command Error (if any) ---");
            while ((line = errorReader.readLine()) != null) {
                System.err.println(line);
            }
            // 5. 等待命令执行完成,并获取退出码
            int exitCode = process.waitFor();
            System.out.println("\n--- Command Finished with Exit Code: " + exitCode + " ---");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

优点

  • 简单直接,无需额外依赖。

缺点 (非常关键!)

  1. 缓冲区阻塞:这是 Runtime.exec() 最经典的问题,如果子进程产生的标准输出或错误输出流没有被及时读取,缓冲区可能会被填满,导致子进程阻塞,进而导致主线程也永久阻塞,上面的代码通过 BufferedReader 循环读取解决了这个问题。
  2. 命令拼接复杂:当命令参数中包含空格或特殊字符时,直接拼接字符串非常容易出错。exec("rm -rf /some path/with spaces") 会失败,因为JVM会将其解析为三个独立的参数。
  3. 跨平台性差Runtime.exec() 的行为在不同操作系统上可能不一致,例如路径分隔符( vs \)、命令格式等。
  4. 难以处理复杂的交互:如果命令需要交互式输入(如输入密码),处理起来非常麻烦。

ProcessBuilder (推荐,现代标准)

从 Java 1.5 开始,引入了 ProcessBuilder 类,它是对 Runtime.exec() 的改进,是执行系统命令的首选方式

示例代码

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
public class ProcessBuilderExample {
    public static void main(String[] args) {
        // 1. 将命令和参数拆分成一个列表,避免字符串拼接问题
        // 注意:每个参数都是一个独立的字符串
        ProcessBuilder pb = new ProcessBuilder("ls", "-l", "/etc/hosts");
        // 2. 可以设置工作目录
        // pb.directory(new File("/tmp"));
        // 3. 重定向输入、输出和错误流
        // 将错误流合并到标准输出流中,方便统一处理
        pb.redirectErrorStream(true);
        try {
            // 4. 启动进程
            Process process = pb.start();
            // 5. 读取输出(由于redirectErrorStream=true,错误信息也会在这里读到)
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            System.out.println("--- Command Output (with Error) ---");
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            // 6. 等待进程结束
            int exitCode = process.waitFor();
            System.out.println("\n--- Command Finished with Exit Code: " + exitCode + " ---");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ProcessBuilder 的优点

  1. 更安全:通过列表传递命令和参数,自动处理了空格和特殊字符,避免了命令注入风险。
  2. 更灵活
    • 可以轻松设置工作目录 (pb.directory())。
    • 可以方便地重定向输入、输出和错误流 (pb.redirectInput(), pb.redirectOutput(), pb.redirectError(), pb.redirectErrorStream(true))。
  3. 更健壮:设计上比 Runtime.exec() 更好,避免了部分常见的陷阱。

高级用法:处理交互式命令

有些命令(如 sudo, passwd, ssh)需要与用户进行交互,这需要我们向进程的输入流写入数据,并从输出流读取提示信息。

示例:模拟 echo "hello world" 并通过管道 | wc -c 计算字符数

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
public class InteractiveCommandExample {
    public static void main(String[] args) {
        try {
            // 使用ProcessBuilder构建命令
            ProcessBuilder pb = new ProcessBuilder("sh", "-c", "echo \"hello world\" | wc -c");
            Process process = pb.start();
            // 获取进程的输入流,用于向进程写入数据
            // InputStream inputStream = process.getOutputStream(); // 对于echo命令,我们不需要向它写东西
            // 获取进程的输出流,用于读取进程的输出
            InputStream inputStream = process.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            System.out.println("--- Interactive Command Output ---");
            while ((line = reader.readLine()) != null) {
                System.out.println(line); // 应该会输出 "13"
            }
            int exitCode = process.waitFor();
            System.out.println("\n--- Command Finished with Exit Code: " + exitCode + " ---");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

第三方库:Apache Commons Exec

如果执行命令的逻辑非常复杂(例如需要超时控制、更复杂的流处理、更好的日志集成等),使用第三方库是一个非常好的选择。Apache Commons Exec 是最流行的库之一。

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

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.StreamHandler;
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");
        DefaultExecutor executor = new DefaultExecutor();
        // 设置超时时间 (毫秒)
        executor.setWorkingDirectory(new File("/etc"));
        executor.setExitValue(0); // 正常退出的值
        // 使用PumpStreamHandler来处理输入输出,可以捕获到String
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
        StreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
        executor.setStreamHandler(streamHandler);
        try {
            System.out.println("--- Executing with Apache Commons Exec ---");
            int exitCode = executor.execute(cmdLine);
            System.out.println("--- Command 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 (IOException e) {
            e.printStackTrace();
        }
    }
}

优点

  • 功能强大:内置超时控制、更完善的流处理、命令解析等。
  • 代码更简洁:封装了底层细节,提供了更高级的API。
  • 可维护性高:对于复杂的命令执行场景,代码更清晰。

总结与最佳实践

方法 优点 缺点 适用场景
Runtime.exec() 无需依赖,最基础 有阻塞风险,命令拼接易错,跨平台差 简单、一次性、无输出的命令(不推荐用于生产)
ProcessBuilder 推荐,安全,灵活,功能齐全 Java 1.5+ 绝大多数场景下的首选,是Java执行命令的标准方式。
Apache Commons Exec 功能强大(超时、流处理等),代码优雅 需要引入第三方依赖 复杂的命令执行逻辑,需要高级功能(如超时控制)的项目。

最佳实践建议:

  1. 首选 ProcessBuilder:对于99%的情况,直接使用 ProcessBuilder 就足够了,它安全、灵活且是Java官方推荐的方式。
  2. 始终处理输出流:无论是标准输出还是错误输出,都要有地方去读取,如果不关心,可以将其重定向到 null/dev/null,以避免缓冲区阻塞。
  3. 使用列表传递命令:将命令和参数拆分成一个 List<String> 或数组,避免字符串拼接带来的问题。
  4. 检查退出码:使用 process.waitFor() 获取退出码,以判断命令是否成功执行。
  5. 考虑超时:对于可能长时间运行的命令,务必设置超时,否则你的程序可能会被无限期挂起。ProcessBuilder 本身不直接支持,但可以结合 Threadprocess.destroy() 来实现,或者直接使用 Apache Commons Exec
  6. 处理异常:妥善处理 IOExceptionInterruptedException
Java如何执行Linux命令行?-图3
(图片来源网络,侵删)
分享:
扫描分享到社交APP
上一篇
下一篇