即使在圖形用戶界面占統治地位的今天控制台輸出仍舊在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下就不會出現問題
From:http://tw.wingwit.com/Article/program/Java/hx/201311/27074.html