在Java程序中截獲控制檯輸出[轉]

在Java程序中截獲控制檯輸出

在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

文檔選項
將此頁作爲電子郵件發送

拓展 Tomcat 應用
下載 IBM 開源 J2EE 應用服務器 WAS CE 新版本 V1.1

級別: 初級

俞良鬆 ([email protected]), 軟件工程師, 獨立顧問和自由撰稿人

2001 年 10 月 16 日

在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下就不會出現問題。

作者:俞良鬆 ([email protected]), 軟件工程師, 獨立顧問和自由撰稿人 

發佈了6 篇原創文章 · 獲贊 0 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章