java截獲標準輸出(1)(轉)

ba發表於2007-08-15
java截獲標準輸出(1)(轉)[@more@]在Java程式中截獲控制檯輸出
內容:

一、Java管道流
  1.1 注意事項一
  1.2 注意事項二
  1.3 注意事項三
  1.4 解決問題
二、捕獲Java控制檯輸出
三、捕獲其他程式的控制檯輸出
參考資料
關於作者




俞良松 ()
軟體工程師,獨立顧問和自由撰稿人
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

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-958248/,如需轉載,請註明出處,否則將追究法律責任。

相關文章