即使在图形用户界面占统治地位的今天控制台输出仍旧在Java程序中占有重要地位控制台不仅是Java程序默认的堆栈跟蹤和错误信息输出窗口而且还是一种实用的调试工具(特别是对习惯于使用println()的人来说)然而控制台窗口有着许多局限例如在Windows x平台上DOS控制台只能容纳行输出如果Java程序一次性向控制台输出大量内容要查看这些内容就很困难了
对于使用javaw这个启动程序的开发者来说控制台窗口尤其宝贵因为用javaw启动java程序时根本不会有控制台窗口出现如果程序遇到了问题并抛出异常根本无法查看Java运行时环境写入到Systemout或Systemerr的调用堆栈跟蹤信息为了捕获堆栈信息一些人采取了用 try/catch()块封装main()的方式但这种方式不一定总是有效在Java运行时的某些时刻一些描述性错误信息会在抛出异常之前被写入 Systemout和Systemerr除非能够监测这两个控制台流否则这些信息就无法看到
因此有些时候检查Java运行时环境(或第三方程序)写入到控制台流的数据并采取合适的操作是十分必要的本文讨论的主题之一就是创建这样一个输入流从这个输入流中可以读入以前写入Java控制台流(或任何其他程序的输出流)的数据我们可以想象写入到输出流的数据立即以输入的形式回流到了 Java程序
本文的目标是设计一个基于Swing的文本窗口显示控制台输出在此期间我们还将讨论一些和Java管道流(PipedInputStream和 PipedOutputStream)有关的重要注意事项图一显示了用来截取和显示控制台文本输出的Java程序用户界面的核心是一个 JTextArea最后我们还要创建一个能够捕获和显示其他程序(可以是非Java的程序)控制台输出的简单程序
图一多线程的控制台输出截取程序
一Java管道流
要在文本框中显示控制台输出我们必须用某种方法截取控制台流换句话说我们要有一种高效地读取写入到Systemout和Systemerr 所有内容的方法如果你熟悉Java的管道流PipedInputStream和PipedOutputStream就会相信我们已经拥有最有效的工具
写入到PipedOutputStream输出流的数据可以从对应的PipedInputStream输入流读取Java的管道流极大地方便了我们截取控制台输出Listing 显示了一种非常简单的截取控制台输出方案
【Listing 用管道流截取控制台输出】
PipedInputStream pipedIS = new PipedInputStream();
PipedOutputStream pipedOS = new PipedOutputStream();
try {
nnect(pipedIS);
}
catch(IOException e) {
Systemerrprintln(连接失败);
Systemexit();
}
PrintStream ps = new PrintStream(pipedOS);
SystemsetOut(ps);
SystemsetErr(ps);
可以看到这里的代码极其简单我们只是建立了一个PipedInputStream把它设置为所有写入控制台流的数据的最终目的地所有写入到控制台流的数据都被转到PipedOutputStream这样从相应的PipedInputStream读取就可以迅速地截获所有写入控制台流的数据接下来的事情似乎只剩下在Swing JTextArea中显示从pipedIS流读取的数据得到一个能够在文本框中显示控制台输出的程序遗憾的是在使用Java管道流时有一些重要的注意事项只有认真对待所有这些注意事项才能保证Listing 的代码稳定地运行下面我们来看第一个注意事项
注意事项一
PipedInputStream运用的是一个字节固定大小的循环缓沖区写入PipedOutputStream的数据实际上保存到对应的 PipedInputStream的内部缓沖区从PipedInputStream执行读操作时读取的数据实际上来自这个内部缓沖区如果对应的 PipedInputStream输入缓沖区已满任何企图写入PipedOutputStream的线程都将被阻塞而且这个写操作线程将一直阻塞直至出现读取PipedInputStream的操作从缓沖区删除数据
这意味着向PipedOutputStream写数据的线程不应该是负责从对应PipedInputStream读取数据的唯一线程从图二可以清楚地看出这里的问题所在假设线程t是负责从PipedInputStream读取数据的唯一线程另外假定t企图在一次对 PipedOutputStream的write()方法的调用中向对应的PipedOutputStream写入字节的数据在t线程阻塞之前它最多能够写入字节的数据(PipedInputStream内部缓沖区的大小)然而一旦t被阻塞读取 PipedInputStream的操作就再也不会出现因为t是唯一读取PipedInputStream的线程这样t线程已经完全被阻塞同时所有其他试图向PipedOutputStream写入数据的线程也将遇到同样的情形
图二管道流工作过程
这并不意味着在一次write()调用中不能写入多于字节的数据但应当保证在写入数据的同时有另一个线程从PipedInputStream读取数据
Listing 示范了这个问题这个程序用一个线程交替地读取PipedInputStream和写入PipedOutputStream每次调用write()向 PipedInputStream的缓沖区写入字节每次调用read()只从缓沖区读取并删除个字节内部缓沖区最终会被写满导致写操作阻塞由于我们用同一个线程执行读写操作一旦写操作被阻塞就不能再从PipedInputStream读取数据
【Listing 用同一个线程执行读/写操作导致线程阻塞】
import javaio*;
public class Listing {
static PipedInputStream pipedIS = new PipedInputStream();
static PipedOutputStream pipedOS =
new PipedOutputStream();
public static void main(String[] a){
try {
nnect(pipedOS);
}
catch(IOException e) {
Systemerrprintln(连接失败);
Systemexit();
}
byte[] inArray = new byte[];
byte[] outArray = new byte[];
int bytesRead = ;
try {
// 向pipedOS发送字节数据
pipedOSwrite(outArray );
Systemoutprintln( 已发送字节);
// 在每一次循环迭代中读入字节
// 发送字节
bytesRead = pipedISread(inArray );
int i=;
while(bytesRead != ) {
pipedOSwrite(outArray );
Systemoutprintln( 已发送字节+i);
i++;
bytesRead = pipedISread(inArray );
}
}
catch(IOException e) {
Systemerrprintln(读取pipedIS时出现错误: + e);
Systemexit();
}
} // main()
}
只要把读/写操作分开到不同的线程Listing 的问题就可以轻松地解决Listing 是Listing 经过修改后的版本它在一个单独的线程中执行写入PipedOutputStream的操作(和读取线程不同的线程)为证明一次写入的数据可以超过 字节我们让写操作线程每次调用PipedOutputStream的write()方法时写入字节那么在 startWriterThread()方法中创建的线程是否会阻塞呢?按照Java运行时线程调度机制它当然会阻塞写操作在阻塞之前实际上最多只能写入字节的有效载荷(即PipedInputStream缓沖区的大小)但这并不会成为问题因为主线程(main)很快就会从 PipedInputStream的循环缓沖区读取数据空出缓沖区空间最终写操作线程会从上一次中止的地方重新开始写入字节有效载荷中的剩余部分
【Listing 把读/写操作分开到不同的线程】
import javaio*;
public class Listing {
static PipedInputStream pipedIS =
new PipedInputStream();
static PipedOutputStream pipedOS =
new PipedOutputStream();
public static void main(String[] args) {
try {
nnect(pipedOS);
}
catch(IOException e) {
Systemerrprintln(连接失败);
Systemexit();
}
byte[] inArray = new byte[];
int bytesRead = ;
// 启动写操作线程
startWriterThread();
try {
bytesRead = pipedISread(inArray );
while(bytesRead != ) {
Systemoutprintln(已经读取 +
bytesRead + 字节);
bytesRead = pipedISread(inArray );
}
}
catch(IOException e) {
Systemerrprintln(读取输入错误);
Systemexit();
}
} // main()
// 创建一个独立的线程
// 执行写入PipedOutputStream的操作
private static void startWriterThread() {
new Thread(new Runnable() {
public void run() {
byte[] outArray = new byte[];
while(true) { // 无终止条件的循环
try {
// 在该线程阻塞之前有最多字节的数据被写入
pipedOSwrite(outArray );
}
catch(IOException e) {
Systemerrprintln(写操作错误);
Systemexit();
}
Systemoutprintln( 已经发送字节);
}
}
})start();
} // startWriterThread()
} // Listing
也许我们不能说这个问题是Java管道流设计上的缺陷但在应用管道流时它是一个必须密切注意的问题下面我们来看看第二个更重要(更危险的)问题
注意事项二
从PipedInputStream读取数据时如果符合下面三个条件就会出现IOException异常
试图从PipedInputStream读取数据
PipedInputStream的缓沖区为空(即不存在可读取的数据)
最后一个向PipedOutputStream写数据的线程不再活动(通过ThreadisAlive()检测)
这是一个很微妙的时刻同时也是一个极其重要的时刻假定有一个线程w向PipedOutputStream写入数据另一个线程r从对应的 PipedInputStream读取数据下面一系列的事件将导致r线程在试图读取PipedInputStream时遇到IOException异常
w向PipedOutputStream写入数据
w结束(wisAlive()返回false)
r从PipedInputStream读取w写入的数据清空PipedInputStream的缓沖区
r试图再次从PipedInputStream读取数据这时PipedInputStream的缓沖区已经为空而且w已经结束从而导致在读操作执行时出现IOException异常
构造一个程序示范这个问题并不困难只需从Listing 的startWriterThread()方法中删除while(true)条件这个改动阻止了执行写操作的方法循环执行使得执行写操作的方法在一次写入操作之后就结束运行如前所述此时主线程试图读取PipedInputStraem时就会遇到一个IOException异常
这是一种比较少见的情况而且不存在直接修正它的方法请不要通过从管道流派生子类的方法修正该问题――在这里使用继承是完全不合适的而且如果Sun以后改变了管道流的实现方法现在所作的修改将不再有效
最后一个问题和第二个问题很相似不同之处在于它在读线程(而不是写线程)结束时产生IOException异常
注意事项三
如果一个写操作在PipedOutputStream上执行同时最近从对应PipedInputStream读取的线程已经不再活动(通过 ThreadisAlive()检测)则写操作将抛出一个IOException异常假定有两个线程w和rw向 PipedOutputStream写入数据而r则从对应的PipedInputStream读取下面一系列的事件将导致w线程在试图写入 PipedOutputStream时遇到IOException异常
写操作线程w已经创建但r线程还不存在
w向PipedOutputStream写入数据
读线程r被创建并从PipedInputStream读取数据
r线程结束
w企图向PipedOutputStream写入数据发现r已经结束抛出IOException异常
实际上这个问题不象第二个问题那样棘手和多个读线程/单个写线程的情况相比也许在应用中有一个读线程(作为响应请求的服务器)和多个写线程(发出请求)的情况更为常见
解决问题
要防止管道流前两个局限所带来的问题方法之一是用一个ByteArrayOutputStream作为代理或替代PipedOutputStream Listing 显示了一个LoopedStreams类它用一个ByteArrayOutputStream提供和Java管道流类似的功能但不会出现死锁和 IOException异常这个类的内部仍旧使用管道流但隔离了本文介绍的前两个问题我们先来看看这个类的公用方法(参见图)构造函数很简单它连接管道流然后调用startByteArrayReaderThread()方法(稍后再讨论该方法)getOutputStream()方法返回一个OutputStream(具体地说是一个ByteArrayOutputStream)用以替代PipedOutputStream写入该 OutputStream的数据最终将在getInputStream()方法返回的流中作为输入出现和使用PipedOutputStream的情形不同向ByteArrayOutputStream写入数据的线程的激活写数据结束不会带来负面效果
图三ByteArrayOutputStream原理
【Listing 防止管道流应用中出现的常见问题】
import javaio*;
public class LoopedStreams {
private PipedOutputStream pipedOS =
new PipedOutputStream();
private boolean keepRunning = true;
private ByteArrayOutputStream byteArrayOS =
new ByteArrayOutputStream() {
public void close() {
keepRunning = false;
try {
superclose();
pipedOSclose();
}
catch(IOException e) {
// 记录错误或其他处理
// 为简单计此处我们直接结束
Systemexit();
}
}
};
private PipedInputStream pipedIS = new PipedInputStream() {
public void close() {
keepRunning = false;
try {
superclose();
}
catch(IOException e) {
// 记录错误或其他处理
// 为简单计此处我们直接结束
Systemexit();
}
}
};
public LoopedStreams() throws IOException {
nnect(pipedIS);
startByteArrayReaderThread();
} // LoopedStreams()
public InputStream getInputStream() {
return pipedIS;
} // getInputStream()
public OutputStream getOutputStream() {
return byteArrayOS;
} // getOutputStream()
private void startByteArrayReaderThread() {
new Thread(new Runnable() {
public void run() {
while(keepRunning) {
// 检查流里面的字节数
if(byteArrayOSsize() > ) {
byte[] buffer = null;
synchronized(byteArrayOS) {
buffer = byteArrayOStoByteArray();
byteArrayOSreset(); // 清除缓沖区
}
try {
// 把提取到的数据发送给PipedOutputStream
pipedOSwrite(buffer bufferlength);
}
catch(IOException e) {
// 记录错误或其他处理
// 为简单计此处我们直接结束
Systemexit();
}
}
else // 没有数据可用线程进入睡眠状态
try {
// 每隔秒查看ByteArrayOutputStream检查新数据
Threadsleep();
}
catch(InterruptedException e) {}
}
}
})start();
} // startByteArrayReaderThread()
} // LoopedStreams
startByteArrayReaderThread()方法是整个类真正的关键所在这个方法的目标很简单就是创建一个定期地检查 ByteArrayOutputStream缓沖区的线程缓沖区中找到的所有数据都被提取到一个byte数组然后写入到 PipedOutputStream由于PipedOutputStream对应的PipedInputStream由getInputStream ()返回从该输入流读取数据的线程都将读取到原先发送给ByteArrayOutputStream的数据前面提到LoopedStreams类解决了管道流存在的前二个问题我们来看看这是如何实现的
ByteArrayOutputStream具有根据需要扩展其内部缓沖区的能力由于存在完全缓沖线程向getOutputStream()返回的流写入数据时不会被阻塞因而第一个问题不会再给我们带来麻烦另外还要顺便说一句ByteArrayOutputStream的缓沖区永远不会缩减例如假设在能够提取数据之前有一块 K的数据被写入到流缓沖区将永远保持至少 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 的ConsoleTextArea类扩展Swing JTextArea捕获控制台输出不要对这个类有这么多代码感到惊讶必须指出的是ConsoleTextArea类有超过%的代码用来进行测试
【Listing 截获Java控制台输出】
import javaio*;
import javautil*;
import javaxswing*;
import javaxswingtext*;
public class ConsoleTextArea extends JTextArea {
public ConsoleTextArea(InputStream[] inStreams) {
for(int i = ; i < inStreamslength; ++i)
startConsoleReaderThread(inStreams[i]);
} // ConsoleTextArea()
public ConsoleTextArea() throws IOException {
final LoopedStreams ls = new LoopedStreams();
// 重定向Systemout和Systemerr
PrintStream ps = new PrintStream(lsgetOutputStream());
SystemsetOut(ps);
SystemsetErr(ps);
startConsoleReaderThread(lsgetInputStream());
} // 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 = brreadLine()) != null) {
boolean caretAtEnd = false;
caretAtEnd = getCaretPosition() == docgetLength() ?
true : false;
sbsetLength();
append(sbappend(s)append(\n)toString());
if(caretAtEnd)
setCaretPosition(docgetLength());
}
}
catch(IOException e) {
JOptionPaneshowMessageDialog(null
从BufferedReader读取错误 + e);
Systemexit();
}
}
})start();
} // startConsoleReaderThread()
// 该类剩余部分的功能是进行测试
public static void main(String[] args) {
JFrame f = new JFrame(ConsoleTextArea测试);
ConsoleTextArea consoleTextArea = null;
try {
consoleTextArea = new ConsoleTextArea();
}
catch(IOException e) {
Systemerrprintln(
不能创建LoopedStreams + e);
Systemexit();
}
consoleTextAreasetFont(javaawtFontdecode(monospaced));
fgetContentPane()add(new JScrollPane(consoleTextArea)
javaawtBorderLayoutCENTER);
fsetBounds( );
fsetVisible(true);
faddWindowListener(new javaawteventWindowAdapter() {
public void windowClosing(
javaawteventWindowEvent evt) {
Systemexit();
}
});
// 启动几个写操作线程向
// Systemout和Systemerr输出
startWriterTestThread(
写操作线程 # Systemerr );
startWriterTestThread(
写操作线程 # Systemout );
startWriterTestThread(
写操作线程 # Systemout );
startWriterTestThread(
写操作线程 # Systemout );
startWriterTestThread(
写操作线程 # Systemerr );
} // 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 = ; i <= count; ++i) {
psprintln(*** + name + hello ! i= + i);
try {
Threadsleep(delay);
}
catch(InterruptedException e) {}
}
}
})start();
} // startWriterTestThread()
} // ConsoleTextArea
main()方法创建了一个JFrameJFrame包含一个ConsoleTextArea的实例这些代码并没有什么特别之处Frame显示出来之后main()方法启动一系列的写操作线程写操作线程向控制台流输出大量信息ConsoleTextArea捕获并显示这些信息如图一所示
ConsoleTextArea提供了两个构造函数没有参数的构造函数用来捕获和显示所有写入到控制台流的数据有一个InputStream[]参数的构造函数转发所有从各个数组元素读取的数据到JTextArea稍后将有一个例子显示这个构造函数的用处首先我们来看看没有参数的 ConsoleTextArea构造函数这个函数首先创建一个LoopedStreams对象然后请求Java运行时环境把控制台输出转发到 LoopedStreams提供的OutputStream最后构造函数调用startConsoleReaderThread()创建一个不断地把文本行追加到JTextArea的线程注意把文本追加到JTextArea之后程序小心地保证了插入点的正确位置
一般来说Swing部件的更新不应该在AWT事件分派线程(AWT Event Dispatch ThreadAEDT)之外进行对于本例来说这意味着所有把文本追加到JTextArea的操作应该在AEDT中进行而不是在 startConsoleReaderThread()方法创建的线程中进行然而事实上在Swing中向JTextArea追加文本是一个线程安全的操作读取一行文本之后我们只需调用JTextappend()就可以把文本追加到JTextArea的末尾
三捕获其他程序的控制台输出
在JTextArea中捕获Java程序自己的控制台输出是一回事去捕获其他程序(甚至包括一些非Java程序)的控制台数据又是另一回事ConsoleTextArea提供了捕获其他应用的输出时需要的基础功能Listing 的AppOutputCapture利用ConsoleTextArea截取其他应用的输出信息然后显示在ConsoleTextArea中
【Listing 截获其他程序的控制台输出】
import javaawt*;
import javaawtevent*;
import javaio*;
import javaxswing*;
public class AppOutputCapture {
private static Process process;
public static void main(String[] args) {
if(argslength == ) {
Systemerrprintln(用法java AppOutputCapture +
<程序名字> {参数 参数 });
Systemexit();
}
try {
// 启动命令行指定程序的新进程
process = RuntimegetRuntime()exec(args);
}
catch(IOException e) {
Systemerrprintln(创建进程时出错\n + e);
Systemexit();
}
// 获得新进程所写入的流
InputStream[] inStreams =
new InputStream[] {
processgetInputStream()processgetErrorStream()};
ConsoleTextArea cta = new
ConsoleTextArea(inStreams);
ctasetFont(javaawtFontdecode(monospaced));
JFrame frame = new JFrame(args[] +
控制台输出);
framegetContentPane()add(new JScrollPane(cta)
BorderLayoutCENTER);
framesetBounds( );
framesetVisible(true);
frameaddWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent evt) {
processdestroy();
try {
processwaitFor(); // 在Win下可能被挂起
}
catch(InterruptedException e) {}
Systemexit();
}
});
} // main()
} // AppOutputCapture
AppOutputCapture的工作过程如下首先利用Runtimeexec()方法启动指定程序的一个新进程启动新进程之后从结果 Process对象得到它的控制台流之后把这些控制台流传入ConsoleTextArea(InputStream[])构造函数(这就是带参数 ConsoleTextArea构造函数的用处)使用AppOutputCapture时在命令行上指定待截取其输出的程序名字例如如果在 Windows 下执行javawexe AppOutputCapture pingexe 则结果如图四所示
图四截取其他程序的控制台输出
使用AppOutputCapture时应该注意被截取输出的应用程序最初输出的一些文本可能无法截取因为在调用Runtimeexec()和 ConsoleTextArea初始化完成之间存在一小段时间差在这个时间差内应用程序输出的文本会丢失当AppOutputCapture窗口被关闭processdestory()调用试图关闭Java程序开始时创建的进程测试结果显示出destroy()方法不一定总是有效(至少在 Windows 上是这样的)似乎当待关闭的进程启动了额外的进程时则那些进程不会被关闭此外在这种情况下AppOutputCapture程序看起来未能正常结束但在Windows NT下一切正常如果用JDK vx运行AppOutputCapture关闭窗口时会出现一个NullPointerException这是一个JDK的BugJDK x和JDK x下就不会出现问题