startByteArrayReaderThread()方法是整个类真正的关键所在。这个方法的目标很简单,就是创建一个定期地检查 ByteArrayOutputStream缓冲区的线程。缓冲区中找到的所有数据都被提取到一个byte数组,然后写入到 PipedOutputStream。由于PipedOutputStream对应的PipedInputStream由getInputStream ()返回,从该输入流读取数据的线程都将读取到原先发送给ByteArrayOutputStream的数据。前面提到,LoopedStreams类解决了管道流存在的前二个问题,我们来看看这是如何实现的。
ByteArrayOutputStream具有根据需要扩展其内部缓冲区的能力。由于存在“完全缓冲”,线程向getOutputStream ()返回的流写入数据时不会被阻塞。因而,第一个问题不会再给我们带来麻烦。另外还要顺便说一句,ByteArrayOutputStream的缓冲区永远不会缩减。例如,假设在能够提取数据之前,有一块500 K的数据被写入到流,缓冲区将永远保持至少500 K的容量。如果这个类有一个方法能够在数据被提取之后修正缓冲区的大小,它就会更完善。
第二个问题得以解决的原因在于,实际上任何时候只有一个线程向PipedOutputStream写入数据,这个线程就是由 startByteArrayReaderThread()创建的线程。由于这个线程完全由LoopedStreams类控制,我们不必担心它会产生 IOException异常。
LoopedStreams类还有一些细节值得提及。首先,我们可以看到byteArrayOS和pipedIS实际上分别是 ByteArrayOutputStream和PipedInputStream的派生类的实例,也即在它们的close()方法中加入了特殊的行为。如果一个LoopedStreams对象的用户关闭了输入或输出流,在startByteArrayReaderThread()中创建的线程必须关闭。覆盖后的close()方法把keepRunning标记设置成false以关闭线程。另外,请注意startByteArrayReaderThread ()中的同步块。要确保在toByteArray()调用和reset()调用之间ByteArrayOutputStream缓冲区不被写入流的线程修改,这是必不可少的。由于ByteArrayOutputStream的write()方法的所有版本都在该流上同步,我们保证了 ByteArrayOutputStream的内部缓冲区不被意外地修改。
注意LoopedStreams类并不涉及管道流的第三个问题。该类的getInputStream()方法返回 PipedInputStream。如果一个线程从该流读取,一段时间后终止,下次数据从ByteArrayOutputStream缓冲区传输到 PipedOutputStream时就会出现IOException异常。
二、捕获Java控制台输出
Listing 5的ConsoleTextArea类扩展Swing JTextArea捕获控制台输出。不要对这个类有这么多代码感到惊讶,必须指出的是,ConsoleTextArea类有超过50%的代码用来进行测试。
【Listing 5:截获Java控制台输出】
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.text.*;
public class ConsoleTextArea extends JTextArea {
public ConsoleTextArea(InputStream[] inStreams) {
for(int i = 0; i < inStreams.length; ++i)
startConsoleReaderThread(inStreams[i]);
} // ConsoleTextArea()
public ConsoleTextArea() throws IOException {
final LoopedStreams ls = new LoopedStreams();
// 重定向System.out和System.err
PrintStream ps = new PrintStream(ls.getOutputStream());
System.setOut(ps);
System.setErr(ps);
startConsoleReaderThread(ls.getInputStream());
} // ConsoleTextArea()
private void startConsoleReaderThread(
InputStream inStream) {
final BufferedReader br =
new BufferedReader(new InputStreamReader(inStream));
new Thread(new Runnable() {
public void run() {
StringBuffer sb = new StringBuffer();
try {
String s;
Document doc = getDocument();
while((s = br.readLine()) != null) {
boolean caretAtEnd = false;
caretAtEnd = getCaretPosition() == doc.getLength() ?
true : false;
sb.setLength(0);
append(sb.append(s).append(´\n´).toString());
if(caretAtEnd)
setCaretPosition(doc.getLength());
}
}
catch(IOException e) {
JOptionPane.showMessageDialog(null,
"从BufferedReader读取错误:" + e);
System.exit(1);
}
}
}).start();
} // startConsoleReaderThread()
// 该类剩余部分的功能是进行测试
public static void main(String[] args) {
JFrame f = new JFrame("ConsoleTextArea测试");
ConsoleTextArea consoleTextArea = null;
try {
consoleTextArea = new ConsoleTextArea();
}
catch(IOException e) {
System.err.println(
"不能创建LoopedStreams:" + e);
System.exit(1);
}
consoleTextArea.setFont(java.awt.Font.decode("monospaced"));
f.getContentPane().add(new JScrollPane(consoleTextArea),
java.awt.BorderLayout.CENTER);
f.setBounds(50, 50, 300, 300);
f.setVisible(true);
f.addWindowListener(new java.awt.event.WindowAdapter() {
public void windowClosing(
java.awt.event.WindowEvent evt) {
System.exit(0);
}
});
// 启动几个写操作线程向
// System.out和System.err输出
startWriterTestThread(
"写操作线程 #1", System.err, 920, 50);
startWriterTestThread(
"写操作线程 #2", System.out, 500, 50);
startWriterTestThread(
"写操作线程 #3", System.out, 200, 50);
startWriterTestThread(
"写操作线程 #4", System.out, 1000, 50);
startWriterTestThread(
"写操作线程 #5", System.err, 850, 50);
} // main()
private static void startWriterTestThread(
final String name, final PrintStream ps,
final int delay, final int count) {
new Thread(new Runnable() {
public void run() {
for(int i = 1; i <= count; ++i) {
ps.println("***" + name + ", hello !, i=" + i);
try {
Thread.sleep(delay);
}
catch(InterruptedException e) {}
}
}
}).start();
} // startWriterTestThread()
} // ConsoleTextArea
main()方法创建了一个JFrame,JFrame包含一个ConsoleTextArea的实例。这些代码并没有什么特别之处。Frame 显示出来之后,main()方法启动一系列的写操作线程,写操作线程向控制台流输出大量信息。ConsoleTextArea捕获并显示这些信息,如图一所示。
ConsoleTextArea提供了两个构造函数。没有参数的构造函数用来捕获和显示所有写入到控制台流的数据,有一个InputStream []参数的构造函数转发所有从各个数组元素读取的数据到JTextArea。稍后将有一个例子显示这个构造函数的用处。首先我们来看看没有参数的 ConsoleTextArea构造函数。这个函数首先创建一个LoopedStreams对象;然后请求Java运行时环境把控制台输出转发到 LoopedStreams提供的OutputStream;最后,构造函数调用startConsoleReaderThread(),创建一个不断地把文本行追加到JTextArea的线程。注意,把文本追加到JTextArea之后,程序小心地保证了插入点的正确位置。
一般来说,Swing部件的更新不应该在AWT事件分派线程(AWT Event Dispatch Thread,AEDT)之外进行。对于本例来说,这意味着所有把文本追加到JTextArea的操作应该在AEDT中进行,而不是在 startConsoleReaderThread()方法创建的线程中进行。然而,事实上在Swing中向JTextArea追加文本是一个线程安全的操作。读取一行文本之后,我们只需调用JText.append()就可以把文本追加到JTextArea的末尾。
三、捕获其他程序的控制台输出
在JTextArea中捕获Java程序自己的控制台输出是一回事,去捕获其他程序(甚至包括一些非Java程序)的控制台数据又是另一回事。 ConsoleTextArea提供了捕获其他应用的输出时需要的基础功能,Listing 6的AppOutputCapture利用ConsoleTextArea,截取其他应用的输出信息然后显示在ConsoleTextArea中。
【Listing 6:截获其他程序的控制台输出】
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;
public class AppOutputCapture {
private static Process process;
public static void main(String[] args) {
if(args.length == 0) {
System.err.println("用法:java AppOutputCapture " +
"<程序名字> {参数1 参数2 ...}");
System.exit(0);
}
try {
// 启动命令行指定程序的新进程
process = Runtime.getRuntime().exec(args);
}
catch(IOException e) {
System.err.println("创建进程时出错...\n" + e);
System.exit(1);
}
// 获得新进程所写入的流
InputStream[] inStreams =
new InputStream[] {
process.getInputStream(),process.getErrorStream()};
ConsoleTextArea cta = new
ConsoleTextArea(inStreams);
cta.setFont(java.awt.Font.decode("monospaced"));
JFrame frame = new JFrame(args[0] +
"控制台输出");
frame.getContentPane().add(new JScrollPane(cta),
BorderLayout.CENTER);
frame.setBounds(50, 50, 400, 400);
frame.setVisible(true);
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent evt) {
process.destroy();
try {
process.waitFor(); // 在Win98下可能被挂起
}
catch(InterruptedException e) {}
System.exit(0);
}
});
} // main()
} // AppOutputCapture
AppOutputCapture的工作过程如下:首先利用Runtime.exec()方法启动指定程序的一个新进程。启动新进程之后,从结果 Process对象得到它的控制台流。之后,把这些控制台流传入ConsoleTextArea(InputStream[])构造函数(这就是带参数 ConsoleTextArea构造函数的用处)。使用AppOutputCapture时,在命令行上指定待截取其输出的程序名字。例如,如果在 Windows 2000下执行javaw.exe AppOutputCapture ping.exe www.yahoo.com,则结果如图四所示。
图四:截取其他程序的控制台输出
使用AppOutputCapture时应该注意,被截取输出的应用程序最初输出的一些文本可能无法截取。因为在调用Runtime.exec ()和ConsoleTextArea初始化完成之间存在一小段时间差。在这个时间差内,应用程序输出的文本会丢失。当AppOutputCapture 窗口被关闭,process.destory()调用试图关闭Java程序开始时创建的进程。测试结果显示出,destroy()方法不一定总是有效(至少在Windows 98上是这样的)。似乎当待关闭的进程启动了额外的进程时,则那些进程不会被关闭。此外,在这种情况下AppOutputCapture程序看起来未能正常结束。但在Windows NT下,一切正常。如果用JDK v1.1.x运行AppOutputCapture,关闭窗口时会出现一个NullPointerException。这是一个JDK的Bug,JDK 1.2.x和JDK 1.3.x下就不会出现问题。
请从这里下载本文完整代码:JavaConsoleOutput_code.zip
参考:
Java 技巧 14:在 Java 中对标准流进行重定向
Java 技巧 33:再谈对流进行重定向
编写多线程的Java 应用程序 如何避免当前编程中最常见的问题
Java 程序中的多线程
关于作者
俞良松,软件工程师,独立顾问和自由撰稿人。最初从事PB和Oracle开发,现主要兴趣在于Internet开发。您可以通过 javaman@163.net 和我联系。