1. 进程的输入/输出流
当你在 Java 中启动一个外部进程时,程序与该进程之间会打开一个“小型对话”——三条通信通道。
第一条通道是标准输出。外部进程想要显示在“屏幕”上的一切,你都可以通过 Process.getInputStream() 捕获。第二条通道是错误流。如果进程要抱怨或输出错误信息,你可以通过 Process.getErrorStream() 得知。最后一条是标准输入。通过它你可以把数据发送给进程,如果进程在等待你的输入,则可使用 Process.getOutputStream() 来实现。
有点让人困惑的是,这些流的名称似乎“对调”了。对进程而言,InputStream 是你读取的内容(它的输出),而 OutputStream 是你写入的地方(它的输入)。但从你的 Java 程序视角看,这就很合理了:你读取的是进程输出,写入的是你要传递给进程的内容。
进程流示意图
+-------------------+ +---------------------+
| Your program | <-------> | External process |
+-------------------+ +---------------------+
| |
| getOutputStream() --------> stdin
| getInputStream() <-------- stdout
| getErrorStream() <-------- stderr
2. 读取进程输出(stdout)
让我们学习如何读取外部进程输出到屏幕的内容。这可能是命令结果、程序版本、成功消息——任何东西。
示例 1:读取命令 java -version 的输出
先启动进程,然后读取它的输出。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
public class ProcessOutputExample {
public static void main(String[] args) throws IOException {
// 创建进程:命令取决于你的操作系统!
ProcessBuilder pb = new ProcessBuilder("java", "-version");
Process process = pb.start();
// 读取错误流(java -version 通常写到这里!)
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Process output: " + line);
}
}
}
}
注意!命令 java -version 并不会把结果写到普通的 stdout,而是写入错误流 stderr。这不是 bug,而是 Java 的一个特性。因此我们读取 process.getErrorStream()。对于大多数其他命令(例如 echo、ls、dir),请使用 getInputStream()。
示例 2:读取命令 echo 的输出
ProcessBuilder pb = new ProcessBuilder("echo", "Hello, Java!");
Process process = pb.start();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Process output: " + line);
}
}
为什么需要 BufferedReader?
标准输入流是字节流(InputStream)。若要按行读取它,使用 InputStreamReader(把字节转换为字符)+ BufferedReader(方便逐行读取)的组合。
3. 向进程的输入流(stdin)写入
有时外部进程会等待我们的输入。例如,你启动了一个脚本,它要求输入用户名,或者一个计算器在等待数字。
为此使用 process.getOutputStream()——没错,就是这样:你“写入”的是进程的输入流。
示例 3:向进程传递数据
假设你有一个简单的 Python 脚本,它读取一行并原样回显:
echo_input.py:
# echo_input.py
line = input()
print("You said:", line)
从 Java 启动它并发送一行文本:
import java.io.*;
public class ProcessStdinExample {
public static void main(String[] args) throws IOException {
ProcessBuilder pb = new ProcessBuilder("python", "echo_input.py");
Process process = pb.start();
// 向进程的 stdin 写入
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(process.getOutputStream()))) {
writer.write("来自 Java 的问候!\n");
writer.flush();
}
// 读取进程的 stdout
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("进程响应: " + line);
}
}
}
}
别忘了调用 flush()——否则数据可能“卡”在缓冲区里,无法发送到进程。
4. 错误处理:读取 stderr
外部进程不仅会给我们带来结果,也可能会“抱怨人生”——输出错误。如果你不读取错误流,可能会错过重要信息,甚至在错误缓冲区被写满时导致进程阻塞(deadlock)。
示例 4:读取进程的 stderr
ProcessBuilder pb = new ProcessBuilder("java", "-version");
Process process = pb.start();
// 读取错误
try (BufferedReader errorReader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = errorReader.readLine()) != null) {
System.out.println("进程错误: " + line);
}
}
为何要同时读取两个流?
如果进程向 stdout 或 stderr 写入了大量数据,而你不读取这些流,操作系统的缓冲区可能会被填满,进程会“卡住”,等待有人释放空间。因此,良好的做法是并行读取两个流(或至少交替读取)。
5. 实践:启动命令、读取输出、处理错误
让我们把上述内容整合到一个小工具中:它启动外部命令,读取其标准输出和错误,并把所有内容打印到屏幕上。
示例 5:通用命令启动器
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class RunCommand {
public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.out.println("请指定要运行的命令,例如: java RunCommand ls -l");
return;
}
var process = new ProcessBuilder(args).start();
// 分别启动读取 stdout 和 stderr 的线程
var t1 = Thread.ofPlatform().start(() -> readStream(process.inputReader(), "[stdout]"));
var t2 = Thread.ofPlatform().start(() -> readStream(process.errorReader(), "[stderr]"));
int exitCode = process.waitFor(); // 等待进程结束
t1.join();
t2.join();
System.out.println("进程结束,退出码: " + exitCode);
}
private static void readStream(BufferedReader reader, String prefix) {
try (reader) {
reader.lines().forEach(line -> System.out.println(prefix + " " + line));
} catch (Exception e) {
System.err.println("读取 " + prefix + " 时出错: " + e.getMessage());
}
}
}
这种做法可以避免死锁(deadlock),并且不会漏掉任何错误!
6. 关于编码的简要说明
默认情况下,InputStreamReader 使用系统编码。如果外部进程使用了不同的编码(例如它用 UTF-8,而你的系统是 Windows-1251),输出可能会显示为“乱码”。请显式指定编码:
new InputStreamReader(process.getInputStream(), "UTF-8")
7. 示例:启动 Python 脚本并向其传递数据
假设你有一个 Python 脚本,它会从输入中读取多行,然后输出结果。
adder.py:
# adder.py
a = int(input())
b = int(input())
print(a + b)
在 Java 中,你可以把数字发送给它并读取答案:
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
public class PythonAdder {
public static void main(String[] args) throws Exception {
var process = new ProcessBuilder("python", "adder.py").start();
// 向 Python 脚本的 stdin 发送两个数字
try (var writer = process.outputWriter(StandardCharsets.UTF_8)) {
writer.println("10");
writer.println("25");
}
// 使用 inputReader 从 stdout 读取结果
try (BufferedReader reader = process.inputReader(StandardCharsets.UTF_8)) {
reader.lines().forEach(line -> System.out.println("来自 Python 的响应: " + line));
}
int exitCode = process.waitFor();
System.out.println("Python 已结束,退出码: " + exitCode);
}
}
8. 为什么会发生 deadlock?如何避免
Deadlock 是一种进程与程序相互等待、无限期僵持的情况。例如,如果进程的输出缓冲区被写满,而你没有读取该流,进程就会停止。如果你在等待进程结束,而进程则在等待你给它写入点什么——这同样会导致死锁。
如何避免:
- 始终读取两个流(stdout 和 stderr)。
- 对于运行时间长/输出很多的进程,使用单独的线程并行读取。
- 不要在同一线程中同时阻塞写入和读取。
9. 处理进程 I/O 时的常见错误
错误 1:只读取一个流(stdout 或 stderr)。
如果只读取 stdout,而进程把错误写到 stderr,错误缓冲区可能会被写满——进程会挂起。解决方案:读取两个流(最好用独立线程)。
错误 2:编码不正确。
如果没有指定正确的编码,输出可能不可读。务必确认外部进程使用的编码(例如显式指定 "UTF-8" 或使用 StandardCharsets.UTF_8)。
错误 3:未关闭流。
未关闭的流可能导致资源泄漏。使用 try-with-resources 或显式关闭所有流。
错误 4:向进程 stdin 写入时未调用 flush()。
如果忘记调用 flush(),数据可能无法发送到进程——它会一直等待输入。
错误 5:平台差异。
命令和语法在 Windows 与 Linux/Mac 之间可能不同。通过 System.getProperty("os.name") 检查操作系统,并据此选择命令。
错误 6:未处理异常。
与进程交互经常会出现 IOException。务必处理异常,避免程序异常退出。
GO TO FULL VERSION