以前写过一篇文章,介绍通过 Java 的 Runtime 类执行操作系统命令行程序:Java调用linux系统shell执行命令。最近项目中又有需要用这个方法,在使用过程中遇到了一些新的问题,感觉以前没有弄清楚,故在此做补充学习记录。
先说明一下这次的需求,在 Java 程序中控制 Hadoop 命令执行 MapReduce 作业,并获取其输出内容。本来没有什么特殊,但由于 MR 执行的是 Kmeans 算法,会产递归产生多个 MR 程序,在捕获输出的时候就只有简单的几句提示,没有 MR 作业的详细信息。
经过查询后发现,执行过程中有一部分信息是作为普通信息输出的,另一部分则是作为 debug 信息输出的。如下图,用红线圈出来一行为普通输出(开始捕获到的只有这一行),其它均为 debug 信息。
为了区别这两种信息,这里就要说一下 Runtime 这个东西了。 Java API 中对该类的解释是 public class java.lang.Runtime extends java.lang.Object。
每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。可以通过 getRuntime 方法获取当前运行时。应用程序不能创建自己的 Runtime 类实例。在API 文档 可以看到,执行过一个命令后,系统会返回一个新的 Process 对象,用于管理子进程。那么,这个新的 Process 对象又是何许东东,继续来看 API ,public abstract class java.lang.Process extends java.lang.Object。该实例可用来控制进程并获得相关信息。
并且,Process 类提供了执行从进程输入、执行输出到进程、等待进程完成、检查进程的退出状态以及销毁(杀掉)进程的方法。
它的所有标准 io(即 stdin、stdout 和 stderr)操作都将通过三个流 (getOutputStream()、getInputStream() 和 getErrorStream()) 重定向到父进程。
父进程使用这些流来提供到子进程的输入和获得从子进程的输出。因为有些本机平台仅针对标准输入和输出流提供有限的缓冲区大小,如果读写子进程的输出流或输入流迅速出现失败,则可能导致子进程阻塞,甚至产生死锁。
现在问题就明显了。以前只获取了 Process.getInputStream() 的 InputStream 对象,没有用 process.getErrorStream() 获取进程的错误输出流传送的数据。
本来是想通过 while 循环来输出两个流的数据,但是发现在利用 BufferedReader 读取 Process stream 流的时候,当使 stream 为空(还没产生,并不是 null ),那么 BufferedReader 则会被阻塞。
所以想了一下还是得用多线程的方式同时输出两个流的内容。
那么,代码更新一下,变成如下的方式。
package com.cz.shell;
import java.io.BufferedReader;
import java.io.InputStreamReader;
class CzStreamOutput extends Thread {
public BufferedReader br;
public CzStreamOutput() {
}
public CzStreamOutput(BufferedReader br) {
this.br = br;
}
public void run() {
String line;
try {
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class CzShell {
public static void runShell(String cmd) throws Exception {
Process process = null;
try {
process = Runtime.getRuntime().exec(cmd);
BufferedReader bri = new BufferedReader(new InputStreamReader(
process.getInputStream()));
BufferedReader bre = new BufferedReader(new InputStreamReader(
process.getErrorStream()));
new CzStreamOutput(bri).start();
new CzStreamOutput(bre).start();
process.waitFor();
bri.close();
bre.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
process.destroy();
}
}
}
在 Linux (Centos 6.2)上运行测试,一切顺利。不过在此说明一下,由于是多线程,理论上会出现一些错误流和输出流顺序颠倒的情况,但是对于多数命令来说,系统的时钟频率要比命令执行时两个流的间隔高很多,出现混乱的可能性极小。