即使在圖形用戶界面占統(tǒng)治地位的今天,控制臺(tái)輸出仍舊在Java程序中占有重要地位。控制臺(tái)不僅是Java程序默認(rèn)的堆棧跟蹤和錯(cuò)誤信息輸出窗口,而且還
是一種實(shí)用的調(diào)試工具(特別是對(duì)習(xí)慣于使用println()的人來說)。然而,控制臺(tái)窗口有著許多局限。例如在Windows
9x平臺(tái)上,DOS控制臺(tái)只能容納50行輸出。如果Java程序一次性向控制臺(tái)輸出大量?jī)?nèi)容,要查看這些內(nèi)容就很困難了。
對(duì)于使用javaw這個(gè)啟動(dòng)程序的開發(fā)者來說,控制臺(tái)窗口尤其寶貴。因?yàn)橛胘avaw啟動(dòng)java程序時(shí),根本不會(huì)有控制臺(tái)窗口出現(xiàn)。如果程序遇到了問題
并拋出異常,根本無法查看Java運(yùn)行時(shí)環(huán)境寫入到System.out或System.err的調(diào)用堆棧跟蹤信息。為了捕獲堆棧信息,一些人采取了用
try/catch()塊封裝main()的方式,但這種方式不一定總是有效,在Java運(yùn)行時(shí)的某些時(shí)刻,一些描述性錯(cuò)誤信息會(huì)在拋出異常之前被寫入
System.out和System.err;除非能夠監(jiān)測(cè)這兩個(gè)控制臺(tái)流,否則這些信息就無法看到。
因此,有些時(shí)候檢查Java運(yùn)行時(shí)環(huán)境(或第三方程序)寫入到控制臺(tái)流的數(shù)據(jù)并采取合適的操作是十分必要的。本文討論的主題之一就是創(chuàng)建這樣一個(gè)輸入流,
從這個(gè)輸入流中可以讀入以前寫入Java控制臺(tái)流(或任何其他程序的輸出流)的數(shù)據(jù)。我們可以想象寫入到輸出流的數(shù)據(jù)立即以輸入的形式“回流”到了
Java程序。
本文的目標(biāo)是設(shè)計(jì)一個(gè)基于Swing的文本窗口顯示控制臺(tái)輸出。在此期間,我們還將討論一些和Java管道流(PipedInputStream和
PipedOutputStream)有關(guān)的重要注意事項(xiàng)。圖一顯示了用來截取和顯示控制臺(tái)文本輸出的Java程序,用戶界面的核心是一個(gè)
JTextArea。最后,我們還要?jiǎng)?chuàng)建一個(gè)能夠捕獲和顯示其他程序(可以是非Java的程序)控制臺(tái)輸出的簡(jiǎn)單程序。
圖一:多線程的控制臺(tái)輸出截取程序
一、Java管道流
要在文本框中顯示控制臺(tái)輸出,我們必須用某種方法“截取”控制臺(tái)流。換句話說,我們要有一種高效地讀取寫入到System.out和System.err
所有內(nèi)容的方法。如果你熟悉Java的管道流PipedInputStream和PipedOutputStream,就會(huì)相信我們已經(jīng)擁有最有效的工
具。
寫入到PipedOutputStream輸出流的數(shù)據(jù)可以從對(duì)應(yīng)的PipedInputStream輸入流讀取。Java的管道流極大地方便了我們截取控制臺(tái)輸出。Listing
1顯示了一種非常簡(jiǎn)單的截取控制臺(tái)輸出方案。
【Listing 1:用管道流截取控制臺(tái)輸出】 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);
|
可以看到,這里的代碼極其簡(jiǎn)單。我們只是建立了一個(gè)PipedInputStream,把它設(shè)置為所有寫入控制臺(tái)流的數(shù)據(jù)的最終目的地。所有寫入到控制臺(tái)
流的數(shù)據(jù)都被轉(zhuǎn)到PipedOutputStream,這樣,從相應(yīng)的PipedInputStream讀取就可以迅速地截獲所有寫入控制臺(tái)流的數(shù)據(jù)。接
下來的事情似乎只剩下在Swing
JTextArea中顯示從pipedIS流讀取的數(shù)據(jù),得到一個(gè)能夠在文本框中顯示控制臺(tái)輸出的程序。遺憾的是,在使用Java管道流時(shí)有一些重要的注
意事項(xiàng)。只有認(rèn)真對(duì)待所有這些注意事項(xiàng)才能保證Listing
1的代碼穩(wěn)定地運(yùn)行。下面我們來看第一個(gè)注意事項(xiàng)。
1.1 注意事項(xiàng)一
PipedInputStream運(yùn)用的是一個(gè)1024字節(jié)固定大小的循環(huán)緩沖區(qū)。寫入PipedOutputStream的數(shù)據(jù)實(shí)際上保存到對(duì)應(yīng)的
PipedInputStream的內(nèi)部緩沖區(qū)。從PipedInputStream執(zhí)行讀操作時(shí),讀取的數(shù)據(jù)實(shí)際上來自這個(gè)內(nèi)部緩沖區(qū)。如果對(duì)應(yīng)的
PipedInputStream輸入緩沖區(qū)已滿,任何企圖寫入PipedOutputStream的線程都將被阻塞。而且這個(gè)寫操作線程將一直阻塞,直
至出現(xiàn)讀取PipedInputStream的操作從緩沖區(qū)刪除數(shù)據(jù)。
這意味著,向PipedOutputStream寫數(shù)據(jù)的線程不應(yīng)該是負(fù)責(zé)從對(duì)應(yīng)PipedInputStream讀取數(shù)據(jù)的唯一線程。從圖二可以清楚地
看出這里的問題所在:假設(shè)線程t是負(fù)責(zé)從PipedInputStream讀取數(shù)據(jù)的唯一線程;另外,假定t企圖在一次對(duì)
PipedOutputStream的write()方法的調(diào)用中向?qū)?yīng)的PipedOutputStream寫入2000字節(jié)的數(shù)據(jù)。在t線程阻塞之
前,它最多能夠?qū)懭?024字節(jié)的數(shù)據(jù)(PipedInputStream內(nèi)部緩沖區(qū)的大小)。然而,一旦t被阻塞,讀取
PipedInputStream的操作就再也不會(huì)出現(xiàn),因?yàn)閠是唯一讀取PipedInputStream的線程。這樣,t線程已經(jīng)完全被阻塞,同時(shí),
所有其他試圖向PipedOutputStream寫入數(shù)據(jù)的線程也將遇到同樣的情形。
圖二:管道流工作過程
這并不意味著在一次write()調(diào)用中不能寫入多于1024字節(jié)的數(shù)據(jù)。但應(yīng)當(dāng)保證,在寫入數(shù)據(jù)的同時(shí),有另一個(gè)線程從PipedInputStream讀取數(shù)據(jù)。
Listing
2示范了這個(gè)問題。這個(gè)程序用一個(gè)線程交替地讀取PipedInputStream和寫入PipedOutputStream。每次調(diào)用write()向
PipedInputStream的緩沖區(qū)寫入20字節(jié),每次調(diào)用read()只從緩沖區(qū)讀取并刪除10個(gè)字節(jié)。內(nèi)部緩沖區(qū)最終會(huì)被寫滿,導(dǎo)致寫操作阻
塞。由于我們用同一個(gè)線程執(zhí)行讀、寫操作,一旦寫操作被阻塞,就不能再?gòu)腜ipedInputStream讀取數(shù)據(jù)。
【Listing 2:用同一個(gè)線程執(zhí)行讀/寫操作導(dǎo)致線程阻塞】 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發(fā)送20字節(jié)數(shù)據(jù) pipedOS.write(outArray, 0, 20); System.out.println(" 已發(fā)送20字節(jié)..."); // 在每一次循環(huán)迭代中,讀入10字節(jié) // 發(fā)送20字節(jié) bytesRead = pipedIS.read(inArray, 0, 10); int i=0; while(bytesRead != -1) { pipedOS.write(outArray, 0, 20); System.out.println(" 已發(fā)送20字節(jié)..."+i); i++; bytesRead = pipedIS.read(inArray, 0, 10); } } catch(IOException e) { System.err.println("讀取pipedIS時(shí)出現(xiàn)錯(cuò)誤: " + e); System.exit(1); } } // main() }
|
只
要把讀/寫操作分開到不同的線程,Listing
2的問題就可以輕松地解決。Listing 3是Listing
2經(jīng)過修改后的版本,它在一個(gè)單獨(dú)的線程中執(zhí)行寫入PipedOutputStream的操作(和讀取線程不同的線程)。為證明一次寫入的數(shù)據(jù)可以超過
1024字節(jié),我們讓寫操作線程每次調(diào)用PipedOutputStream的write()方法時(shí)寫入2000字節(jié)。那么,在
startWriterThread()方法中創(chuàng)建的線程是否會(huì)阻塞呢?按照J(rèn)ava運(yùn)行時(shí)線程調(diào)度機(jī)制,它當(dāng)然會(huì)阻塞。寫操作在阻塞之前實(shí)際上最多只能
寫入1024字節(jié)的有效載荷(即PipedInputStream緩沖區(qū)的大小)。但這并不會(huì)成為問題,因?yàn)橹骶€程(main)很快就會(huì)從
PipedInputStream的循環(huán)緩沖區(qū)讀取數(shù)據(jù),空出緩沖區(qū)空間。最終,寫操作線程會(huì)從上一次中止的地方重新開始,寫入2000字節(jié)有效載荷中的
剩余部分。
【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; // 啟動(dòng)寫操作線程 startWriterThread(); try { bytesRead = pipedIS.read(inArray, 0, 10); while(bytesRead != -1) { System.out.println("已經(jīng)讀取" + bytesRead + "字節(jié)..."); bytesRead = pipedIS.read(inArray, 0, 10); } } catch(IOException e) { System.err.println("讀取輸入錯(cuò)誤."); System.exit(1); } } // main() // 創(chuàng)建一個(gè)獨(dú)立的線程 // 執(zhí)行寫入PipedOutputStream的操作 private static void startWriterThread() { new Thread(new Runnable() { public void run() { byte[] outArray = new byte[2000]; while(true) { // 無終止條件的循環(huán) try { // 在該線程阻塞之前,有最多1024字節(jié)的數(shù)據(jù)被寫入 pipedOS.write(outArray, 0, 2000); } catch(IOException e) { System.err.println("寫操作錯(cuò)誤"); System.exit(1); } System.out.println(" 已經(jīng)發(fā)送2000字節(jié)..."); } } }).start(); } // startWriterThread() } // Listing3
|
也許我們不能說這個(gè)問題是Java管道流設(shè)計(jì)上的缺陷,但在應(yīng)用管道流時(shí),它是一個(gè)必須密切注意的問題。下面我們來看看第二個(gè)更重要(更危險(xiǎn)的)問題。
1.2 注意事項(xiàng)二
從PipedInputStream讀取數(shù)據(jù)時(shí),如果符合下面三個(gè)條件,就會(huì)出現(xiàn)IOException異常:
- 試圖從PipedInputStream讀取數(shù)據(jù),
- PipedInputStream的緩沖區(qū)為“空”(即不存在可讀取的數(shù)據(jù)),
-
最后一個(gè)向PipedOutputStream寫數(shù)據(jù)的線程不再活動(dòng)(通過Thread.isAlive()檢測(cè))。
這是一個(gè)很微妙的時(shí)刻,同時(shí)也是一個(gè)極其重要的時(shí)刻。假定有一個(gè)線程w向PipedOutputStream寫入數(shù)據(jù);另一個(gè)線程r從對(duì)應(yīng)的
PipedInputStream讀取數(shù)據(jù)。下面一系列的事件將導(dǎo)致r線程在試圖讀取PipedInputStream時(shí)遇到IOException異
常:
- w向PipedOutputStream寫入數(shù)據(jù)。
- w結(jié)束(w.isAlive()返回false)。
-
r從PipedInputStream讀取w寫入的數(shù)據(jù),清空PipedInputStream的緩沖區(qū)。
-
r試圖再次從PipedInputStream讀取數(shù)據(jù)。這時(shí)PipedInputStream的緩沖區(qū)已經(jīng)為空,而且w已經(jīng)結(jié)束,從而導(dǎo)致在讀操作執(zhí)行時(shí)出現(xiàn)IOException異常。
構(gòu)
造一個(gè)程序示范這個(gè)問題并不困難,只需從Listing
3的startWriterThread()方法中,刪除while(true)條件。這個(gè)改動(dòng)阻止了執(zhí)行寫操作的方法循環(huán)執(zhí)行,使得執(zhí)行寫操作的方法在
一次寫入操作之后就結(jié)束運(yùn)行。如前所述,此時(shí)主線程試圖讀取PipedInputStraem時(shí),就會(huì)遇到一個(gè)IOException異常。
這是一種比較少見的情況,而且不存在直接修正它的方法。請(qǐng)不要通過從管道流派生子類的方法修正該問題――在這里使用繼承是完全不合適的。而且,如果Sun以后改變了管道流的實(shí)現(xiàn)方法,現(xiàn)在所作的修改將不再有效。
最后一個(gè)問題和第二個(gè)問題很相似,不同之處在于,它在讀線程(而不是寫線程)結(jié)束時(shí)產(chǎn)生IOException異常。
1.3 注意事項(xiàng)三
如果一個(gè)寫操作在PipedOutputStream上執(zhí)行,同時(shí)最近從對(duì)應(yīng)PipedInputStream讀取的線程已經(jīng)不再活動(dòng)(通過
Thread.isAlive()檢測(cè)),則寫操作將拋出一個(gè)IOException異常。假定有兩個(gè)線程w和r,w向
PipedOutputStream寫入數(shù)據(jù),而r則從對(duì)應(yīng)的PipedInputStream讀取。下面一系列的事件將導(dǎo)致w線程在試圖寫入
PipedOutputStream時(shí)遇到IOException異常:
- 寫操作線程w已經(jīng)創(chuàng)建,但r線程還不存在。
- w向PipedOutputStream寫入數(shù)據(jù)。
- 讀線程r被創(chuàng)建,并從PipedInputStream讀取數(shù)據(jù)。
- r線程結(jié)束。
-
w企圖向PipedOutputStream寫入數(shù)據(jù),發(fā)現(xiàn)r已經(jīng)結(jié)束,拋出IOException異常。
實(shí)際上,這個(gè)問題不象第二個(gè)問題那樣棘手。和多個(gè)讀線程/單個(gè)寫線程的情況相比,也許在應(yīng)用中有一個(gè)讀線程(作為響應(yīng)請(qǐng)求的服務(wù)器)和多個(gè)寫線程(發(fā)出請(qǐng)求)的情況更為常見。
1.4 解決問題
要防止管道流前兩個(gè)局限所帶來的問題,方法之一是用一個(gè)ByteArrayOutputStream作為代理或替代PipedOutputStream。
Listing
4顯示了一個(gè)LoopedStreams類,它用一個(gè)ByteArrayOutputStream提供和Java管道流類似的功能,但不會(huì)出現(xiàn)死鎖和
IOException異常。這個(gè)類的內(nèi)部仍舊使用管道流,但隔離了本文介紹的前兩個(gè)問題。我們先來看看這個(gè)類的公用方法(參見圖3)。構(gòu)造函數(shù)很簡(jiǎn)單,
它連接管道流,然后調(diào)用startByteArrayReaderThread()方法(稍后再討論該方法)。getOutputStream()方法返
回一個(gè)OutputStream(具體地說,是一個(gè)ByteArrayOutputStream)用以替代PipedOutputStream。寫入該
OutputStream的數(shù)據(jù)最終將在getInputStream()方法返回的流中作為輸入出現(xiàn)。和使用PipedOutputStream的情形
不同,向ByteArrayOutputStream寫入數(shù)據(jù)的線程的激活、寫數(shù)據(jù)、結(jié)束不會(huì)帶來負(fù)面效果。
圖三:ByteArrayOutputStream原理
【Listing 4:防止管道流應(yīng)用中出現(xiàn)的常見問題】 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) { // 記錄錯(cuò)誤或其他處理 // 為簡(jiǎn)單計(jì),此處我們直接結(jié)束 System.exit(1); } } }; private PipedInputStream pipedIS = new PipedInputStream() { public void close() { keepRunning = false; try { super.close(); } catch(IOException e) { // 記錄錯(cuò)誤或其他處理 // 為簡(jiǎn)單計(jì),此處我們直接結(jié)束 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) { // 檢查流里面的字節(jié)數(shù) if(byteArrayOS.size() > 0) { byte[] buffer = null; synchronized(byteArrayOS) { buffer = byteArrayOS.toByteArray(); byteArrayOS.reset(); // 清除緩沖區(qū) } try { // 把提取到的數(shù)據(jù)發(fā)送給PipedOutputStream pipedOS.write(buffer, 0, buffer.length); } catch(IOException e) { // 記錄錯(cuò)誤或其他處理 // 為簡(jiǎn)單計(jì),此處我們直接結(jié)束 System.exit(1); } } else // 沒有數(shù)據(jù)可用,線程進(jìn)入睡眠狀態(tài) try { // 每隔1秒查看ByteArrayOutputStream檢查新數(shù)據(jù) Thread.sleep(1000); } catch(InterruptedException e) {} } } }).start(); } // startByteArrayReaderThread() } // LoopedStreams
|
startByteArrayReaderThread()方法是整個(gè)類真正的關(guān)鍵所在。這個(gè)方法的目標(biāo)很簡(jiǎn)單,就是創(chuàng)建一個(gè)定期地檢查
ByteArrayOutputStream緩沖區(qū)的線程。緩沖區(qū)中找到的所有數(shù)據(jù)都被提取到一個(gè)byte數(shù)組,然后寫入到
PipedOutputStream。由于PipedOutputStream對(duì)應(yīng)的PipedInputStream由getInputStream
()返回,從該輸入流讀取數(shù)據(jù)的線程都將讀取到原先發(fā)送給ByteArrayOutputStream的數(shù)據(jù)。前面提到,LoopedStreams類解
決了管道流存在的前二個(gè)問題,我們來看看這是如何實(shí)現(xiàn)的。
ByteArrayOutputStream具有根據(jù)需要擴(kuò)展其內(nèi)部緩沖區(qū)的能力。由于存在“完全緩沖”,線程向getOutputStream()返回
的流寫入數(shù)據(jù)時(shí)不會(huì)被阻塞。因而,第一個(gè)問題不會(huì)再給我們帶來麻煩。另外還要順便說一句,ByteArrayOutputStream的緩沖區(qū)永遠(yuǎn)不會(huì)縮
減。例如,假設(shè)在能夠提取數(shù)據(jù)之前,有一塊500
K的數(shù)據(jù)被寫入到流,緩沖區(qū)將永遠(yuǎn)保持至少500
K的容量。如果這個(gè)類有一個(gè)方法能夠在數(shù)據(jù)被提取之后修正緩沖區(qū)的大小,它就會(huì)更完善。
第二個(gè)問題得以解決的原因在于,實(shí)際上任何時(shí)候只有一個(gè)線程向PipedOutputStream寫入數(shù)據(jù),這個(gè)線程就是由
startByteArrayReaderThread()創(chuàng)建的線程。由于這個(gè)線程完全由LoopedStreams類控制,我們不必?fù)?dān)心它會(huì)產(chǎn)生
IOException異常。
LoopedStreams類還有一些細(xì)節(jié)值得提及。首先,我們可以看到byteArrayOS和pipedIS實(shí)際上分別是
ByteArrayOutputStream和PipedInputStream的派生類的實(shí)例,也即在它們的close()方法中加入了特殊的行為。如
果一個(gè)LoopedStreams對(duì)象的用戶關(guān)閉了輸入或輸出流,在startByteArrayReaderThread()中創(chuàng)建的線程必須關(guān)閉。覆
蓋后的close()方法把keepRunning標(biāo)記設(shè)置成false以關(guān)閉線程。另外,請(qǐng)注意startByteArrayReaderThread
()中的同步塊。要確保在toByteArray()調(diào)用和reset()調(diào)用之間ByteArrayOutputStream緩沖區(qū)不被寫入流的線程修
改,這是必不可少的。由于ByteArrayOutputStream的write()方法的所有版本都在該流上同步,我們保證了
ByteArrayOutputStream的內(nèi)部緩沖區(qū)不被意外地修改。
注意LoopedStreams類并不涉及管道流的第三個(gè)問題。該類的getInputStream()方法返回PipedInputStream。如果
一個(gè)線程從該流讀取,一段時(shí)間后終止,下次數(shù)據(jù)從ByteArrayOutputStream緩沖區(qū)傳輸?shù)絇ipedOutputStream時(shí)就會(huì)出現(xiàn)
IOException異常。
二、捕獲Java控制臺(tái)輸出
Listing 5的ConsoleTextArea類擴(kuò)展Swing
JTextArea捕獲控制臺(tái)輸出。不要對(duì)這個(gè)類有這么多代碼感到驚訝,必須指出的是,ConsoleTextArea類有超過50%的代碼用來進(jìn)行測(cè)試。
【Listing 5:截獲Java控制臺(tái)輸出】 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讀取錯(cuò)誤:" + e); System.exit(1); } } }).start(); } // startConsoleReaderThread() // 該類剩余部分的功能是進(jìn)行測(cè)試 public static void main(String[] args) { JFrame f = new JFrame("ConsoleTextArea測(cè)試"); ConsoleTextArea consoleTextArea = null; try { consoleTextArea = new ConsoleTextArea(); } catch(IOException e) { System.err.println( "不能創(chuàng)建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); } }); // 啟動(dòng)幾個(gè)寫操作線程向 // 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()方法創(chuàng)建了一個(gè)JFrame,JFrame包含一個(gè)ConsoleTextArea的實(shí)例。這些代碼并沒有什么特別之處。Frame顯示出來
之后,main()方法啟動(dòng)一系列的寫操作線程,寫操作線程向控制臺(tái)流輸出大量信息。ConsoleTextArea捕獲并顯示這些信息,如圖一所示。
ConsoleTextArea提供了兩個(gè)構(gòu)造函數(shù)。沒有參數(shù)的構(gòu)造函數(shù)用來捕獲和顯示所有寫入到控制臺(tái)流的數(shù)據(jù),有一個(gè)InputStream[]參數(shù)
的構(gòu)造函數(shù)轉(zhuǎn)發(fā)所有從各個(gè)數(shù)組元素讀取的數(shù)據(jù)到JTextArea。稍后將有一個(gè)例子顯示這個(gè)構(gòu)造函數(shù)的用處。首先我們來看看沒有參數(shù)的
ConsoleTextArea構(gòu)造函數(shù)。這個(gè)函數(shù)首先創(chuàng)建一個(gè)LoopedStreams對(duì)象;然后請(qǐng)求Java運(yùn)行時(shí)環(huán)境把控制臺(tái)輸出轉(zhuǎn)發(fā)到
LoopedStreams提供的OutputStream;最后,構(gòu)造函數(shù)調(diào)用startConsoleReaderThread(),創(chuàng)建一個(gè)不斷地
把文本行追加到JTextArea的線程。注意,把文本追加到JTextArea之后,程序小心地保證了插入點(diǎn)的正確位置。
一
般來說,Swing部件的更新不應(yīng)該在AWT事件分派線程(AWT Event
Dispatch
Thread,AEDT)之外進(jìn)行。對(duì)于本例來說,這意味著所有把文本追加到JTextArea的操作應(yīng)該在AEDT中進(jìn)行,而不是在
startConsoleReaderThread()方法創(chuàng)建的線程中進(jìn)行。然而,事實(shí)上在Swing中向JTextArea追加文本是一個(gè)線程安全的
操作。讀取一行文本之后,我們只需調(diào)用JText.append()就可以把文本追加到JTextArea的末尾。
三、捕獲其他程序的控制臺(tái)輸出
在JTextArea中捕獲Java程序自己的控制臺(tái)輸出是一回事,去捕獲其他程序(甚至包括一些非Java程序)的控制臺(tái)數(shù)據(jù)又是另一回事。ConsoleTextArea提供了捕獲其他應(yīng)用的輸出時(shí)需要的基礎(chǔ)功能,Listing
6的AppOutputCapture利用ConsoleTextArea,截取其他應(yīng)用的輸出信息然后顯示在ConsoleTextArea中。
【Listing 6:截獲其他程序的控制臺(tái)輸出】 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 " + "<程序名字> {參數(shù)1 參數(shù)2 ...}"); System.exit(0); } try { // 啟動(dòng)命令行指定程序的新進(jìn)程 process = Runtime.getRuntime().exec(args); } catch(IOException e) { System.err.println("創(chuàng)建進(jìn)程時(shí)出錯(cuò)...\n" + e); System.exit(1); } // 獲得新進(jìn)程所寫入的流 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] + "控制臺(tái)輸出"); 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()方法啟動(dòng)指定程序的一個(gè)新進(jìn)程。啟動(dòng)新進(jìn)程之后,從結(jié)果
Process對(duì)象得到它的控制臺(tái)流。之后,把這些控制臺(tái)流傳入ConsoleTextArea(InputStream[])構(gòu)造函數(shù)(這就是帶參數(shù)
ConsoleTextArea構(gòu)造函數(shù)的用處)。使用AppOutputCapture時(shí),在命令行上指定待截取其輸出的程序名字。例如,如果在
Windows
2000下執(zhí)行javaw.exe AppOutputCapture ping.exe
www.yahoo.com,則結(jié)果如圖四所示。
圖四:截取其他程序的控制臺(tái)輸出
使用AppOutputCapture時(shí)應(yīng)該注意,被截取輸出的應(yīng)用程序最初輸出的一些文本可能無法截取。因?yàn)樵谡{(diào)用Runtime.exec()和
ConsoleTextArea初始化完成之間存在一小段時(shí)間差。在這個(gè)時(shí)間差內(nèi),應(yīng)用程序輸出的文本會(huì)丟失。當(dāng)AppOutputCapture窗口被
關(guān)閉,process.destory()調(diào)用試圖關(guān)閉Java程序開始時(shí)創(chuàng)建的進(jìn)程。測(cè)試結(jié)果顯示出,destroy()方法不一定總是有效(至少在
Windows
98上是這樣的)。似乎當(dāng)待關(guān)閉的進(jìn)程啟動(dòng)了額外的進(jìn)程時(shí),則那些進(jìn)程不會(huì)被關(guān)閉。此外,在這種情況下AppOutputCapture程序看起來未能正
常結(jié)束。但在Windows
NT下,一切正常。如果用JDK
v1.1.x運(yùn)行AppOutputCapture,關(guān)閉窗口時(shí)會(huì)出現(xiàn)一個(gè)NullPointerException。這是一個(gè)JDK的Bug,JDK
1.2.x和JDK 1.3.x下就不會(huì)出現(xiàn)問題。
原文地址: http://www.ibm.com/developerworks/cn/java/l-console/