杰瑞科技汇

Java进程输出如何实时捕获?

处理其输出(包括标准输出 stdout 和标准错误 stderr)是至关重要的,主要有以下几个原因:

Java进程输出如何实时捕获?-图1
(图片来源网络,侵删)
  1. 获取结果:许多命令的执行结果都输出到标准输出中。
  2. 错误诊断:标准错误流用于输出错误信息、警告或调试信息。stderr 缓冲区满了,外部进程可能会阻塞,导致你的 Java 程序也挂起。
  3. 防止死锁:这是最关键也最容易出错的一点,如果子进程产生了大量输出,而你的 Java 程序没有及时读取,子进程会等待操作系统缓冲区有空间,从而进入阻塞状态,如果父进程也在等待子进程结束,那么就会形成“死锁”。

下面我将从不同层面和场景,为你详细讲解如何处理 Java Process 的输出。


核心概念:标准流

一个进程通常有三个标准流:

  • 标准输入InputStream,用于向进程输入数据。
  • 标准输出OutputStream,进程通过它输出正常信息。
  • 标准错误OutputStream,进程通过它输出错误信息。

在 Java 中,Process 对象提供了获取这些流的方法:

  • InputStream getInputStream():获取子进程的标准输出
  • InputStream getErrorStream():获取子进程的标准错误
  • OutputStream getOutputStream():获取子进程的标准输入

基础用法与陷阱(不推荐的方式)

初学者可能会写出如下代码,这种方式在大多数情况下是错误的,因为它极易导致死锁。

Java进程输出如何实时捕获?-图2
(图片来源网络,侵删)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class BadProcessExample {
    public static void main(String[] args) {
        try {
            // 启动一个进程,"ping" 命令,它会持续输出
            Process process = Runtime.getRuntime().exec("ping -c 5 google.com");
            // 错误示范:先等待进程结束,再读取输出
            int exitCode = process.waitFor(); // 阻塞在这里,等待进程结束
            // 只有在进程结束后,才能读取输出
            System.out.println("Process exited with code: " + exitCode);
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println("[STDOUT] " + line);
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

为什么这个例子是错的?

假设 ping 命令产生了大量的输出,超出了操作系统的输出缓冲区大小。ping 进程会尝试写入 stdout,但缓冲区已满,ping 进程会阻塞,等待 Java 程序读取缓冲区以释放空间。

你的 Java 程序正在 process.waitFor()阻塞,等待 ping 进程结束,这就形成了一个死锁:

  • ping 进程在等待 Java 读取输出。
  • Java 进程在等待 ping 进程结束。

正确的输出处理方式

为了正确处理输出,必须遵循一个核心原则:在等待进程结束之前,必须启动独立的线程来持续读取 stdoutstderr

Java进程输出如何实时捕获?-图3
(图片来源网络,侵删)

使用 ProcessBuilderThread(经典可靠)

这是最经典、最可控的方式,适用于任何 Java 版本,我们为 stdoutstderr 分别创建一个线程来读取。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class GoodProcessExample {
    // 用于读取输入流的辅助类
    private static class StreamGobbler implements Runnable {
        private final InputStream inputStream;
        private final String type;
        public StreamGobbler(InputStream inputStream, String type) {
            this.inputStream = inputStream;
            this.type = type;
        }
        @Override
        public void run() {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    // 根据类型打印到不同的地方,例如控制台或日志文件
                    System.out.println("[" + type + "] " + line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        try {
            // 推荐使用 ProcessBuilder
            ProcessBuilder pb = new ProcessBuilder("ping", "-c", "5", "google.com");
            pb.redirectErrorStream(false); // false 表示分别处理 stdout 和 stderr
            Process process = pb.start();
            // 创建并启动线程来读取标准输出
            Thread stdoutThread = new Thread(new StreamGobbler(process.getInputStream(), "STDOUT"));
            // 创建并启动线程来读取标准错误
            Thread stderrThread = new Thread(new StreamGobbler(process.getErrorStream(), "STDERR"));
            stdoutThread.start();
            stderrThread.start();
            // 等待进程执行完成
            int exitCode = process.waitFor();
            System.out.println("\nProcess finished with exit code: " + exitCode);
            // 等待输出读取线程也结束(确保所有输出都被处理)
            stdoutThread.join();
            stderrThread.join();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

优点:

  • 可靠:彻底避免了死锁问题。
  • 灵活:可以分别处理 stdoutstderr,例如将错误信息记录到日志文件,而将正常信息打印到控制台。
  • 兼容性好:适用于所有 Java 版本。

合并 stdoutstderr(简化处理)

如果你不关心 stdoutstderr 的来源,希望将它们混合在一起处理,可以使用 ProcessBuilder.redirectErrorStream(true)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class MergedStreamExample {
    public static void main(String[] args) {
        try {
            ProcessBuilder pb = new ProcessBuilder("sh", "-c", "ls /nonexistent; echo 'This is a normal message'");
            // 将标准错误流合并到标准输出流
            pb.redirectErrorStream(true);
            Process process = pb.start();
            // 只需要一个线程来读取合并后的流
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    System.out.println(line); // 所有输出(包括错误)都在这里
                }
            }
            int exitCode = process.waitFor();
            System.out.println("\nProcess finished with exit code: " + exitCode);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行结果可能会是:

ls: cannot access '/nonexistent': No such or directory
This is a normal message
Process finished with exit code: 0

优点:

  • 简单:只需要管理一个输入流,代码更简洁。

缺点:

  • 信息丢失:无法区分哪些是标准输出,哪些是标准错误,不利于精确的错误处理和日志分析。

Java 9+ 的 Process API 改进

从 Java 9 开始,Process 类增加了新方法,使得处理输出变得更加方便。

1 ProcessHandle

可以更方便地获取和管理进程信息。

Process process = pb.start();
ProcessHandle processHandle = process.toHandle();
System.out.println("PID: " + processHandle.pid());

2 isAlive()

检查进程是否还在运行。

while (process.isAlive()) {
    // 做一些其他事情
}

3 更优雅的输出处理(Java 11+)

Java 11 引入了一些新方法,如 reader(),使得 InputStream 可以直接被当作 BufferedReader 使用,简化了代码。

import java.io.BufferedReader;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
public class Java11ProcessExample {
    public static void main(String[] args) {
        try {
            ProcessBuilder pb = new ProcessBuilder("ping", "-c", "3", "localhost");
            Process process = pb.start();
            // 使用 CompletableFuture 异步处理输出
            CompletableFuture<String> futureOutput = CompletableFuture.supplyAsync(() -> {
                // Java 11+ 的 reader() 方法非常方便
                try (BufferedReader reader = process.inputReader()) {
                    return reader.lines().reduce("", (s1, s2) -> s1 + "\n" + s2);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            // 可以同时异步处理错误流...
            // ...
            System.out.println("Waiting for process to complete...");
            int exitCode = process.waitFor();
            System.out.println("Process finished with exit code: " + exitCode);
            // 获取合并后的输出
            String output = futureOutput.get(); // .get() 会等待 future 完成
            System.out.println("--- Full Output ---");
            System.out.println(output);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这种方式结合了现代的异步编程模型,代码更简洁,但需要 Java 11 或更高版本。


将输出重定向到文件

如果你不需要在 Java 程序中实时处理输出,而是希望将其保存到文件,可以使用 ProcessBuilder 的重定向功能,这非常高效,因为它完全由操作系统处理,避免了 Java 程序的 I/O 开销。

import java.io.File;
import java.io.IOException;
public class RedirectToFileExample {
    public static void main(String[] args) {
        try {
            ProcessBuilder pb = new ProcessBuilder("ls", "-l", "/");
            // 将标准输出重定向到文件
            File outputFile = new File("output.log");
            pb.redirectOutput(outputFile);
            // 将标准错误也重定向到同一个文件(或另一个文件)
            // pb.redirectError(outputFile);
            // 或者重定向到标准输出
            pb.redirectErrorStream(true);
            Process process = pb.start();
            int exitCode = process.waitFor();
            System.out.println("Command executed. Output saved to " + outputFile.getAbsolutePath());
            System.out.println("Process exited with code: " + exitCode);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行后, 目录下的文件列表就会被写入到 output.log 文件中。


总结与最佳实践

场景 推荐方法 优点 缺点
需要实时处理输出 ProcessBuilder + Thread 可靠、灵活、兼容性好 代码稍显繁琐
不区分 stdoutstderr ProcessBuilder.redirectErrorStream(true) + 单线程 代码简洁 无法区分信息来源
Java 11+ 环境 Process.inputReader() + CompletableFuture 代码现代、简洁 需要 Java 11+
只需保存到文件,不关心内容 ProcessBuilder.redirectOutput() 性能最高,由 OS 处理 无法在 Java 中获取输出内容
诊断死锁问题 检查 waitFor() 是否在读取流之前 - -

核心最佳实践:

  1. 首选 ProcessBuilder:它比 Runtime.exec() 更强大、更灵活。
  2. 永远不要在 waitFor() 之前不读取输出流:这是导致死锁最常见的原因。
  3. stdoutstderr 使用独立的消费者线程:这是最健壮、最通用的解决方案。
  4. 考虑重定向:如果只是需要记录日志,重定向到文件是最高效的选择。
  5. 利用新 API:如果你的项目使用 Java 9+,请优先使用 ProcessHandlereader() 等新特性来简化代码。
分享:
扫描分享到社交APP
上一篇
下一篇