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); } // 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 程序中的多線程 關於作者 俞良松,軟體工程師,獨立顧問和自由撰稿人。最初從事PB和Oracle開發,現主要興趣在於Internet開發。您可以通過 javaman@163.net 和我聯繫。
[火星人
]
java截獲標準輸出(2) 已經有431 次圍觀
本文地址: http://coctec.com/docs/linux/show-post-190089.html