java的管道流

來源:互聯網
上載者:User

 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異常:

  1. 試圖從PipedInputStream讀取資料,
  2. PipedInputStream的緩衝區為“空”(即不存在可讀取的資料),
  3. 最後一個向PipedOutputStream寫資料的線程不再活動(通過Thread.isAlive()檢測)。

這是一個很微妙的時刻,同時也是一個極其重要的時刻。假定有一個線程w向PipedOutputStream寫入資料;另一個線程r從對應的 PipedInputStream讀取資料。下面一系列的事件將導致r線程在試圖讀取PipedInputStream時遇到IOException異常:

  1. w向PipedOutputStream寫入資料。
  2. w結束(w.isAlive()返回false)。
  3. r從PipedInputStream讀取w寫入的資料,清空PipedInputStream的緩衝區。
  4. 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異常:

  1. 寫操作線程w已經建立,但r線程還不存在。
  2. w向PipedOutputStream寫入資料。
  3. 讀線程r被建立,並從PipedInputStream讀取資料。
  4. r線程結束。
  5. 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的末尾。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.