本文的目标是设计一个基于Swing的JTextArea显示控制台输出。此期间,我们还将讨论一些和Java管道流(PipedInputStream 和PipedOutputStream)有关的注意事项。最后还要创建一个能够捕获和显示其他程序(可以是非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读取数据的唯一线程。从图1 可以清楚地看出这里的问题所在:假设线程t是负责从PipedInputStream读取数据的唯一线程;另外,假定t企图在一次对 PipedOutputStream的write()方法的调用中向对应的PipedOutputStream写入2000字节的数据。在t线程阻塞之 前,它最多能够写入1024字节的数据(PipedInputStream内部缓冲区的大小)。然而,一旦t被阻塞,读取 PipedInputStream的操作就再也不会出现,因为t是唯一读取PipedInputStream的线程。这样,t线程已经完全被阻塞,同时, 所有其他试图向PipedOutputStream写入数据的线程也将遇到同样的情形。
图1:管道流工作过程
这并不意味着在一次write()调用中不能写入多于1024字节的数据。但应当保证,在写入数据的同时,有另一个线程从 PipedInputStream读取数据。
Listing 2示范了这个问题。这个程序用一个线程交替地读取PipedInputStream和写入PipedOutputStream。每次调用write()向 PipedInputStream的缓冲区写入20字节,每次调用read()只从缓冲区读取并删除10个字节。内部缓冲区最终会被写满,导致写操作阻 塞。由于我们用同一个线程执行读、写操作,一旦写操作被阻塞,就不能再从PipedInputStream读取数据。
【Listing 2 :用同一个线程执行读/写操作导致线程阻塞】
import java.io.*; //用同一个线程执行读/写操作导致线程阻塞】 public class PipedStreamWithOneThread { 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.write(outArray, 0, 20); // 向pipedOS发送20字节数据 System.out.println(" 已发送20字节..."); // 在每一次循环迭代中,读入10字节// 发送20字节 bytesRead = pipedIS.read(inArray, 0, 10); System.out.println(" 已读取10字节..."); System.out.println(" 10"); int i=0; while(bytesRead != -1) { pipedOS.write(outArray, 0, 20); System.out.println(" 已发送20字节..."+i); i++; bytesRead = pipedIS.read(inArray, 0, 10); System.out.println(" 已读取10字节..."); System.out.println(10+10*i); } }catch(IOException e) { System.err.println("读取pipedIS时出现错误: " + e); System.exit(1); } } }
1. import java.io.*;
2.
3. // 用同一个线程执行读/写操作导致线程阻塞】
4. public class PipedStreamWithOneThread {
5. static PipedInputStream pipedIS = new PipedInputStream();
6. static PipedOutputStream pipedOS = new PipedOutputStream();
7. public static void main(String[] a){
8. try {
9. pipedIS.connect(pipedOS);
10. }catch(IOException e) {
11. System.err.println(" 连接失败");
12. System.exit(1);
13. }
14. byte[] inArray = new byte[10];
15. byte[] outArray = new byte[20];
16. int bytesRead = 0;
17. try {
18. pipedOS.write(outArray, 0, 20); // 向pipedOS发送20字节数据
19. System.out.println(" 已发送20字节...");
20. // 在每一次循环迭代中,读入10字节// 发送20字节
21. bytesRead = pipedIS.read(inArray, 0, 10);
22. System.out.println(" 已读取10字节...");
23. System.out.println(" 10");
24. int i=0;
25. while(bytesRead != -1) {
26. pipedOS.write(outArray, 0, 20);
27. System.out.println(" 已发送20字节..."+i);
28. i++;
29. bytesRead = pipedIS.read(inArray, 0, 10);
30. System.out.println(" 已读取10字节...");
31. System.out.println(10+10*i);
32. }
33. }catch(IOException e) {
34. System.err.println("读取 pipedIS时出现错误: " + e);
35. System.exit(1);
36. }
37. }
38. }
只要把读/写操作分开到不同的线程,Listing 2的问题就可以轻松地解决。
Listing 3是Listing 2经过修改后的版本,它在一个单独的线程中执行写入PipedOutputStream的操作(和读取线程不同的线程)。为证明一次写入的数据可以超过 1024字节,我们让写操作线程每次调用PipedOutputStream的write()方法时写入2000字节。那么,在 startWriterThread()方法中创建的线程是否会阻塞呢?按照Java运行时线程调度机制,它当然会阻塞。写操作在阻塞之前实际上最多只能 写入1024字节的有效载荷(即PipedInputStream缓冲区的大小)。但这并不会成为问题,因为主线程(main)很快就会从 PipedInputStream的循环缓冲区读取数据,空出缓冲区空间。最终,写操作线程会从上一次中止的地方重新开始,写入2000字节有效载荷中的 剩余部分。
【Listing 3 :把读/写操作分开到不同的线程】
import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; //把读/写操作分开到不同的线程 public class PipedStreamWithTwoThread { 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]; int bytesRead = 0; startWriterThread(); //启动write线程 try { bytesRead = pipedIS.read(inArray, 0, 10); //每次读10字节 System.out.println("已读取"+bytesRead+"字节..."); int i=0; while(bytesRead != -1) { bytesRead = pipedIS.read(inArray, 0, 10); System.out.println("已读取"+bytesRead+"字节..."); //下面这段防止程序无限执行 /*i++; if (i==300) { System.exit(0); }*/ } }catch(IOException e) { System.err.println("读取pipedIS时出现错误: " + e); System.exit(1); } } //创建一个独立的线程 执行write操作 private static void startWriterThread(){ new Thread(new Runnable(){ public void run(){ byte outArray[]=new byte[2000]; int j=0; while (j==0) { //无限循环 try { pipedOS.write(outArray, 0, 2000); //一次最多写入1024字节 j++; } catch (IOException e) { System.err.println("写操作失败"); System.exit(1); } System.out.println("已发送2000字节..."); } } }).start(); } }
1. import java.io.IOException;
2. import java.io.PipedInputStream;
3. import java.io.PipedOutputStream;
4.
5. //把读/写操作分开到不同的线程
6. public class PipedStreamWithTwoThread {
7. static PipedInputStream pipedIS = new PipedInputStream();
8. static PipedOutputStream pipedOS = new PipedOutputStream();
9. public static void main(String[] a){
10. try {
11. pipedIS.connect(pipedOS);
12. }catch(IOException e) {
13. System.err.println(" 连接失败");
14. System.exit(1);
15. }
16. byte[] inArray = new byte[10];
17. int bytesRead = 0;
18. startWriterThread(); //启动write线程
19. try {
20. bytesRead = pipedIS.read(inArray, 0, 10); //每次读10字节
21. System.out.println("已读取"+bytesRead+"字节...");
22. int i=0;
23. while(bytesRead != -1) {
24. bytesRead = pipedIS.read(inArray, 0, 10);
25. System.out.println("已读取"+bytesRead+" 字节...");
26. //下面这段防止程序无限执行
27. /*i++;
28. if (i==300) {
29. System.exit(0);
30. }*/
31. }
32. }catch(IOException e) {
33. System.err.println(" 读取pipedIS时出现错误: " + e);
34. System.exit(1);
35. }
36. }
37.
38. //创建一个独立的线程 执行write操作
39. private static void startWriterThread(){
40. new Thread(new Runnable(){
41. public void run(){
42. byte outArray[]=new byte[2000];
43. int j=0;
44. while (j==0) { //无限循环
45. try {
46. pipedOS.write(outArray, 0, 2000); //一次最多写入1024字节
47. j++;
48. } catch (IOException e) {
49. System.err.println("写操作失败");
50. System.exit(1);
51. }
52. System.out.println("已发送2000字节...");
53. }
54. }
55. }).start();
56. }
57. }
也许我们不能说这个问题是Java管道流设计上的缺陷,但在应用管道流时,它是一个必须密切注意的问题。下面我们来看看第二 个更重要(更危险的)问题。
<!-- 分页 -->
1.2 注意事项二
从PipedInputStream读取数据时,如果符合下面三个条件,就会出现IOException异常:
- 试图从PipedInputStream读取数据,
- PipedInputStream的缓冲区为“空”(即不存在可读取的数据),
- 最后一个向PipedOutputStream写数据的线程不再活动(通过Thread.isAlive()检测)。
这是一个很微妙的时刻,同时也是一个极其重要的时刻。假定有一个线程w向PipedOutputStream写入数据;另一个线程r从对应的 PipedInputStream读取数据。下面一系列的事件将导致r线程在试图读取PipedInputStream时遇到IOException异 常:
- w向PipedOutputStream写入数据。
- w结束(w.isAlive()返回false)。
- r从PipedInputStream读取w写入的数据,清空PipedInputStream的缓冲区。
- r试图再次从PipedInputStream读取数据。这时PipedInputStream的缓冲区已经为空,而且w已经结束,从而导致在读 操作执行时出现IOException异常。
构造一个程序示范这个问题并不困难,只需从Listing 3的startWriterThread()方法中,删除while(true)条件。这个改动阻止了执行写操作的方法循环执行,使得执行写操作的方法在 一次写入操作之后就结束运行。如前所述,此时主线程试图读取PipedInputStraem时,就会遇到一个IOException异常。
这是一种比较少见的情况,而且不存在直接修正它的方法。请不要通过从管道流派生子类的方法修正该问题——在这里使用继承是完全不合适的。而且, 如果Sun以后改变了管道流的实现方法,现在所作的修改将不再有效。
最后一个问题和第二个问题很相似,不同之处在于,它在读线程(而不是写线程)结束时产生IOException异常。
1.3 注意事项三
如果一个写操作在PipedOutputStream上执行,同时最近从对应PipedInputStream读取的线程已经不再活动(通过 Thread.isAlive()检测),则写操作将抛出一个IOException异常。假定有两个线程w和r,w向 PipedOutputStream写入数据,而r则从对应的PipedInputStream读取。下面一系列的事件将导致w线程在试图写入 PipedOutputStream时遇到IOException异常:
- 写操作线程w已经创建,但r线程还不存在。
- w向PipedOutputStream写入数据。
- 读线程r被创建,并从PipedInputStream读取数据。
- r线程结束。
- w企图向PipedOutputStream写入数据,发现r已经结束,抛出IOException异常。
实际上,这个问题不象第二个问题那样棘手。和多个读线程/单个写线程的情况相比,也许在应用中有一个读线程(作为响应请求的服务器)和多个写线 程(发出请求)的情况更为常见。
1.4 解决问题
要防止管道流前两个局限所带来的问题,方法之一是用一个ByteArrayOutputStream作为代理或替代 PipedOutputStream。Listing 4显示了一个LoopedStreams类,它用一个ByteArrayOutputStream提供和Java管道流类似的功能,但不会出现死锁和 IOException异常。这个类的内部仍旧使用管道流,但隔离了本文介绍的前两个问题。我们先来看看这个类的公用方法(参见图2)。构造函数很简单, 它连接管道流,然后调用startByteArrayReaderThread()方法(稍后再讨论该方法)。getOutputStream()方法返 回一个OutputStream(具体地说,是一个ByteArrayOutputStream)用以替代PipedOutputStream。写入该 OutputStream的数据最终将在getInputStream()方法返回的流中作为输入出现。和使用PipedOutputStream的情形 不同,向ByteArrayOutputStream写入数据的线程的激活、写数据、结束不会带来负面效果。
图2:ByteArrayOutputStream原理
【Listing 4:防止管道流应用中出现的常见问题】
import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; public class LoopedStreams { private PipedOutputStream pipedOS=new PipedOutputStream(); private boolean keepRunning=true; private ByteArrayOutputStream byteArrayOS=new ByteArrayOutputStream(){ public void close() { System.out.println("byteArrayOS.close()"); keepRunning=false; try { super.close(); pipedOS.close(); } catch (IOException e) { System.out.println("关闭byteArrayOS错误:"+e.getMessage()); System.exit(1); } } }; private PipedInputStream pipedIS=new PipedInputStream(){ public void close() { System.out.println("pipedIS.close()"); keepRunning=false; try { super.close(); } catch (IOException e) { System.out.println("关闭pipedIS错误:"+e.getMessage()); System.exit(1); } } }; public LoopedStreams() throws IOException{ // TODO 自动生成构造函数存根 pipedOS.connect(pipedIS); startByteArrayReaderThread(); } public OutputStream getOutputStream() { return byteArrayOS; } public InputStream getInputStream() { return pipedIS; } public void startByteArrayReaderThread() { new Thread(new Runnable(){ public void run() { while (keepRunning) { if (byteArrayOS.size()>0) { //检查流里面的字节 byte buffer[]=null; synchronized (byteArrayOS) { buffer=byteArrayOS.toByteArray(); byteArrayOS.reset(); //清空缓冲区 } try { pipedOS.write(buffer, 0, buffer.length); //把提取的流发送到pipedOS } catch (IOException e) { System.out.println(e.getMessage()); System.exit(1); } }else { //没有数据可用,线程进入休眠状态 try { Thread.sleep(1000); //休眠1秒 } catch (InterruptedException e) { } } } } }).start(); } }
1. import java.io.ByteArrayOutputStream;
2. import java.io.IOException;
3. import java.io.InputStream;
4. import java.io.OutputStream;
5. import java.io.PipedInputStream;
6. import java.io.PipedOutputStream;
7.
8. public class LoopedStreams {
9.
10. private PipedOutputStream pipedOS=new PipedOutputStream();
11. private boolean keepRunning=true;
12. private ByteArrayOutputStream byteArrayOS=new ByteArrayOutputStream(){
13. public void close() {
14. System.out.println("byteArrayOS.close()");
15. keepRunning=false;
16. try {
17. super.close();
18. pipedOS.close();
19. } catch (IOException e) {
20. System.out.println("关闭 byteArrayOS错误:"+e.getMessage());
21. System.exit(1);
22. }
23. }
24. };
25. private PipedInputStream pipedIS=new PipedInputStream(){
26. public void close() {
27. System.out.println("pipedIS.close()");
28. keepRunning=false;
29. try {
30. super.close();
31. } catch (IOException e) {
32. System.out.println("关闭pipedIS错误:"+e.getMessage());
33. System.exit(1);
34. }
35. }
36. };
37.
38. public LoopedStreams() throws IOException{
39. // TODO 自动生成构造函数存根
40. pipedOS.connect(pipedIS);
41. startByteArrayReaderThread();
42. }
43.
44. public OutputStream getOutputStream() {
45. return byteArrayOS;
46. }
47.
48. public InputStream getInputStream() {
49. return pipedIS;
50. }
51.
52. public void startByteArrayReaderThread() {
53. new Thread(new Runnable(){
54. public void run() {
55. while (keepRunning) {
56. if (byteArrayOS.size()>0) { //检查流里面的字节
57. byte buffer[]=null;
58. synchronized (byteArrayOS) {
59. buffer=byteArrayOS.toByteArray();
60. byteArrayOS.reset(); //清空缓冲区
61. }
62. try {
63. pipedOS.write(buffer, 0, buffer.length); //把提取的流发送到pipedOS
64. } catch (IOException e) {
65. System.out.println(e.getMessage());
66. System.exit(1);
67. }
68. }else { //没有数据可用,线程进入休眠状态
69. try {
70. Thread.sleep(1000); //休眠1秒
71. } catch (InterruptedException e) {
72. }
73.
74. }
75. }
76. }
77. }).start();
78. }
79. }
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的ConsoleToTextArea类扩展Swing JTextArea捕获控制台输出。不要对这个类有这么多代码感到惊讶,必须指出的是,ConsoleToTextArea类有超过50%的代码用来进行 测试。
【Listing 5 :截获Java控制台输出】
1. import java.awt.BorderLayout;
2. import java.awt.event.WindowAdapter;
3. import java.awt.event.WindowEvent;
4. import java.io.BufferedReader;
5. import java.io.IOException;
6. import java.io.InputStream;
7. import java.io.InputStreamReader;
8. import java.io.PrintStream;
9.
10. import javax.swing.JFrame;
11. import javax.swing.JOptionPane;
12. import javax.swing.JScrollPane;
13. import javax.swing.JTextArea;
14. import javax.swing.text.Document;
15.
16. public class ConsoleToTextArea extends JTextArea{
17.
18. //转发所有从各个数组元素读取的数据到JTextArea
19. public ConsoleToTextArea(InputStream[] inStreams) {
20. for(int i = 0; i < inStreams.length; i++){
21. startConsoleReaderThread(inStreams[i]);
22. }
23. }
24.
25. //捕获和显示所有写入到控制台流的数据
26. public ConsoleToTextArea() throws IOException{
27. final LoopedStreams loopedStreams=new LoopedStreams();
28.
29. //重定向System.out和System.err
30. PrintStream printStream=new PrintStream(loopedStreams.getOutputStream());
31. System.setOut(printStream);
32. System.setErr(printStream);
33. startConsoleReaderThread(loopedStreams.getInputStream());
34. }
35.
36. private void startConsoleReaderThread(InputStream inStream){
37. final BufferedReader bufferedReader=
38. new BufferedReader(new InputStreamReader(inStream));
39.
40. new Thread(new Runnable(){
41. public void run() {
42. StringBuffer stringBuffer=new StringBuffer();
43. try {
44. String s;
45. Document document=getDocument();
46. while ((s=bufferedReader.readLine())!=null) {
47. boolean caretAtEnd=false;
48. caretAtEnd=getCaretPosition()==document.getLength()?true:false;
49. stringBuffer.setLength(0);
50. append(stringBuffer.append(s).append("\n").toString());
51. if (caretAtEnd) {
52. setCaretPosition(document.getLength());
53. }
54. }
55. } catch (IOException e) {
56. JOptionPane.showMessageDialog(null, "从 bufferedReader中读取错误:"+e.getMessage());
57. System.exit(1);
58. }
59. }
60. }).start();
61. }
62.
63. /**
64. * @param args 以下代码用于测试
65. */
66. public static void main(String[] args) {
67. JFrame jFrame=new JFrame("ConsoleToTextArea测试");
68. ConsoleToTextArea consoleToTextArea=null;
69. try {
70. consoleToTextArea=new ConsoleToTextArea();
71. } catch (IOException e) {
72. System.err.println("创建 ConsoleToTextArea出错");
73. System.exit(1);
74. }
75. jFrame.getContentPane().add(new JScrollPane(consoleToTextArea),BorderLayout.CENTER);
76. jFrame.setBounds(50,50,300,300);
77. jFrame.setVisible(true);
78. jFrame.addWindowListener(new WindowAdapter(){
79. public void windowClosing(WindowEvent e) {
80. System.exit(0);
81. }
82. });
83.
84. startWriterTestThread("写操作#1",System.err,950,50);
85. startWriterTestThread("写操作#2",System.out,500,50);
86. startWriterTestThread("写操作#3",System.out,200,50);
87. startWriterTestThread("写操作#4",System.out,1000,50);
88. startWriterTestThread("写操作#5",System.err,850,50);
89. }
90.
91. public static void startWriterTestThread(final String name,final PrintStream ps,final int dalay,final int count) {
92. new Thread(new Runnable(){
93. public void run() {
94. for (int i = 1; i < count; i++) {
95. ps.println(name+" i="+i);
96. try {
97. Thread.sleep(dalay);
98. } catch (InterruptedException e) {
99. // TODO: handle exception
100. }
101. }
102. }
103. }).start();
104. }
105.
106. }
import java.awt.BorderLayout; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.text.Document; public class ConsoleToTextArea extends JTextArea{ //转发所有从各个数组元素读取的数据到JTextArea public ConsoleToTextArea(InputStream[] inStreams) { for(int i = 0; i < inStreams.length; i++){ startConsoleReaderThread(inStreams[i]); } } //捕获和显示所有写入到控制台流的数据 public ConsoleToTextArea() throws IOException{ final LoopedStreams loopedStreams=new LoopedStreams(); //重定向System.out和System.err PrintStream printStream=new PrintStream(loopedStreams.getOutputStream()); System.setOut(printStream); System.setErr(printStream); startConsoleReaderThread(loopedStreams.getInputStream()); } private void startConsoleReaderThread(InputStream inStream){ final BufferedReader bufferedReader= new BufferedReader(new InputStreamReader(inStream)); new Thread(new Runnable(){ public void run() { StringBuffer stringBuffer=new StringBuffer(); try { String s; Document document=getDocument(); while ((s=bufferedReader.readLine())!=null) { boolean caretAtEnd=false; caretAtEnd=getCaretPosition()==document.getLength()?true:false; stringBuffer.setLength(0); append(stringBuffer.append(s).append("\n").toString()); if (caretAtEnd) { setCaretPosition(document.getLength()); } } } catch (IOException e) { JOptionPane.showMessageDialog(null, "从bufferedReader中读取错误:"+e.getMessage()); System.exit(1); } } }).start(); } /** * @param args 以下代码用于测试 */ public static void main(String[] args) { JFrame jFrame=new JFrame("ConsoleToTextArea测试"); ConsoleToTextArea consoleToTextArea=null; try { consoleToTextArea=new ConsoleToTextArea(); } catch (IOException e) { System.err.println("创建ConsoleToTextArea出错"); System.exit(1); } jFrame.getContentPane().add(new JScrollPane(consoleToTextArea),BorderLayout.CENTER); jFrame.setBounds(50,50,300,300); jFrame.setVisible(true); jFrame.addWindowListener(new WindowAdapter(){ public void windowClosing(WindowEvent e) { System.exit(0); } }); startWriterTestThread("写操作#1",System.err,950,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); } public static void startWriterTestThread(final String name,final PrintStream ps,final int dalay,final int count) { new Thread(new Runnable(){ public void run() { for (int i = 1; i < count; i++) { ps.println(name+" i="+i); try { Thread.sleep(dalay); } catch (InterruptedException e) { // TODO: handle exception } } } }).start(); } }
main()方法创建了一个JFrame,JFrame包含一个ConsoleToTextArea 的实例。这些代码并没有什么特别之处。Frame显示出来之后,main()方法 启动一系列的写操作线程,写操作线程向控制台流输出大量信息。ConsoleToTextArea 捕 获并显示这些信息,如图一所示。
<!-- 分页 -->
ConsoleToTextArea提供了两个构造函数。没有参数的构造函数用来捕获和显示所有写入到控制台流的数据,有一个 InputStream[]参数的构造函数转发所有从各个数组元素读取的数据到JTextArea。稍后将有一个例子显示这个构造函数的用处。首先我们来 看看没有参数的ConsoleToTextArea构造函数。这个函数首先创建一个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程序)的控制台数据又是另一回事。 ConsoleToTextArea提供了捕获其他应用的输出时需要的基础功能,Listing 6的OtherProgrameConsoleCapture利用ConsoleToTextArea,截取其他应用的输出信息然后显示在 ConsoleToTextArea中。
【Listing 6 :截获其他程序的控制台输出】
import java.awt.BorderLayout; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.IOException; import java.io.InputStream; import javax.swing.JFrame; import javax.swing.JScrollPane; public class OtherProgrameConsoleCapture { private static Process process; /** * @param args */ public static void main(String[] args) { if (args.length==0) { System.err.println("用法:java OtherProgrameConsoleCapture " + "<程序名字>{参数1 参数2...}"); /*BufferedReader cin = new BufferedReader( new InputStreamReader(System.in )); try { String arg= cin.readLine(); } catch (Exception e) { // TODO: handle exception }*/ System.exit(0); } try { //启动命令行指定程序的新进程 process=Runtime.getRuntime().exec(args); } catch (IOException e) { System.err.println("创建进程出错!"); System.exit(1); } //获得新进程写入的流 InputStream[] inStreams =new InputStream[] { process.getInputStream(),process.getErrorStream()}; ConsoleToTextArea consoleToTextArea=new ConsoleToTextArea(inStreams); JFrame jFrame=new JFrame(args[0]+"控制台输出"); jFrame.getContentPane().add(new JScrollPane(consoleToTextArea),BorderLayout.CENTER); jFrame.setBounds(50, 50, 400, 400); jFrame.setVisible(true); jFrame.addWindowListener(new WindowAdapter(){ public void windowClosing(WindowEvent e) { process.destroy(); try { process.waitFor(); //win98下可能会被挂起 } catch (InterruptedException ee) { System.exit(0); } } }); } }
1. import java.awt.BorderLayout;
2. import java.awt.event.WindowAdapter;
3. import java.awt.event.WindowEvent;
4. import java.io.IOException;
5. import java.io.InputStream;
6.
7. import javax.swing.JFrame;
8. import javax.swing.JScrollPane;
9.
10. public class OtherProgrameConsoleCapture {
11.
12. private static Process process;
13. /**
14. * @param args
15. */
16. public static void main(String[] args) {
17. if (args.length==0) {
18. System.err.println("用法:java OtherProgrameConsoleCapture " +
19. "<程序名字>{参数1 参数2...}");
20. /*BufferedReader cin = new BufferedReader( new InputStreamReader(System.in ));
21. try {
22. String arg= cin.readLine();
23. } catch (Exception e) {
24. // TODO: handle exception
25. }*/
26. System.exit(0);
27. }
28. try {
29. //启动命令行指定程序的新进程
30. process=Runtime.getRuntime().exec(args);
31. } catch (IOException e) {
32. System.err.println("创建进程出错!");
33. System.exit(1);
34. }
35. //获得新进程写入的流
36. InputStream[] inStreams =new InputStream[] {
37. process.getInputStream(),process.getErrorStream()};
38.
39. ConsoleToTextArea consoleToTextArea=new ConsoleToTextArea(inStreams);
40. JFrame jFrame=new JFrame(args[0]+"控制台输出");
41. jFrame.getContentPane().add(new JScrollPane(consoleToTextArea),BorderLayout.CENTER);
42. jFrame.setBounds(50, 50, 400, 400);
43. jFrame.setVisible(true);
44. jFrame.addWindowListener(new WindowAdapter(){
45. public void windowClosing(WindowEvent e) {
46. process.destroy();
47. try {
48. process.waitFor(); //win98下可能会被挂起
49. } catch (InterruptedException ee) {
50. System.exit(0);
51. }
52. }
53. });
54.
55. }
56.
57. }
OtherProgrameConsoleCapture 的工作过程如下:首先利用Runtime.exec()方法启动指定程序的一个新进程。启动新进程之后,从结果Process对象得到它的控制台流。之 后,把这些控制台流传入ConsoleToTextArea(InputStream[])构造函数(这就是带参数ConsoleToTextArea构 造函数的用处)。使用OtherProgrameConsoleCapture 时,在命令行上指定待截取其输出的程序名字。例如,如果在Windows XP下进入cmd中进入到classes目录 执行 java OtherProgrameConsoleCapture ping www.yahoo.akadns.net 查看效果(图3)
图3:截取其他程序的控制台输出
使用OtherProgrameConsoleCapture 时应该注意,被截取输出的应用程序最初输出的一些文本可能无法截取。因为在调用Runtime.exec()和ConsoleToTextArea初始化 完成之间存在一小段时间差。在这个时间差内,应用程序输出的文本会丢失。当OtherProgrameConsoleCapture 窗口被关闭,process.destory()调用试图关闭Java程序开始时创建的进程。测试结果显示出,destroy()方法不一定总是有效(至 少在Windows 98上是这样的)。似乎当待关闭的进程启动了额外的进程时,则那些进程不会被关闭。此外,在这种情况下 OtherProgrameConsoleCapture 程序看起来未能正常结束。但在Windows NT下,一切正常。如果用JDK v1.1.x运行OtherProgrameConsoleCapture ,关闭窗口时会出现一个NullPointerException。这是一个JDK的Bug,JDK 1.2.x和JDK 1.3.x下就不会出现问题。