在Java程式中截獲控制台輸出
內容:
一、Java管道流
1.1 注意事項一
1.2 注意事項二
1.3 注意事項三
1.4 解決問題
二、捕獲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 = pipedIS.read(inArray, 0, 10);
while(bytesRead != -1) {
System.out.println("已經讀取" +
bytesRead + "位元組...");
bytesRead = pipedIS.read(inArray, 0, 10);
}
}
catch(IOException e) {
System.err.println("讀取輸入錯誤.");
System.exit(1);
}
} // main()
// 建立一個獨立的線程
// 執行寫入PipedOutputStream的操作
private static void startWriterThread() {
new Thread(new Runnable() {
public void run() {
byte[] outArray = new byte[2000];
while(true) { // 無終止條件的迴圈
try {
// 在該線程阻塞之前,有最多1024位元組的資料被寫入
pipedOS.write(outArray, 0, 2000);
}
catch(IOException e) {
System.err.println("寫操作錯誤");
System.exit(1);
}
System.out.println(" 已經發送2000位元組...");
}
}
}).start();
} // startWriterThread()
} // Listing3
也許我們不能說這個問題是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異常。這個類的內部仍舊使用管道流,但隔離了本文介紹的前兩個問題。我們先來看看這個類的公用方法(參見圖3)。建構函式很簡單, 它串連管道流,然後調用startByteArrayReaderThread()方法(稍後再討論該方法)。getOutputStream()方法返 回一個OutputStream(具體地說,是一個ByteArrayOutputStream)用以替代PipedOutputStream。寫入該 OutputStream的資料最終將在getInputStream()方法返回的流中作為輸入出現。和使用PipedOutputStream的情形 不同,向ByteArrayOutputStream寫入資料的線程的啟用、寫資料、結束不會帶來負面效果。
圖三:ByteArrayOutputStream原理
【Listing 4:防止管道流應用中出現的常見問題】
import java.io.*;
public class LoopedStreams {
private PipedOutputStream pipedOS =
new PipedOutputStream();
private boolean keepRunning = true;
private ByteArrayOutputStream byteArrayOS =
new ByteArrayOutputStream() {
public void close() {
keepRunning = false;
try {
super.close();
pipedOS.close();
}
catch(IOException e) {
// 記錄錯誤或其他處理
// 為簡單計,此處我們直接結束
System.exit(1);
}
}
};
private PipedInputStream pipedIS = new PipedInputStream() {
public void close() {
keepRunning = false;
try {
super.close();
}
catch(IOException e) {
// 記錄錯誤或其他處理
// 為簡單計,此處我們直接結束
System.exit(1);
}
}
};
public LoopedStreams() throws IOException {
pipedOS.connect(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(byteArrayOS.size() > 0) {
byte[] buffer = null;
synchronized(byteArrayOS) {
buffer = byteArrayOS.toByteArray();
byteArrayOS.reset(); // 清除緩衝區
}
try {
// 把提取到的資料發送給PipedOutputStream
pipedOS.write(buffer, 0, buffer.length);
}
catch(IOException e) {
// 記錄錯誤或其他處理
// 為簡單計,此處我們直接結束
System.exit(1);
}
}
else // 沒有資料可用,線程進入睡眠狀態
try {
// 每隔1秒查看ByteArrayOutputStream檢查新資料
Thread.sleep(1000);
}
catch(InterruptedException e) {}
}
}
}).start();
} // startByteArrayReaderThread()
} // LoopedStreams
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 程式中的多線程