在java程序中截获控制台输出
俞良松 (javaman@163.net)
软件工程师,独立顾问和自由撰稿人
2001 年 10 月
在java开发中,控制台输出仍是一个重要的工具,但默认的控制台输出有着各种各样的局限。本文介绍如何用java管道流截取控制台输出,分析管道流应用中应该注意的问题,提供了截取java程序和非java程序控制台输出的实例。
即使在图形用户界面占统治地位的今天,控制台输出仍旧在java程序中占有重要地位。控制台不仅是java程序默认的堆栈跟踪和错误信息输出窗口,而且还是一种实用的调试工具(特别是对习惯于使用println()的人来说)。然而,控制台窗口有着许多局限。例如在windows 9x平台上,dos控制台只能容纳50行输出。如果java程序一次性向控制台输出大量内容,要查看这些内容就很困难了。
对于使用javaw这个启动程序的开发者来说,控制台窗口尤其宝贵。因为用javaw启动java程序时,根本不会有控制台窗口出现。如果程序遇到了问题并抛出异常,根本无法查看java运行时环境写入到system.out或system.err的调用堆栈跟踪信息。为了捕获堆栈信息,一些人采取了用try/catch()块封装main()的方式,但这种方式不一定总是有效,在java运行时的某些时刻,一些描述性错误信息会在抛出异常之前被写入system.out和system.err;除非能够监测这两个控制台流,否则这些信息就无法看到。
因此,有些时候检查java运行时环境(或第三方程序)写入到控制台流的数据并采取合适的操作是十分必要的。本文讨论的主题之一就是创建这样一个输入流,从这个输入流中可以读入以前写入java控制台流(或任何其他程序的输出流)的数据。我们可以想象写入到输出流的数据立即以输入的形式“回流”到了java程序。
本文的目标是设计一个基于swing的文本窗口显示控制台输出。在此期间,我们还将讨论一些和java管道流(pipedinputstream和pipedoutputstream)有关的重要注意事项。图一显示了用来截取和显示控制台文本输出的java程序,用户界面的核心是一个jtextarea。最后,我们还要创建一个能够捕获和显示其他程序(可以是非java的程序)控制台输出的简单程序。
图一:多线程的控制台输出截取程序
一、java管道流
要在文本框中显示控制台输出,我们必须用某种方法“截取”控制台流。换句话说,我们要有一种高效地读取写入到system.out和system.err所有内容的方法。如果你熟悉java的管道流pipedinputstream和pipedoutputstream,就会相信我们已经拥有最有效的工具。
写入到pipedoutputstream输出流的数据可以从对应的pipedinputstream输入流读取。java的管道流极大地方便了我们截取控制台输出。listing 1显示了一种非常简单的截取控制台输出方案。
【listing 1:用管道流截取控制台输出】
pipedinputstream pipedis = new pipedinputstream();
pipedoutputstream pipedos = new pipedoutputstream();
try {
pipedos.connect(pipedis);
}
catch(ioexception e) {
system.err.println("连接失败");
system.exit(1);
}
printstream ps = new printstream(pipedos);
system.setout(ps);
system.seterr(ps);
可以看到,这里的代码极其简单。我们只是建立了一个pipedinputstream,把它设置为所有写入控制台流的数据的最终目的地。所有写入到控制台流的数据都被转到pipedoutputstream,这样,从相应的pipedinputstream读取就可以迅速地截获所有写入控制台流的数据。接下来的事情似乎只剩下在swing jtextarea中显示从pipedis流读取的数据,得到一个能够在文本框中显示控制台输出的程序。遗憾的是,在使用java管道流时有一些重要的注意事项。只有认真对待所有这些注意事项才能保证listing 1的代码稳定地运行。下面我们来看第一个注意事项。
1.1 注意事项一
pipedinputstream运用的是一个1024字节固定大小的循环缓冲区。写入pipedoutputstream的数据实际上保存到对应的pipedinputstream的内部缓冲区。从pipedinputstream执行读操作时,读取的数据实际上来自这个内部缓冲区。如果对应的pipedinputstream输入缓冲区已满,任何企图写入pipedoutputstream的线程都将被阻塞。而且这个写操作线程将一直阻塞,直至出现读取pipedinputstream的操作从缓冲区删除数据。
这意味着,向pipedoutputstream写数据的线程不应该是负责从对应pipedinputstream读取数据的唯一线程。从图二可以清楚地看出这里的问题所在:假设线程t是负责从pipedinputstream读取数据的唯一线程;另外,假定t企图在一次对pipedoutputstream的write()方法的调用中向对应的pipedoutputstream写入2000字节的数据。在t线程阻塞之前,它最多能够写入1024字节的数据(pipedinputstream内部缓冲区的大小)。然而,一旦t被阻塞,读取pipedinputstream的操作就再也不会出现,因为t是唯一读取pipedinputstream的线程。这样,t线程已经完全被阻塞,同时,所有其他试图向pipedoutputstream写入数据的线程也将遇到同样的情形。
图二:管道流工作过程
这并不意味着在一次write()调用中不能写入多于1024字节的数据。但应当保证,在写入数据的同时,有另一个线程从pipedinputstream读取数据。
listing 2示范了这个问题。这个程序用一个线程交替地读取pipedinputstream和写入pipedoutputstream。每次调用write()向pipedinputstream的缓冲区写入20字节,每次调用read()只从缓冲区读取并删除10个字节。内部缓冲区最终会被写满,导致写操作阻塞。由于我们用同一个线程执行读、写操作,一旦写操作被阻塞,就不能再从pipedinputstream读取数据。
【listing 2:用同一个线程执行读/写操作导致线程阻塞】
import java.io.*;
public class listing2 {
static pipedinputstream pipedis = new pipedinputstream();
static pipedoutputstream pipedos =
new pipedoutputstream();
public static void main(string[] a){
try {
pipedis.connect(pipedos);
}
catch(ioexception e) {
system.err.println("连接失败");
system.exit(1);
}
byte[] inarray = new byte[10];
byte[] outarray = new byte[20];
int bytesread = 0;
try {
// 向pipedos发送20字节数据
pipedos.write(outarray, 0, 20);
system.out.println(" 已发送20字节...");
// 在每一次循环迭代中,读入10字节
// 发送20字节
bytesread = pipedis.read(inarray, 0, 10);
int i=0;
while(bytesread != -1) {
pipedos.write(outarray, 0, 20);
system.out.println(" 已发送20字节..."+i);
i++;
bytesread = pipedis.read(inarray, 0, 10);
}
}
catch(ioexception e) {
system.err.println("读取pipedis时出现错误: " + e);
system.exit(1);
}
} // main()
}
只要把读/写操作分开到不同的线程,listing 2的问题就可以轻松地解决。listing 3是listing 2经过修改后的版本,它在一个单独的线程中执行写入pipedoutputstream的操作(和读取线程不同的线程)。为证明一次写入的数据可以超过1024字节,我们让写操作线程每次调用pipedoutputstream的write()方法时写入2000字节。那么,在startwriterthread()方法中创建的线程是否会阻塞呢?按照java运行时线程调度机制,它当然会阻塞。写操作在阻塞之前实际上最多只能写入1024字节的有效载荷(即pipedinputstream缓冲区的大小)。但这并不会成为问题,因为主线程(main)很快就会从pipedinputstream的循环缓冲区读取数据,空出缓冲区空间。最终,写操作线程会从上一次中止的地方重新开始,写入2000字节有效载荷中的剩余部分。
【listing 3:把读/写操作分开到不同的线程】
import java.io.*;
public class listing3 {
static pipedinputstream pipedis =
new pipedinputstream();
static pipedoutputstream pipedos =
new pipedoutputstream();
public static void main(string[] args) {
try {
pipedis.connect(pipedos);
}
catch(ioexception e) {
system.err.println("连接失败");
system.exit(1);
}
byte[] inarray = new byte[10];
int bytesread = 0;
// 启动写操作线程
startwriterthread();
try {
bytesread = pipedi
闽公网安备 35060202000074号