<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    Vincent

    Vicent's blog
    隨筆 - 74, 文章 - 0, 評論 - 5, 引用 - 0
    數據加載中……

    WebWork教程- Interceptor(攔截器)

         摘要: Interceptor (攔截器)將 Action 共用的行為獨立出來,在 Action 執行前后運行。這也就是我們所說的 AOP ( Aspect Oriented Programming ,面向切面編程),它是分散關注的編程方法,它將通用需求功能從不相關類之中分離出來;同時,能夠使得很多類共享一個行為,一...  閱讀全文

    posted @ 2006-09-01 13:39 Binary 閱讀(615) | 評論 (0)編輯 收藏

    WebWork介紹-Action篇

         摘要: Action 簡介 Action 在 MVC 模式中擔任控制部分的角色 , 在 WebWork 中使用的最多 , 用于接收頁面參數,起到對 HttpRequest 判斷處理作用。每個請求的動作都對應于一個相應的 ...  閱讀全文

    posted @ 2006-09-01 13:38 Binary 閱讀(229) | 評論 (0)編輯 收藏

    Log4J學習筆記

    一、簡介
      在程序中輸出信息的目的有三:一是監視程序運行情況;一是將程序的運行情況記錄到日志文件中,以備將來查看;一是做為調試器。但信息輸出的手段不僅限于System.out.println()或System.out.print(),還有日志記錄工具可以選擇。與System.out.pringln()和System.out.print()相比,日志記錄工具可以控制輸出級別,并且可以在配置文件中對輸出級別進行設置,這樣開發階段的信息在程序發布后就可以通過設置輸出級別來消除掉,而無須對代碼進行修正了?,F在流行的日志記錄工具很多, Log4J就是其中的佼佼者。
      Log4J是由著名開源組織Apache推出的一款日志記錄工具,供Java編碼人員做日志輸出之用,可以從網站http://logging.apache.org/log4j上免費獲得,最新版本1.2.11。獲得logging-log4j-1.2.11.zip文件后,解壓縮,需要的是其中的log4j-1.2.11.jar文件,將該文件放到特定的文件夾中備用,我放到了我機器的G:\YPJCCK\Log4J\lib文件夾中。
      這里選擇的IDE是Eclipse和JBuilder。Eclipse用的是3.0.1加語言包,可以到www.eclipse.org網站上下載;JBuilder用的是JBuilder 2005。
    二、配置類庫
      下面打開Eclipse或JBuilder。
      如果使用的是Eclipse,那么在Eclipse打開后,點擊菜單"文件"->"新建"->"項目",打開"新建項目"對話框:

    請選中"Java項目",點擊"下一步",進入"新建Java項目"對話框:

    在這個對話框中需要設置項目的名稱以及項目所在目錄,我為自己的項目起名為Log4JTest,目錄為G:\YPJCCK\Log4J\Eclipse\ Log4JTest。設置好后點擊"下一步",進入下一個窗口。在這個窗口中選擇名為"庫"的選項卡,然后點擊"添加外部JAR"按鈕,將保存于特定文件夾中的log4j-1.2.11.jar文件引用進來。

    設置好后,點擊"完成",至此,已經具備了在Eclipse下使用Log4J的環境。
      如果使用的是JBuilder,那么在JBuilder打開后,點擊菜單"Tools"->"Configure" ->"Libraries",打開"Configure Libraries"對話框:

    點擊"New"按鈕,打開"New Library Wizard"對話框:

    使用"Add"按鈕將保存于特定文件夾中的log4j-1.2.11.jar文件引用進來,并設置Name,即該類庫的名字,我將Name設置為 Log4J。設置好后點擊"OK"按鈕,回到"Configure Libraries"對話框,再點擊"OK"按鈕,則JUnit類庫已經被添加到JBuilder當中。
      下面繼續,在JBuilder中創建項目。點擊菜單"File"->"New Project",打開"Project Wizard"對話框:

    在這個窗口中設置項目名稱及存放目錄,我的項目名稱仍為Log4JTest,路徑為G:/YPJCCK/log4J/JBuilder/Log4JTest。點擊"Next"進入下一個窗口:

    在這個窗口中選擇"Required Libraries"選項卡,點擊"Add"按鈕,將剛才設置的JUnit庫引用進來。然后點擊"Next"按鈕,進入下一個窗口:

    在這個窗口中用鼠標點擊Encoding下拉列表框,然后按一下"G"鍵,選中相應選項,此時該項目的字符集就被設置成GBK了。如果做的是國內項目,這絕對是個好習慣。最后點擊"Finish",項目創建完成。
    三、編寫一個簡單的示例
      在了解Log4J的使用方法之前,先編寫一個簡單的示例,以對Log4J有個感性認識。
    如果使用的是Eclipse,請點擊"文件"->"新建"->"類",打開"新建Java類"對話框,設置包為 piv.zheng.log4j.test,名稱為Test,并確保"public static void main(String[] args)"選項選中;如果使用的是JBuilder,請點擊"File"->"New Class",打開"Class Wizard"對話框,設置Package為piv.zheng.log4j.test,Class name為Test,并確保"Generate main method"選項選中。設置完成后,點擊"OK"。代碼如下:
      package piv.zheng.log4j.test;
      
      import org.apache.log4j.Logger;
      import org.apache.log4j.Level;
      import org.apache.log4j.SimpleLayout;
      import org.apache.log4j.ConsoleAppender;
      
      public class Test {
        
        public static void main(String[] args) {
          SimpleLayout layout = new SimpleLayout();
          
          ConsoleAppender appender = new ConsoleAppender(layout);
          
          Logger log = Logger.getLogger(Test.class);
          log.addAppender(appender);
          log.setLevel(Level.FATAL);
          
          log.debug("Here is DEBUG");
          log.info("Here is INFO");
          log.warn("Here is WARN");
          log.error("Here is ERROR");
          log.fatal("Here is FATAL");
        }
      }
    至此,示例編寫完成。請點擊運行按鈕旁邊的倒三角,選擇"運行為"->"2 Java應用程序"(Eclipse),或者在Test類的選項卡上點擊鼠標右鍵,在調出的快捷菜單中點擊"Run using defaults"(JBuilder),運行程序,觀察從控制臺輸出的信息。
    四、Log4J入門
      看過程序的運行效果后可能會奇怪,為何控制臺只輸出了"FATAL - Here is FATAL"這樣一條信息,而程序代碼中的log.debug()、log.info()等方法也都設置了類似的內容,卻沒有被輸出?其實答案很簡單,但在公布之前,先來了解一下Log4J的使用。
      請先看前邊的示例代碼,會發現,示例中用到了Logger、Level、 ConsoleAppender、SimpleLayout等四個類。其中Logger類使用最多,甚至輸出的信息也是在其對象log的fatal方法中設置的,那么Logger究竟是做什么的呢?其實Logger就是傳說中的日志記錄器(在Log4J中稱為Category),創建方法有三:
      1.根Category,默認創建,獲取方法:

    Logger log = Logger.getRootLogger();

      2.用戶創建的Category,方法:

    Logger log = Logger.getLogger("test");

    其中字符串test是為Category設定的名稱。Category的名稱允許使用任何字符,但區分大小寫,例如:

    Logger l1 = Logger.getLogger("x");
    Logger l2 = Logger.getLogger("X");

    l1和l2就是兩個Category;而如果名稱完全相同,例如:

    Logger l1 = Logger.getLogger("x");
    Logger l2 = Logger.getLogger("x");

    l1和l2就是同一個Category。此外,符號"."在Category的名稱中有特殊作用,這一點將在后邊介紹。
      3.與方法2類似,只是參數由字符串換成了類對象,其目的是通過類對象獲取類的全名。這個方法比較常用,示例中使用的就是這個方法。
      那么Category是如何輸出信息的呢?其實示例中用到的debug、info、warn、error、fatal等五個方法都是用來輸出信息的。什么,怎么這么多?原因很簡單,Log4J支持分級輸出。Log4J的輸出級別有五個,由低到高依次是DEBUG(調試)、INFO(信息)、WARN(警告)、ERROR(錯誤)和FATAL(致命),分別與以上方法對應。當輸出級別設置為DEBUG時,以上方法都能夠輸出信息,當輸出級別設置為INFO 時,則只有debug方法將不能再輸出信息,依此類推,當輸出級別設置為FATAL時,就只有fatal方法可以輸出信息了?,F在再回頭看前邊的問題,為何只有設置給fatal方法的信息被輸出就不難理解了,示例中有這樣一行代碼:

    log.setLevel(Level.FATAL);

    正是這行代碼將log對象的輸出級別設成了FATAL。在為log對象設置輸出級別時用到了Level類,該類中定義了DEBUG、INFO、WARN、 ERROR、FATAL等五個靜態對象,與五個輸出級別相對應。此外,Level還有兩個特殊的靜態對象ALL和OFF,前者允許所有的方法輸出信息,其級別其實比DEBUG還低;后者則會禁止所有的方法輸出信息,其級別比FATAL要高。除前邊示例中用到的五個方法,Logger還提供了這五個方法的重載,以在輸出信息的同時拋出異常,以fatal方法為例:

    log.fatal("Here is FATAL", new Exception("Exception"));

    執行后輸出信息:
      FATAL - Here is FATAL
      java.lang.Exception: Exception
        at piv.zheng.log4j.test.Test.main(Test.java:24)
    其他方法類似。此外,Logger還提供了log方法,該方法不針對任何輸出級別,需要在調用時設置,例如:

    log.log(Level.FATAL, "Here is FATAL");
    log.log(Level.FATAL, "Here is FATAL", new Exception("Exception"));

    雖然一般情況下log方法不如其它方法方便,但由于允許設置級別,因此log方法在很多時候反而比其它方法更靈活,甚至可以在輸出級別為OFF時輸出信息。不過log方法主要是給用戶自定義的輸出級別用的,而且設立OFF輸出級別的目的也為了不輸出任何信息,因此請不要在log方法中使用OFF來輸出信息。
      此外,Category的輸出級別并非必須,若未設置,子Category會默認使用其父Category的輸出級別,若父Category也沒設置,就使用再上一級Category的設置,直到根Category為止。根Category默認輸出級別為DEBUG,因此在示例中,若將 "log.setLevel(Level.FATAL);"一行注釋掉,則所有方法都會輸出信息。
      下面簡單介紹一下Log4J中 Category的繼承關系。其實在Log4J中Category之間是存在繼承關系的,根Category默認創建,是級別最高的Category,用戶創建的Category均繼承自它。而用戶創建的Category之間也存在繼承關系,例如:

    Logger lx = Logger.getLogger("x");
    Logger lxy = Logger.getLogger("xy");
    Logger lx_y = Logger.getLogger("x.y");
    Logger lx_z = Logger.getLogger("x.z");
    Logger lx_y_z = Logger.getLogger("x.y.z");

    其中的lx_y、lx_z就是lx的子Category,而lx_y_z是lx_y的子Category。但lxy并不是lx的子Category。也許有點亂,下面來一個一個看。首先看與lx_y、lx_z對應的Category的名稱"x.y"和"x.z","."前邊的是什么,"x",這說明與名稱為 "x"的Category對應lx就是它們的父Category;而與lx_y_z對應的Category的名稱"x.y.z",最后一個"."前邊的是什么,"x.y",這說明lx_y是lx_y_z的父Category;至于lxy,由于與之對應的Category名稱"xy"之間沒有".",因此它是一個與lx同級的Category,其父Category就是根Category器。此外還有一種情況,例如有一個名稱為"a.b"的 Category,如果沒有名稱為"a"的Category,那么它的父Category也是根Category。前邊說過,"."在Category名稱中有特殊作用,其實它的作用就是繼承。至此,為何使用類對象來創建Category也就不難理解了。
      可是,僅有Category是無法完成信息輸出的,還需要為Category添加Appender,即Category的輸出源。前邊的例子使用的是ConsoleAppender,即指定 Category將信息輸出到控制臺。其實Log4J提供的Appender有很多,這里選擇幾常用的進行介紹。
      1.org.apache.log4j.WriterAppender,可以根據用戶選擇將信息輸出到Writer或OutputStream。
      示例代碼:
        SimpleLayout layout = new SimpleLayout ();
        
        //向文件中輸出信息,OutputStream示例
        WriterAppender appender1 = null;
        try {
          appender1 = new WriterAppender(layout, new FileOutputStream("test.txt"));
        }
        catch(Exception ex) {}
        
        //向控制臺輸出信息,Writer示例
        WriterAppender appender2 = null;
        try {
          appender2 = new WriterAppender(layout, new OutputStreamWriter(System.out));
        }
        catch(Exception ex) {}
        
        //Category支持同時向多個目標輸出信息
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender1);
        log.addAppender(appender2);
        
        log.debug("output");
    這個示例由第一個示例修改而來,沒有設置輸出級別,而且向Category中添加了兩個輸出源,運行后會在控制臺中輸出"DEBUG - output",并在工程目錄下生成test.txt文件,該文件中也記錄著"DEBUG - output"。若要將test.txt文件放到其它路徑下,例如f:,則將"test.txt"改為"f:/test.txt",又如e:下的temp 文件夾,就改為"e:/temp/test.txt"。后邊FileAppender、RollingFileAppender以及 DailyRollingFileAppender設置目標文件時也都可以這樣來寫。
      2.org.apache.log4j.ConsoleAppender,向控制臺輸出信息,繼承了WriterAppender,前邊的示例使用的就是它。
      3.org.apache.log4j.FileAppender,向文件輸出信息,也繼承了WriterAppender。
      示例代碼:
        SimpleLayout layout = new SimpleLayout();
        
        //若文件不存在則創建文件,若文件已存在則向文件中追加信息
        FileAppender appender = null;
        try {
          appender = new FileAppender(layout, "test.txt");
        } catch(Exception e) {}
        
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        log.debug("output");
    這個示例也由第一個示例修改而來,運行后會在工程目錄下生成test.txt文件,該文件中記錄著"DEBUG - output"。再次運行程序,查看文件,則"DEBUG - output"有兩行。
      另外,FileAppender還有一個構造:

    FileAppender(Layout layout, String filename, boolean append)

    與示例的類似,只是多了一個boolean型的參數append。append參數是個開關,用來設置當程序重啟,而目標文件已存在時,是向目標文件追加信息還是覆蓋原來的信息,當值為true時就追加,這是FileAppender默認的,當值為false時則覆蓋。此外,FileAppender還提供了setAppend方法來設置append開關。
      4.org.apache.log4j.RollingFileAppender,繼承了 FileAppender,也是向文件輸出信息,但文件大小可以限制。當文件大小超過限制時,該文件會被轉為備份文件或刪除,然后重新生成。文件的轉換或刪除與設置的備份文件最大數量有關,當數量大于0時就轉為備份文件,否則(小于等于0)刪除,默認的備份文件數量是1。轉換備份文件非常簡單,就是修改文件名,在原文件名之后加上".1",例如文件test.txt,轉為備份文件后文件名為"test.txt.1"。但若同名的備份文件已存在,則會先將該備份文件刪除或更名,這也與設置的備份文件最大數量有關,若達到最大數量就刪除,否則更名。若備份文件更名時也遇到同樣情況,則使用同樣的處理方法,依此類推,直到達到設置的備份文件最大數量。備份文件更名也很簡單,就是將擴展名加1,例如test.txt.1文件更名后變為test.txt.2, test.txt.2文件更名后變為test.txt.3。
      示例代碼:
        SimpleLayout layout = new SimpleLayout();
        
        //若文件不存在則創建文件,若文件已存在則向文件中追加內容
        RollingFileAppender appender = null;
        try {
          appender = new RollingFileAppender(layout, "test.txt");
        } catch(Exception e) {}
        //限制備份文件的數量,本例為2個
        appender.setMaxBackupIndex(2);
        //限制目標文件的大小,單位字節,本例為10字節
        appender.setMaximumFileSize(10);
        
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        
        log.debug("output0");
        log.debug("output1");
        log.debug("output2");
    程序運行后,會在工程目錄下生成test.txt、test.txt.1和test.txt.2三個文件,其中test.txt內容為空,而后兩個文件則分別記錄著"DEBUG - output2"和"DEBUG - output1",這是怎么回事?原來由于目標文件大小被限制為10字節,而三次使用log.debug方法輸出的信息都超過了10字節,這樣就導致了三次備份文件轉換,所以test.txt內容為空。而備份文件最大數量被設為2,因此第一次轉換的備份文件就被刪掉了,而后兩次的則保存下來。此外,由于 test.txt轉換備份文件時是先轉為test.txt.1,再轉為test.txt.2,因此最后test.txt.2的內容是"DEBUG - output1",而test.txt.1是"DEBUG - output2",這點千萬別弄混了。
      另外,RollingFileAppender還提供了兩個方法:
      (1)setMaxFileSize,功能與setMaximumFileSize一樣,但參數是字符串,有兩種情況:一是僅由數字組成,默認單位為字節,例如"100",即表示限制文件大小為100字節;一是由數字及存儲單位組成,例如"1KB"、"1MB"、"1GB",其中單位不區分大小寫,分別表示限制文件大小為1K、1M、1G。
      (2)rollOver,手動將目標文件轉換為備份文件,使用起來較靈活,適用于復雜情況。
      示例代碼:
        SimpleLayout layout = new SimpleLayout();
        
        RollingFileAppender appender = null;
        try {
          appender = new RollingFileAppender(layout, "test.txt");
        } catch(Exception e) {}
        appender.setMaxBackupIndex(2);

        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        
        log.debug("output0");
        appender.rollOver();
        log.debug("output1");
        appender.rollOver();
        log.debug("output2");
        appender.rollOver();
    這里沒限制目標文件大小,但程序運行后,效果與上例相同。
      5.org.apache.log4j.DailyRollingFileAppender,也繼承了FileAppender,并且也是向文件輸出信息,但會根據設定的時間頻率生成備份文件。
      時間頻率格式簡介:
      '.'yyyy-MM,按月生成,生成時間為每月最后一天午夜過后,例如test.txt在2005年7月31日午夜過后會被更名為test.txt.2005-07,然后重新生成。
      '.'yyyy-ww,按周生成,生成時間為每周六午夜過后,例如test.txt在2005年8月13日午夜過后會被更名為test.txt.2005-33,33表示當年第33周。
      '.'yyyy-MM-dd,按天生成,生成時間為每天午夜過后,例如2005年8月16日午夜過后,test.txt會被更名為test.txt.2005-08-16。
      '.'yyyy-MM-dd-a,也是按天生成,但每天會生成兩次,中午12:00過后一次,午夜過后一次,例如test.txt在2005年8月16 日12:00過后會被更名為test.txt.2005-8-16-上午,午夜過后會被更名為test.txt.2005-8-16-下午。
      '.'yyyy-MM-dd-HH,按小時生成,例如test.txt在2005年8月16日12:00過后會被更名為test.txt.2005-8-16-11。
      '.'yyyy-MM-dd-HH-mm,按分鐘生成,例如test.txt在2005年8月16日12:00過后會被更名為test.txt.2005-8-16-11-59。
      示例代碼:
        SimpleLayout layout = new SimpleLayout();
        
        DailyRollingFileAppender appender = null;
        try {
          appender = new DailyRollingFileAppender(layout, "test.txt", "'.'yyyy-MM-dd-HH-mm");
        } catch(Exception e) {}
        
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        log.debug("output");
    編碼完成后運行程序,等一分鐘后再次運行,由于我是在2005年8月17日15:42分第一次運行程序的,因此工程目錄下最終有兩個文件test.txt和test.txt.2005-08-17-15-42。
      6.org.apache.log4j.AsyncAppender,用于管理不同類型的Appender,也能實現同時向多個源輸出信息,但其執行是異步的。
      示例代碼:
        SimpleLayout layout = new SimpleLayout();
        
        //向控制臺輸出
        ConsoleAppender appender1 = null;
        try {
          appender1 = new ConsoleAppender(layout);
        } catch(Exception e) {}
        
        //向文件輸出
        FileAppender appender2 = null;
        try {
          appender2 = new FileAppender(layout, "test.txt");
        } catch(Exception e) {}
        
        //使用AsyncAppender實現同時向多個目標輸出信息
        AsyncAppender appender = new AsyncAppender();
        appender.addAppender(appender1);
        appender.addAppender(appender2);
        
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        log.debug("output");
    此外,AsyncAppender和Logger都提供了更多的方法來管理Appender,例如getAppender、 getAllAppenders、removeAppender和removeAllAppenders,分別用來獲取指定的Appender、獲取全部 Appender、移除指定的Appender以及移除全部Appender。
      7.org.apache.log4j.jdbc.JDBCAppender,將信息輸出到數據庫。
      示例代碼:
        JDBCAppender appender = new JDBCAppender();
        appender.setDriver("com.mysql.jdbc.Driver");
        appender.setURL("jdbc:mysql://localhost:3306/zheng");
        appender.setUser("root");
        appender.setPassword("11111111");
        appender.setSql("insert into log4j (msg) values ('%m')");
        
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        log.debug("output");
    這里使用的數據庫是MySQL 5.0.4beta,用戶名root,密碼11111111,我在其中建了一個庫zheng,包含表log4j,該表只有一個字段msg,類型為varchar(300)。此外,本例用到的JDBC驅動可以從http://dev.mysql.com/downloads/connector/j/3.1.html下載,版本3.1.8a,下載mysql-connector-java-3.1.8a.zip文件后解壓縮,需要其中的mysql-connector- java-3.1.8-bin.jar文件。下面再來看代碼。由于JDBCAppender內部默認使用PatternLayout格式化輸出信息,因此這里沒用到SimpleLayout,而appender.setSql所設置的SQL語句就是PatternLayout所需的格式化字符串,故此其中才有"%m"這樣的字符,有關PatternLayout的具體內容后邊介紹。執行后,表log4j增加一條記錄,內容為"output"。
      8.org.apache.log4j.nt.NTEventLogAppender,向Windows NT系統日志輸出信息。
      示例代碼:
        SimpleLayout layout = new SimpleLayout();
        
        NTEventLogAppender appender = new NTEventLogAppender("Java", layout);

        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        log.debug("output");
    注意,要完成此示例,還需向C:\WINNT\system32文件夾(我的操作系統裝在了C:\)中復制一個名為 NTEventLogAppender.dll的文件。如果跟我一樣用的是Log4J 1.2.11,實在對不住,Log4J 1.2.11并未提供該文件。雖然logging-log4j-1.2.11.zip文件解壓縮后,其下的src\java\org\apache\ log4j\nt文件夾中有一個make.bat文件執行后可以編譯出該文件,但還需要配置,很麻煩。還好,條條大道通羅馬,1.2.11不行,就換 1.2.9,可以從http://apache.justdn.org/logging/log4j/1.2.9下載,下載后解壓縮logging-log4j-1.2.9.zip文件,在其下的src\java\org\apache\log4j\nt文件夾中找到 NTEventLogAppender.dll,復制過去就可以了。程序執行后,打開"事件查看器",選擇"應用程序日志",其中有一條來源為Java的記錄,這條記錄就是剛才輸出的信息了。
      9.org.apache.log4j.lf5.LF5Appender,執行時會彈出一個窗口,信息在該窗口中以表格的形式顯示。
      示例代碼:
        LF5Appender appender = new LF5Appender();
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        log.debug("output");
    由于LF5Appender不需要Layout格式化輸出信息,因此這里沒有設置。此外LF5Appender還提供了一個setMaxNumberOfRecords方法,用來限制信息在表格中顯示的行數。
      10.org.apache.log4j.net.SocketAppender,以套接字方式向服務器發送日志,然后由服務器將信息輸出。
      示例代碼:
      //指定要連接的服務器地址及端口,這里使用的是本機9090端口
      SocketAppender appender = new SocketAppender("localhost", 9090);
      Logger log = Logger.getLogger(Test.class);
      log.addAppender(appender);
      log.debug("output");
    SocketAppender不需要設置Layout,因為SocketAppender不負責輸出信息。那么如何看到信息輸出的效果呢?這就需要SocketServer和SimpleSocketServer了。
      示例代碼1:
        package piv.zheng.log4j.test;
        
        import org.apache.log4j.net.SocketServer;
        
        public class TestServer {
          public static void main(String[] args) {
            SocketServer.main(new String[]{"9090", "test.properties", "G:/YPJCCK/Log4J"});
          }
        }
    這是SocketServer的示例。SocketServer只有一個靜態方法main,該方法意味著SocketServer不僅可以在代碼中被調用,也可以用java命令執行。main方法只有一個參數,是個字符串數組,但要求必須有三個元素:元素一用來指定端口,本例為9090;元素二用來指定輸出信息時需要的配置文件,該文件放在工程目錄下,本例使用的test.properties內容如下:
      log4j.rootLogger=, console
      log4j.appender.console =org.apache.log4j.ConsoleAppender
      log4j.appender.console.layout=org.apache.log4j.SimpleLayout
    該配置指定SocketServer使用ConsoleAppender以SimpleLayout格式輸出信息;元素三用來指定一個路徑,以存放.lcf 文件,我指定的是本機的G:/YPJCCK/Log4J文件夾。.lcf文件也是輸出信息時使用的配置文件,格式與元素二所指定的配置文件一樣,但 test.properties是默認配置文件,即當.lcf文件找不到時才使用。那么.lcf文件如何命名呢?其實.lcf文件的名稱并不是隨意起的,當SocketAppender與SocketServer建立連接時,SocketServer就會獲得SocketAppender所在計算機的IP 地址與網絡ID,并將其格式化成"網絡ID/IP地址"這樣的字符串,然后獲取其中的網絡ID作為.lcf文件的主名,例如 "zhengyp/127.0.0.1",其中的"zhengyp"就是主文件名,而后再根據這個文件名來調用相應的.lcf文件。這意味著對不同的計算機可以提供不同的配置文件,使信息輸出時有不同的效果。此外,SocketServer還默認了一個名為generic.lcf的文件,用于處理網絡ID 獲取不到或其他情況,本例是用的就是這個文件,內容如下:
      log4j.rootLogger=, console
      log4j.appender.console =org.apache.log4j.ConsoleAppender
      log4j.appender.console.layout=org.apache.log4j.PatternLayout
      log4j.appender.console.layout.ConversionPattern=%m%n
    該配置指定SocketServer使用ConsoleAppender以PatternLayout格式輸出信息。運行程序時請先運行 SocketServer,再運行SocketAppender。SocketAppender運行結束后,就可以從SocketServer的控制臺看到輸出的信息了。
      示例代碼2:
        package piv.zheng.log4j.test;
        
        import org.apache.log4j.net.SimpleSocketServer;

        public class TestServer {
          public static void main(String[] args) {
            SimpleSocketServer.main(new String[]{"9090", "test.properties"});
          }
        }
    這是SimpleSocketServer的示例,與SocketServer相比,只允許指定一個默認的配置文件,而無法對不同計算機使用不同的配置文件。
      11.org.apache.log4j.net.SocketHubAppender,也是以套接字方式發送日志,但與SocketAppender相反,SocketHubAppender是服務器端,而不是客戶端。
      示例代碼:
        //指定服務器端口,這里使用的是本機9090端口
        SocketHubAppender appender = new SocketHubAppender(9090);
        
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        while (true) {
          Thread.sleep(1000);
          log.debug("output"); //輸出信息
        }
    由于SocketHubAppender一旦運行就開始發送消息,而無論有無接收者,因此這里使用了while語句并將條件設為true以保證程序持續運行。不過為了保證性能,這里還使用了Thread.sleep(1000),這樣程序每循環一次都休眠1秒,如果機器性能不好,還可以將值設的再大些。此外,由于SocketHubAppender也不負責輸出信息,因此同樣不需要設置Layout。那么如何看到信息輸出的效果呢?這里我自己寫了個客戶端程序,代碼如下:
      package piv.zheng.log4j.test;
      
      import java.net.Socket;
      import java.lang.Thread;
      import org.apache.log4j.LogManager;
      import org.apache.log4j.PropertyConfigurator;
      import org.apache.log4j.net.SocketNode;
      
      public class TestClient {
        public static void main(String[] args) throws Exception {
          //創建客戶端套接字對象
          Socket s = new Socket("localhost", 9090);
          //調用配置文件
          PropertyConfigurator.configure("test.properties");
          //從套接字中恢復Logger,并輸出信息
          new Thread(new SocketNode(s, LogManager.getLoggerRepository())).start();
        }
      }
    由于SocketHubAppender與SocketAppender一樣,發送的也是SocketNode對象,因此編寫該程序時參考了 SocketServer的源碼。此外,這里的配置文件直接使用了上例的test.properties文件。運行程序時請先運行 SocketHubAppender,再運行客戶端程序,然后從客戶端的控制臺就可以看到效果了。
      13.org.apache.log4j.net.TelnetAppender,與SocketHubAppender有些類似,也是作為服務器發送信息,但TelnetAppender發送的不是SocketNode對象,而是Category輸出的結果。
      示例代碼:
        SimpleLayout layout = new SimpleLayout();
        
        TelnetAppender appender = new TelnetAppender();
        appender.setLayout(layout); //設置Layout
        appender.setPort(9090); //設置端口號
        appender.activateOptions(); //應用設置
        
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        
        while (true) {
          java.lang.Thread.sleep(1000);
          log.debug("output"); //輸出信息
        }
        //appender.close();
    注意最后一行被注釋掉的代碼,若該行代碼執行,則TelnetAppender的資源會被清理,從而導致TelnetAppender無法繼續運行。那么如何看到信息輸出的效果呢?這里提供兩種方法:方法一,使用Telnet工具,我使用的就是Windows自帶的Telnet。運行 TelnetAppender程序后,點擊[開始]菜單->[運行],在"運行"框中輸入"telnet",回車,telnet客戶端彈出,這是一個命令行程序,輸入命令"open localhost 9090",回車,然后就可以看到效果了。方法二,自己寫程序,代碼如下:
      package piv.zheng.log4j.test;
      
      import java.net.*;
      import java.io.*;
      
      public class TestClient {
        public static void main(String[] args) throws Exception {
          //創建客戶端套接字對象
          Socket s = new Socket("localhost", 9090);
          //將BufferedReader與Socket綁定,以輸出Socket獲得的信息
          BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
          //獲得信息并輸出
          String line = in.readLine();
          while (line != null) {
            System.out.println(line);
            line = in.readLine();
          }
        }
      }
      13.org.apache.log4j.net.SMTPAppender,向指定的電子郵件發送信息,但只能發送ERROR和FATAL級別的信息,而且還沒提供身份驗證功能。
      示例代碼:
      SimpleLayout loyout = new SimpleLayout();
      
      SMTPAppender appender = new SMTPAppender();
      appender.setLayout(loyout); //設置Layout
      appender.setFrom("zhengyp@126.com"); //設置發件人
      appender.setSMTPHost("smtp.126.com"); //設置發送郵件服務器
      appender.setTo("zhengyp@126.com"); //設置收件人
      appender.setSubject("Log4J Test"); //設置郵件標題
      appender.activateOptions(); //應用設置
      
      Logger log = Logger.getLogger(Test.class);
      log.addAppender(appender);
      log.debug("Here is DEBUG");
      log.info("Here is INFO");
      log.warn("Here is WARN");
      log.error("Here is ERROR");
      log.fatal("Here is FATAL");
    要運行此示例,還需要JavaMail 和JAF,前者是Sun推出的電子郵件類庫,可以從http://java.sun.com/products/javamail/downloads/index.html下載,最新版本1.3.3,下載javamail-1_3_3-ea.zip壓縮包后需要其中的mail.jar文件;后者全稱是JavaBeans Activation Framework,提供了對輸入任意數據塊的支持,并能相應地對其進行處理,可以從http://www.sun.com/download中找到,最新版本1.1,下載jaf-1_1-ea.zip壓縮包后需要其中的activation.jar文件。不過,程序運行后會拋出兩次異常,分別是log.error和log.fatal方法導致的,失敗的原因很簡單,我用的郵件服務器需要身份驗證。
      14.piv.zheng.log4j.test.SMTPAppender,自定義的,依照Log4J提供的SMTPAppender修改而來,增加了身份驗證功能,并去掉了對級別的限制。由于代碼太長,所以放到了另一篇文章《自定義SMTPAppender的源碼》中,有興趣的請自行去查看。
      示例代碼:
        SimpleLayout layout = new SimpleLayout();
        
        SMTPAppender appender = new SMTPAppender(layout);
        appender.setFrom("zhengyp@126.com"); //發件人
        appender.setSMTPHost("smtp.126.com"); //發送郵件服務器
        appender.setTo("zhengyp@126.com"); //收件人
        appender.setSubject("Log4J Test"); //郵件標題
        appender.setAuth("true"); //身份驗證標識
        appender.setUsername("zhengyp"); //用戶名
        appender.setPassword("1111111"); //密碼
        appender.activateOptions(); //應用設置
        
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        log.debug("output");
    同樣需要JavaMail 和JAF。程序運行后會發送一封郵件,快去查看一下自己的郵箱吧^_^
      此外,Log4J還提供了SyslogAppender、JMSAppender(均在org.apache.log4j.net包下)以及更多的 Appender,或者用來向Unix操作系統的syslogd服務發送信息,或者通過JMS方式發送信息,或者以其他方式發送信息。由于條件有現,就不再介紹了。
      不過,在前邊的示例中還使用了SimpleLayout和PatternLayout來格式化輸出的信息,這里也簡單介紹一下。
      1.org.apache.log4j.SimpleLayout,一直用的就是它,輸出的格式比較簡單,就是"級別 - 信息"。
      2.org.apache.log4j.HTMLLayout,以HTML格式輸出信息。
      示例代碼:
        HTMLLayout layout = new HTMLLayout();
        layout.setTitle("Log4J Test"); //HTML頁標題
        
        FileAppender appender = null;
        try {
          appender = new FileAppender(layout, "test.html");
        } catch(Exception e) {}
        
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        log.debug("output");
    程序運行后會在工程目錄下生成一個HTML頁,可以用瀏覽器來查看。
      3.org.apache.log4j.xml.XMLLayout,以XML格式輸出信息。
      示例代碼:
        XMLLayout layout = new XMLLayout();
        
        FileAppender appender = null;
        try {
          appender = new FileAppender(layout, "test.xml");
        } catch(Exception e) {}
        
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        log.debug("output");
    程序運行后會在工程目錄下生成一個test.xml文件。
      4.org.apache.log4j.TTCCLayout,輸出信息的同時輸出日志產生時間、相關線程及Category等信息。
      示例代碼:
        TTCCLayout layout = new TTCCLayout();
        //是否打印與TTCCLayout關聯的Category的名稱,默認為true,表示打印
        layout.setCategoryPrefixing(true);
        //是否打印當前線程,默認為true,表示打印
        layout.setThreadPrinting(true);
        //是否打印輸出和當前線程相關的NDC信息,默認為true,表示打印
        layout.setContextPrinting(true);
        //設置日期時間格式
        layout.setDateFormat("iso8601");
        //設置時區
        layout.setTimeZone("GMT+8:00");
        //設置時區后需要調用此方法應用設置
        layout.activateOptions();
        
        ConsoleAppender appender = new ConsoleAppender(layout);
        
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        log.debug("output");
    注意,TTCCLayout輸出的時間格式及時區是可以設置的:
      (1)setDateFormat,設置日期時間格式,有五個常用值:"NULL",表示不輸出;"RELATIVE",輸出信息所用的時間,以毫秒為單位,默認使用該值;"ABSOLUTE",僅輸出時間部分;"DATE",按當前所在地區顯示日期和時間;"ISO8601",按ISO8601標準顯示日期和時間。這些字符串不區分大小寫。此外,還可以使用時間模式字符來格式化日期時間,詳細內容請參考J2SE文檔中的 java.text.SimpleDateFormat類。
     ?。?)setTimeZone,設置時區,詳細內容請參考J2SE文檔中的java.util.TimeZone類和java.util.SimpleTimeZone類。但請注意,當日期格式為"RELATIVE"時,設置時區會造成沖突。
      5.org.apache.log4j.PatternLayout,用模式字符靈活指定信息輸出的格式。
      示例代碼:
        String pattern = "Logger: %c %n"
            + "Date: %d{DATE} %n"
            + "Message: %m %n";
        PatternLayout layout = new PatternLayout(pattern);
        
        ConsoleAppender appender = new ConsoleAppender(layout);
        
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        log.debug("output");
    模式字符串簡介:
      %c:Category名稱。還可以使用%c{n}的格式輸出Category的部分名稱,其中n為正整數,輸出時會從Category名稱的右側起查 n個".",然后截取第n個"."右側的部分輸出,例如Category的名稱為"x.y.z",指定格式為"%c{2}",則輸出"y.z"。
      %C:輸出信息時Category所在類的名稱,也可以使用%C{n}的格式輸出。
      %d:輸出信息的時間,也可以用%d{FormatString}的格式輸出,其中FormatString的值請參考TTCCLayout的setDateFormat方法,但NULL和RELATIVE在%d中無法使用。
      %F:輸出信息時Category所在類文件的名稱。
      %l:輸出信息時Category所在的位置,使用"%C.%M(%F:%L)"可以產生同樣的效果。
      %L:輸出信息時Category在類文件中的行號。
      %m:信息本身。
      %M:輸出信息時Category所在的方法。
      %n:換行符,可以理解成回車。
      %p:日志級別。
      %r:輸出信息所用的時間,以毫秒為單位。
      %t:當前線程。
      %x:輸出和當前線程相關的NDC信息。
      %X:輸出與當前現成相關的MDC信息。
      %%:輸出%。
    此外,還可以在%與模式字符之間加上修飾符來設置輸出時的最小寬度、最大寬度及文本對齊方式,例如:
      %30d{DATE}:按當前所在地區顯示日期和時間,并指定最小寬度為30,當輸出信息少于30個字符時會補以空格并右對齊。
      %-30d{DATE}:也是按當前所在地區顯示日期和時間,指定最小寬度為30,并在字符少于30時補以空格,但由于使用了"-",因此對齊方式為左對齊,與默認情況一樣。
      %.40d{DATE}:也是按當前所在地區顯示日期和時間,但指定最大寬度為40,當輸出信息多于40個字符時會將左邊多出的字符截掉。此外,最大寬度只支持默認的左對齊方式,而不支持右對齊。
      %30.40d{DATE}:如果輸出信息少于30個字符就補空格并右對齊,如果多于40個字符,就將左邊多出的字符截掉。
      %-30.40d{DATE}:如果輸出信息少于30個字符就補空格并左對齊,如果多于40個字符,就將左邊多出的字符截掉。
    五、Log4J進階
      了解以上內容后,就已經初步掌握Log4J了,但要想靈活使用Log4J,則還需要了解其配置功能。這里簡單介紹一下。
      1.org.apache.log4j.BasicConfigurator,默認使用ConsoleAppender以PatternLayout (使用PatternLayout.TTCC_CONVERSION_PATTERN,即"%r [%t] %p %c %x - %m%n"格式)輸出信息。
      示例代碼:
        BasicConfigurator.configure();
        Logger log = Logger.getLogger(Test.class);
        log.debug("output");
    注意,BasicConfigurator及其它Configurator其實都只對根Category進行配置,但由于用戶創建的Category會繼承根Category的特性(聲明,許多資料介紹Category繼承關系時都主要在討論輸出級別,而事實上,Category間繼承的不僅是輸出級別,所有特性都可以繼承),因此輸出時仍會顯示BasicConfigurator配置的效果。此外,還可以使用configure方法指定Appender,以自定義輸出。BasicConfigurator允許同時指定多個Appender。
      示例代碼:
        SimpleLayout layout1 = new SimpleLayout();
        ConsoleAppender appender1 = new ConsoleAppender(layout1);
        BasicConfigurator.configure(appender1);
        
        String pattern = "Logger: %c %n"
            + "Date: %d{DATE} %n"
            + "Message: %m %n";
        PatternLayout layout2 = new PatternLayout(pattern);
        FileAppender appender2 = null;
        try {
          appender2 = new FileAppender(layout2, "test.log", false);
        }
        catch(Exception e){}
        BasicConfigurator.configure(appender2);
        
        Logger log = Logger.getLogger(Test.class);
        log.debug("output");
    這里用BasicConfigurator指定了兩個Appender,即ConsoleAppender和FileAppender,程序運行后信息會在以SimpleLayout輸出到控制臺的同時以PatternLayout輸出到test.log文件。若要清除這些Appender,可以調用 BasicConfigurator的resetConfiguration方法。
      2. org.apache.log4j.PropertyConfigurator,調用文本配置文件輸出信息,通常使用.properties文件。配置文件以"鍵=值"的形式保存數據,注釋以"#"開頭。PropertyConfigurator和配置文件在介紹SocketAppender和 SocketHubAppender時曾提到過。使用PropertyConfigurator可以避免硬編碼。
      示例代碼:
        PropertyConfigurator.configure("test.properties");
        Logger log = Logger.getLogger(Test.class);
        log.debug("output");
    要完成該示例,還需要在工程目錄下創建一個test.properties文件,內容如下:
      ##設置根Category,其值由輸出級別和指定的Appender兩部分組成
      #這里設置輸出級別為DEBUG
      log4j.rootLogger=DEBUG,appender
      ##輸出信息到控制臺
      #創建一個名為appender的Appender,類型為ConsoleAppender
      log4j.appender.appender=org.apache.log4j.ConsoleAppender
      #設置appender以SimpleLayout輸出
      log4j.appender.appender.layout=org.apache.log4j.SimpleLayout
    此外,PropertyConfigurator也允許同時指定多個Appender,例如:
      #這里沒有設置輸出級別,但指定了兩個Appender
      log4j.rootLogger=,appender1,appender2
      #輸出信息到控制臺
      log4j.appender.appender1=org.apache.log4j.ConsoleAppender
      log4j.appender.appender1.layout=org.apache.log4j.SimpleLayout
      #輸出信息到文件
      log4j.appender.appender2=org.apache.log4j.FileAppender
      log4j.appender.appender2.File=test.log
      log4j.appender.appender2.Append=false
      log4j.appender.appender2.layout=org.apache.log4j.PatternLayout
      log4j.appender.appender2.layout.ConversionPattern=Logger: %c %nDate: %d{DATE} %nMessage: %m %n
    關于更多配置,網上示例很多,這里不再贅述。但要說明一件事,就是配置文件中的鍵是怎么來的。參照后一個示例,查看 PropertyConfigurator源碼,會發現"log4j.rootLogger"是定義好的,只能照寫;而"log4j.appender" 字樣也可以找到,與指定的Appender名稱appender1、appender2聯系起來,log4j.appender.appender1和 log4j.appender.appender2也就不難理解了;再看下去,還能找到"prefix + ".layout"",這樣log4j.appender.appender1.layout也有了;可是 log4j.appender.appender2.File 和log4j.appender.appender2.Append呢?還記得前邊介紹FileAppender時曾提到的setAppend方法嗎?其實FileAppender還有個getAppend方法,這說明FileAppender具有Append屬性。那么File呢?當然也是 FileAppender的屬性了。至于log4j.appender.appender2.layout.ConversionPattern也一樣,只不過FileAppender換成了PatternLayout。其實別的Appender和Layout的屬性也都是這樣定義成鍵來進行設置的。此外,定義鍵時,屬性的首字母不區分大小寫,例如"File",也可以寫成"file"。
      3. org.apache.log4j.xml.DOMConfigurator,調用XML配置文件輸出信息。其定義文檔是log4j- 1.2.11.jar中org\apache\log4j\xml包下的log4j.dtd文件。與PropertyConfigurator相比, DOMConfigurator似乎是趨勢。
      示例代碼:
        DOMConfigurator.configure("test.xml");
        Logger log = Logger.getLogger(Test.class);
        log.debug("output");
    要完成該示例,也需要在工程目錄下創建一個test.xml文件,內容如下:
      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
      <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
        <!-- 輸出信息到控制臺
        創建一個名為appender的Appender,類型為ConsoleAppender -->
        <appender name="appender" class="org.apache.log4j.ConsoleAppender">
          <!-- 設置appender以SimpleLayout輸出 -->
          <layout class="org.apache.log4j.SimpleLayout"/>
        </appender>
        <!-- 設置根Category,其值由輸出級別和指定的Appender兩部分組成
        這里設置輸出級別為DEBUG -->
        <root>
          <priority value ="debug" />
          <appender-ref ref="appender"/>
        </root>
      </log4j:configuration>
    此外,DOMConfigurator也允許同時指定多個Appender,例如:
      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
      <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
        <!-- 輸出信息到控制臺 -->
        <appender name="appender1" class="org.apache.log4j.ConsoleAppender">
          <layout class="org.apache.log4j.SimpleLayout"/>
        </appender>
        <!-- 輸出信息到文件 -->
        <appender name="appender2" class="org.apache.log4j.FileAppender">
          <param name="File" value="test.log"/>
          <param name="Append" value="false"/>
          <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="Logger: %c %nDate: %d{DATE} %nMessage: %m %n"/>
          </layout>
        </appender>
        <!-- 這里沒有設置輸出級別,但指定了兩個Appender -->
        <root>
          <appender-ref ref="appender1"/>
          <appender-ref ref="appender2"/>
        </root>
      </log4j:configuration>
    由于以上兩個示例是在PropertyConfigurator的兩個示例基礎上改的,而且也寫了注釋,因此這里只簡單介紹一下<param> 標記。<param>標記有兩個屬性,name和value,前者的值也是Appender或Layout的屬性名,作用與 log4j.appender.appender2.File這樣的鍵一樣。設置時,首字母同樣不區分大小寫,例如"File"也可以寫成"file"。此外還請注意,使用這兩段XML代碼時應將中文注釋去掉,或者把<?xml version="1.0" encoding="UTF-8" ?>中的UTF-8改成GBK或GB2312,否則會導致錯誤。這里使用的UTF-8是XML默認的字符集。
      4. org.apache.log4j.lf5.DefaultLF5Configurator,默認使用LF5Appender來輸出信息,需要調用 log4j-1.2.11.jar中org\apache\log4j\lf5\config包下的defaultconfig.properties文件。
      示例代碼:
        try {
          DefaultLF5Configurator.configure();
        }
        catch(Exception e){}
        Logger log = Logger.getLogger(Test.class);
        log.debug("output");
      下面討論另外一個話題:Diagnostic Context。Diagnostic Context意為診斷環境,針對于多用戶并發環境,在這種環境下,通常需要對每個客戶端提供獨立的線程以處理其請求,此時若要在日志信息中對客戶端加以區分,為每個線程分別創建Category是個辦法。但這樣做并不高效,反而會導致大量資源被占用。Diagnostic Context所要解決的就是這個問題。Diagnostic Context會為當前線程提供一定空間,然后將信息保存到該空間供Category調用。與創建一個Category相比,這點信息所占的資源自然要少得多。
      1.org.apache.log4j.NDC。NDC是Nested Diagnostic Context的簡寫,意為嵌套診斷環境,使用時提供一個堆棧對象來保存信息。堆棧的特點是數據后進先出、先進后出,即清理堆棧時,后保存的數據會被先清掉,而先保存的數據則被后清掉。
      示例代碼:
        PatternLayout layout = new PatternLayout("%m %x%n");
        ConsoleAppender appender = new ConsoleAppender(layout);
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        
        String tmp = "zhengyp"; //模擬從客戶端獲取的信息
        log.debug("Start");
        NDC.push(tmp); //添加信息到堆棧中
        log.debug("Before");
        NDC.pop(); //將信息從堆棧中移除
        log.debug("After");
        NDC.remove(); //將當前線程移除,退出NDC環境
        log.debug("End");
    這里使用了PatternLayout來格式化信息,其模式字符%x就是用來輸出NDC信息的。程序運行后會輸出如下內容:
      Start
      Before zhengyp
      After
      End
    可以看到,第二行輸出時由于已向堆棧中添加了信息,因此"zhengyp"也會同時輸出;而第三行輸出時由于信息已被移除,因此就沒再輸出"zhengyp"。不過這個示例僅簡單演示了NDC的用法,而沒有顯示出NDC的堆棧特性,所以下面再提供一個示例,代碼如下:
      TTCCLayout layout = new TTCCLayout();
      ConsoleAppender appender = new ConsoleAppender(layout);
      Logger log = Logger.getLogger(Test.class);
      log.addAppender(appender);
      
      log.debug("Start");
      NDC.push("zhengyp"); //添加信息到堆棧中
      log.debug("Test1");
      NDC.push("192.168.0.1"); //向堆棧中追加信息
      log.debug("Test2");
      NDC.pop(); //從堆棧中移除信息,但移除的只是最后的信息
      log.debug("Test3");
      NDC.pop(); //再次從堆棧中移除信息
      log.debug("Test4");???
      log.debug("End");
    這里格式化輸出信息使用的是TTCCLayout,還記得其setContextPrinting方法嗎?程序運行后,從輸出的信息就可以看到效果了。此外,NDC還提供了其他方法:
     ?。?)get,獲取堆棧中的全部信息。以上例為例,當輸出Test2時,使用該方法會獲得"zhengyp 192.168.0.1"。
     ?。?)peek,獲取堆棧中最后的信息。仍以上例為例,當輸出Test1時會獲得"zhengyp",Test2時為"192.168.0.1",而當輸出Test3時由于"192.168.0.1"已被移除,"zhengyp"又成了最后的信息,因此獲得的仍是"zhengyp"。
     ?。?)clear,清空堆棧中的全部信息。
     ?。?)setMaxDepth,設置堆棧的最大深度,即當前的信息可以保留多少,對之后追加的信息沒有影響。當需要一次清掉多條信息時,使用setMaxDepth會比多次調用pop方便。
      2.org.apache.log4j.MDC。MDC是Mapped Diagnostic Context的簡寫,意為映射診斷環境,提供了一個Map對象來保存信息。Map對象使用Key、Value的形式保存值。
      示例代碼:
        PatternLayout layout = new PatternLayout("%m %X{name} %X{ip}%n");
        ConsoleAppender appender = new ConsoleAppender(layout);
        Logger log = Logger.getLogger(Test.class);
        log.addAppender(appender);
        
        log.debug("Start");
        //添加信息到Map中
        MDC.put("name", "zhengyp1");
        MDC.put("ip", "192.168.1.1");
        log.debug("Test1");
        
        //添加信息到Map中,若Key重復,則覆蓋之前的值
        MDC.put("name", "zhengyp2");
        MDC.put("ip", "192.168.1.2");
        log.debug("Test2");
        
        //將信息從Map中移除,此時信息不再輸出
        MDC.remove("name");
        MDC.remove("ip");
        log.debug("End");
    這個示例演示了MDC的基本用法,格式化信息用的也是PatternLayout,模式字符為"%X",其格式必須為"%X{Key}"。其中Key就是向 Map對象添加信息時put方法所用的Key,這里為name和ip。由于可以使用"%X{Key}"輸出信息,因此MDC使用起來會比NDC更靈活。此外,MDC還提供了get方法來獲取指定Key的信息。
    六、小結
      用了近半個月,終于大概掌握了Log4J。由于本文是邊學邊寫的,目的是將Log4J的用法記錄下來,而非提供一份中文參考,因此內容并不細致,但盡量提供了示例。不過到最后才發現,示例存在問題,其實Logger做為類的static成員比較恰當,而我為了圖方便,竟直接寫到了main方法中,這一點還請注意。
      此外,這里再推薦一下《The Complete log4j Manual》,是對Log4J較詳細的介紹,在網上可以找到,只不過是英文的。


    posted @ 2006-09-01 13:25 Binary 閱讀(435) | 評論 (0)編輯 收藏

    Java 技術: 使您輕松地進行多線程應用程序編程

    產者-消費者方案是多線程應用程序開發中最常用的構造之一 ― 因此困難也在于此。因為在一個應用程序中可以多次重復生產者-消費者行為,其代碼也可以如此。軟件開發人員 Ze'ev Bubis 和 Saffi Hartal 創建了 Consumer 類,該類通過在一些多線程應用程序中促進代碼重用以及簡化代碼調試和維護來解決這個問題。請通過單擊本文頂部或底部的 討論來參與本文的 論壇,與作者和其他讀者分享您的想法。

    多線程應用程序通常利用生產者-消費者編程方案,其中由生產者線程創建重復性作業,將其傳遞給作業隊列,然后由消費者線程處理作業。雖然這種編程方法很有用,但是它通常導致重復的代碼,這對于調試和維護可能是真正的問題。

    為了解決這個問題并促進代碼重用,我們創建了 Consumer 類。 Consumer 類包含所有用于作業隊列和消費者線程的代碼,以及使這兩者能夠結合在一起的邏輯。這使我們可以專注于業務邏輯 ― 關于應該如何處理作業的細節 ― 而不是專注于編寫大量冗余的代碼。同時,它還使得調試多線程應用程序的任務變得更為容易。

    在本文中,我們將簡單觀察一下多線程應用程序開發中公共線程用法,同時,解釋一下生產者-消費者編程方案,并研究一個實際的示例來向您演示 Consumer 類是如何工作的。請注意,對于多線程應用程序開發或消費者-生產者方案,本文不作深入介紹;有關那些主題,請參閱 參考資料獲取文章的清單。

    多線程基礎知識

    多線程是一種使應用程序能同時處理多個操作的編程技術。通常有兩種不同類型的多線程操作使用多個線程:

    • 適時事件,當作業必須在特定的時間或在特定的間隔內調度執行時
    • 后臺處理,當后臺事件必須與當前執行流并行處理或執行時

    適時事件的示例包括程序提醒、超時事件以及諸如輪詢和刷新之類的重復性操作。后臺處理的示例包括等待發送的包或等待處理的已接收的消息。





    回頁首


    生產者-消費者關系

    生產者-消費者方案很適合于后臺處理類別的情況。這些情況通常圍繞一個作業“生產者”方和一個作業“消費者”方。當然,關于作業并行執行還有其它考慮事項。在大多數情況下,對于使用同一資源的作業,應以“先來先服務”的方式按順序處理,這可以通過使用單線程的消費者輕松實現。通過使用這種方法,我們使用單個線程來訪問單個資源,而不是用多個線程來訪問單個資源。

    要啟用標準消費者,當作業到來時創建一個作業隊列來存儲所有作業。生產者線程通過將新對象添加到消費者隊列來交付這個要處理的新對象。然后消費者線程從隊列取出每個對象,并依次處理。當隊列為空時,消費者進入休眠。當新的對象添加到空隊列時,消費者會醒來并處理該對象。因為大多數應用程序喜歡順序處理方式,所以消費者通常是單線程的。





    回頁首


    問題:代碼重復

    因為生產者-消費者方案很常用,所以在構建應用程序時它可能會出現幾次,這導致了代碼重復。我們認識到,這顯示了在應用程序開發過程期間多次使用了生產者-消費者方案的問題。

    當第一次需要生產者-消費者行為時,通過編寫一個采用一個線程和一個隊列的類來實現該行為。當第二次需要這種行為時,我們著手從頭開始實現它,但是接著認識到以前已經做過這件事了。我們復制了代碼并修改了處理對象的方式。當第三次在該應用程序中實現生產者-消費者行為時,很明顯我們復制了太多代碼。我們決定,需要一個適用的 Consumer 類,它將處理我們所有的生產者-消費者方案。





    回頁首


    我們的解決方案:Consumer 類

    我們創建 Consumer 類的目的是:在我們的應用程序中,消除這種代碼重復 ― 為每個生產者-消費者實例編寫一個新作業隊列和消費者線程來解決這個問題。有了適當的 Consumer 類,我們所必須做的只是編寫專門用于作業處理(業務邏輯)的代碼。這使得我們的代碼更清晰、更易于維護以及更改起來更靈活。

    我們對 Consumer 類有如下需求:

    • 重用:我們希望這個類包括所有東西。一個線程、一個隊列以及使這兩者結合在一起的所有邏輯。這將使我們只須編寫隊列中“消費”特定作業的代碼。(因而,例如,程序員使用 Consumer 類時,將重載 onConsume(ObjectjobToBeConsumed) 方法。)
    • 隊列選項:我們希望能夠設置將由 Consumer 對象使用的隊列實現。但是,這意味著我們必須確保隊列是線程安全的或使用一個不會與消費操作沖突的單線程生產者。無論使用哪種方法,都必須將隊列設計成允許不同的進程能訪問其方法。
    • Consumer 線程優先級:我們希望能夠設置 Consumer 線程運行的優先級。
    • Consumer 線程命名:線程擁有一個有意義的名稱會比較方便,當然這的確有助于調試。例如,如果您向 Java 虛擬機發送了一個信號,它將生成一個完整的線程轉儲 ― 所有線程及其相應堆棧跟蹤的快照。要在 Windows 平臺上生成這個線程轉儲,您必須在 Java 程序運行的窗口中按下鍵序列 <ctrl><break> ,或者單擊窗口上的“關閉”按鈕。有關如何使用完整的線程轉儲來診斷 Java 軟件問題的更多信息,請參閱 參考資料。




    回頁首


    類代碼

    getThread() 方法中,我們使用“惰性創建”來創建 Consumer 的線程,如清單 1 所示:


    清單 1. 創建 Consumer 的線程
         /**
           * Lazy creation of the Consumer's thread.
           *
           * @return  the Consumer's thread
           */
          private Thread getThread()
          {
             if (_thread==null)
             {
                _thread = new Thread()
                {
                   public void run()
                   {
                      Consumer.this.run();
                   }
                };
             }
             return _thread;
    

    該線程的 run() 方法運行 Consumerrun() 方法,它是主消費者循環,如清單 2 所示:


    清單 2. run() 方法是主 Consumer 循環
         /**
           *  Main Consumer's thread method.
           */
          private void run()
          {
             while (!_isTerminated)
             {
                // job handling loop
          while (true)
                {
                   Object o;
                   synchronized (_queue)
                   {
                      if (_queue.isEmpty())
              break;
                      o = _queue.remove();
                   }
                   if (o == null)
              break;
                   onConsume(o);
                }
    
                // if we are not terminated and the queue is still empty
                // then wait until new jobs arrive.
    
                synchronized(_waitForJobsMonitor)
                {
                   if (_isTerminated)
              break;
                   if(_queue.isEmpty())
                   {
            try
                      {
                         _waitForJobsMonitor.wait();
                      }
                      catch (InterruptedException ex)
                      {
                      }
                   }
                }
             }
    }// run()
    

    基本上, Consumer 的線程一直運行,直到隊列中不再有等待的作業為止。然后它進入休眠,只在第一次調用 add(Object) 時醒來,該方法向隊列添加一個新作業并“踢”醒該線程。

    使用 wait()notify() 機制來完成“睡眠”和“踢”。實際的消費者工作由 OnConsume(Object) 方法處理,如清單 3 所示:


    清單 3. 喚醒和通知 Consumer
         /**
          * Add an object to the Consumer.
          * This is the entry point for the producer.
          * After the item is added, the Consumer's thread
          * will be notified.
          *
          * @param  the object to be 'consumed' by this consumer
          */
          public void add(Object o)
          {
             _queue.add(o);
             kickThread();
          }
    
          /**
           * Wake up the thread (without adding new stuff to consume)
           *
           */
          public void kickThread()
          {
             if (!this._thread.isInterrupted())
             {
                synchronized(_waitForJobsMonitor)
                {
                   _waitForJobsMonitor.notify();
                }
             }
          }
    





    回頁首


    示例:MessagesProcessor

    為了向您展示 Consumer 類是如何工作的,我們將使用一個簡單示例。 MessagesProcessor 類以異步方式處理進入的消息(也就是說,不干擾調用線程)。其工作是在每個消息到來時打印它。 MessagesProcessor 具有一個處理到來的消息作業的內部 Consumer 。當新作業進入空隊列時, Consumer 調用 processMessage(String) 方法來處理它,如清單 4 所示:


    清單 4. MessagesProcessor 類
          class MessagesProcessor
          {
             String _name;
             // anonymous inner class that supplies the consumer
             // capabilities for the MessagesProcessor
             private Consumer _consumer = new Consumer()
             {
                // that method is called on each event retrieved
                protected void onConsume(Object o)
                {
                   if (!(o instanceof String))
                   {
                      System.out.println("illegal use, ignoring");
                      return;
                   }
                   MessagesProcesser.this.processMessage((String)o);
                }
             }.setName("MessagesProcessor").init();
    
             public void gotMessageEvent(String s)
             {
                _consumer.add(s);
             }
             private void processMessage(String s)
             {
                System.out.println(_name+" processed message: "+s);
             }
    
             private void terminate()
             {
               _consumer.terminateWait();
               _name = null;
             }
    
             MessagesProcessor()
             {
                _name = "Example Consumer";
             }
          }
    

    正如您可以從上面的代碼中所看到的,定制 Consumer 相當簡單。我們使用了一個匿名內部類來繼承 Consumer 類,并重載抽象方法 onConsume() 。因此,在我們的示例中,只需調用 processMessage 。





    回頁首


    Consumer 類的高級特性

    除了開始時提出的基本需求以外,我們還為 Consumer 類提供了一些我們覺得有用的高級特性。

    事件通知

    • onThreadTerminate():只在終止 Consumer 前調用該方法。我們出于調試目的覆蓋了這個方法。
    • goingToRest():只在 Consumer 線程進入休眠前調用該方法(也就是說,只在調用 _waitForJobsMonitor.wait() 之前調用)。只在需要消費者在進入休眠之前處理一批已處理工作的復雜情況中,可能需要這種通知。

    終止

    • terminate():Consumer 線程的異步終止。
    • terminateWait():設置調用線程一直等待,直到消費者線程實際終止為止。

    在我們的示例中,如果使用 terminate() 而不是 terminateWait() ,那么將會出現問題,因為在將 _name 設置成空值之后調用 onConsume() 方法。這將導致執行 processMessage 的線程拋出一個 NullPointerException 。





    回頁首


    結束語:Consumer 類的好處

    可在 參考資料一節下載 Consumer 類的源代碼。請自由使用源代碼,并按照您的需要擴展它。我們發現將這個類用于多線程應用程序開發有許多好處:

    • 代碼重用/重復代碼的消除:如果您有 Consumer 類,就不必為您應用程序中的每個實例編寫一個新的消費者。如果在應用程序開發中頻繁使用生產者-消費者方案,這可以很大程度地節省時間。另外,請牢記重復代碼是滋生錯誤的沃土。它還使基本代碼的維護更為困難。
    • 更少錯誤:使用驗證過的代碼是一種防止錯誤的好實踐,尤其是處理多線程應用程序時。因為 Consumer 類已經被調試過,所以它更安全。消費者還通過在線程和資源之間擔任安全中介來防止與線程相關的錯誤。消費者可以代表其它線程以順序的方式訪問資源。
    • 漂亮、清晰的代碼:使用 Consumer 類有助于我們編寫出更簡單的代碼,這樣的代碼更容易理解和維護。如果我們不使用 Consumer 類,就必須編寫代碼來處理兩種不同的功能:消費邏輯(隊列和線程管理、同步等)和指定消費者的用法或功能的代碼。

    posted @ 2006-08-24 17:53 Binary 閱讀(250) | 評論 (0)編輯 收藏

    Java 理論和實踐: 理解 JTS ― 幕后魔術

    雖然用 Java? 語言編寫的程序在理論上是不會出現“內存泄漏”的,但是有時對象在不再作為程序的邏輯狀態的一部分之后仍然不被垃圾收集。本月,負責保障應用程序健康的工程師 Brian Goetz 探討了無意識的對象保留的常見原因,并展示了如何用弱引用堵住泄漏。

    要讓垃圾收集(GC)回收程序不再使用的對象,對象的邏輯 生命周期(應用程序使用它的時間)和對該對象擁有的引用的實際 生命周期必須是相同的。在大多數時候,好的軟件工程技術保證這是自動實現的,不用我們對對象生命周期問題花費過多心思。但是偶爾我們會創建一個引用,它在內存中包含對象的時間比我們預期的要長得多,這種情況稱為無意識的對象保留(unintentional object retention)。

    全局 Map 造成的內存泄漏

    無意識對象保留最常見的原因是使用 Map 將元數據與臨時對象(transient object)相關聯。假定一個對象具有中等生命周期,比分配它的那個方法調用的生命周期長,但是比應用程序的生命周期短,如客戶機的套接字連接。需要將一些元數據與這個套接字關聯,如生成連接的用戶的標識。在創建 Socket 時是不知道這些信息的,并且不能將數據添加到 Socket 對象上,因為不能控制 Socket 類或者它的子類。這時,典型的方法就是在一個全局 Map 中存儲這些信息,如清單 1 中的 SocketManager 類所示:


    清單 1. 使用一個全局 Map 將元數據關聯到一個對象
    												
    														public class SocketManager {
        private Map<Socket,User> m = new HashMap<Socket,User>();
        
        public void setUser(Socket s, User u) {
            m.put(s, u);
        }
        public User getUser(Socket s) {
            return m.get(s);
        }
        public void removeUser(Socket s) {
            m.remove(s);
        }
    }
    
    SocketManager socketManager;
    ...
    socketManager.setUser(socket, user);
    
    												
    										

    這種方法的問題是元數據的生命周期需要與套接字的生命周期掛鉤,但是除非準確地知道什么時候程序不再需要這個套接字,并記住從 Map 中刪除相應的映射,否則,SocketUser 對象將會永遠留在 Map 中,遠遠超過響應了請求和關閉套接字的時間。這會阻止 SocketUser 對象被垃圾收集,即使應用程序不會再使用它們。這些對象留下來不受控制,很容易造成程序在長時間運行后內存爆滿。除了最簡單的情況,在幾乎所有情況下找出什么時候 Socket 不再被程序使用是一件很煩人和容易出錯的任務,需要人工對內存進行管理。





    回頁首


    找出內存泄漏

    程序有內存泄漏的第一個跡象通常是它拋出一個 OutOfMemoryError,或者因為頻繁的垃圾收集而表現出糟糕的性能。幸運的是,垃圾收集可以提供能夠用來診斷內存泄漏的大量信息。如果以 -verbose:gc 或者 -Xloggc 選項調用 JVM,那么每次 GC 運行時在控制臺上或者日志文件中會打印出一個診斷信息,包括它所花費的時間、當前堆使用情況以及恢復了多少內存。記錄 GC 使用情況并不具有干擾性,因此如果需要分析內存問題或者調優垃圾收集器,在生產環境中默認啟用 GC 日志是值得的。

    有工具可以利用 GC 日志輸出并以圖形方式將它顯示出來,JTune 就是這樣的一種工具(請參閱 參考資料)。觀察 GC 之后堆大小的圖,可以看到程序內存使用的趨勢。對于大多數程序來說,可以將內存使用分為兩部分:baseline 使用和 current load 使用。對于服務器應用程序,baseline 使用就是應用程序在沒有任何負荷、但是已經準備好接受請求時的內存使用,current load 使用是在處理請求過程中使用的、但是在請求處理完成后會釋放的內存。只要負荷大體上是恒定的,應用程序通常會很快達到一個穩定的內存使用水平。如果在應用程序已經完成了其初始化并且負荷沒有增加的情況下,內存使用持續增加,那么程序就可能在處理前面的請求時保留了生成的對象。

    清單 2 展示了一個有內存泄漏的程序。MapLeaker 在線程池中處理任務,并在一個 Map 中記錄每一項任務的狀態。不幸的是,在任務完成后它不會刪除那一項,因此狀態項和任務對象(以及它們的內部狀態)會不斷地積累。


    清單 2. 具有基于 Map 的內存泄漏的程序
    												
    														public class MapLeaker {
        public ExecutorService exec = Executors.newFixedThreadPool(5);
        public Map<Task, TaskStatus> taskStatus 
            = Collections.synchronizedMap(new HashMap<Task, TaskStatus>());
        private Random random = new Random();
    
        private enum TaskStatus { NOT_STARTED, STARTED, FINISHED };
    
        private class Task implements Runnable {
            private int[] numbers = new int[random.nextInt(200)];
    
            public void run() {
                int[] temp = new int[random.nextInt(10000)];
                taskStatus.put(this, TaskStatus.STARTED);
                doSomeWork();
                taskStatus.put(this, TaskStatus.FINISHED);
            }
        }
    
        public Task newTask() {
            Task t = new Task();
            taskStatus.put(t, TaskStatus.NOT_STARTED);
            exec.execute(t);
            return t;
        }
    }
    
    												
    										

    圖 1 顯示 MapLeaker GC 之后應用程序堆大小隨著時間的變化圖。上升趨勢是存在內存泄漏的警示信號。(在真實的應用程序中,坡度不會這么大,但是在收集了足夠長時間的 GC 數據后,上升趨勢通常會表現得很明顯。)


    圖 1. 持續上升的內存使用趨勢

    確信有了內存泄漏后,下一步就是找出哪種對象造成了這個問題。所有內存分析器都可以生成按照對象類進行分解的堆快照。有一些很好的商業堆分析工具,但是找出內存泄漏不一定要花錢買這些工具 —— 內置的 hprof 工具也可完成這項工作。要使用 hprof 并讓它跟蹤內存使用,需要以 -Xrunhprof:heap=sites 選項調用 JVM。

    清單 3 顯示分解了應用程序內存使用的 hprof 輸出的相關部分。(hprof 工具在應用程序退出時,或者用 kill -3 或在 Windows 中按 Ctrl+Break 時生成使用分解。)注意兩次快照相比,Map.EntryTaskint[] 對象有了顯著增加。

    請參閱 清單 3

    清單 4 展示了 hprof 輸出的另一部分,給出了 Map.Entry 對象的分配點的調用堆棧信息。這個輸出告訴我們哪些調用鏈生成了 Map.Entry 對象,并帶有一些程序分析,找出內存泄漏來源一般來說是相當容易的。


    清單 4. HPROF 輸出,顯示 Map.Entry 對象的分配點
    												
    														TRACE 300446:
    	java.util.HashMap$Entry.<init>(<Unknown Source>:Unknown line)
    	java.util.HashMap.addEntry(<Unknown Source>:Unknown line)
    	java.util.HashMap.put(<Unknown Source>:Unknown line)
    	java.util.Collections$SynchronizedMap.put(<Unknown Source>:Unknown line)
    	com.quiotix.dummy.MapLeaker.newTask(MapLeaker.java:48)
    	com.quiotix.dummy.MapLeaker.main(MapLeaker.java:64)
    
    												
    										





    回頁首


    弱引用來救援了

    SocketManager 的問題是 Socket-User 映射的生命周期應當與 Socket 的生命周期相匹配,但是語言沒有提供任何容易的方法實施這項規則。這使得程序不得不使用人工內存管理的老技術。幸運的是,從 JDK 1.2 開始,垃圾收集器提供了一種聲明這種對象生命周期依賴性的方法,這樣垃圾收集器就可以幫助我們防止這種內存泄漏 —— 利用弱引用。

    弱引用是對一個對象(稱為 referent)的引用的持有者。使用弱引用后,可以維持對 referent 的引用,而不會阻止它被垃圾收集。當垃圾收集器跟蹤堆的時候,如果對一個對象的引用只有弱引用,那么這個 referent 就會成為垃圾收集的候選對象,就像沒有任何剩余的引用一樣,而且所有剩余的弱引用都被清除。(只有弱引用的對象稱為弱可及(weakly reachable)。)

    WeakReference 的 referent 是在構造時設置的,在沒有被清除之前,可以用 get() 獲取它的值。如果弱引用被清除了(不管是 referent 已經被垃圾收集了,還是有人調用了 WeakReference.clear()),get() 會返回 null。相應地,在使用其結果之前,應當總是檢查 get() 是否返回一個非 null 值,因為 referent 最終總是會被垃圾收集的。

    用一個普通的(強)引用拷貝一個對象引用時,限制 referent 的生命周期至少與被拷貝的引用的生命周期一樣長。如果不小心,那么它可能就與程序的生命周期一樣 —— 如果將一個對象放入一個全局集合中的話。另一方面,在創建對一個對象的弱引用時,完全沒有擴展 referent 的生命周期,只是在對象仍然存活的時候,保持另一種到達它的方法。

    弱引用對于構造弱集合最有用,如那些在應用程序的其余部分使用對象期間存儲關于這些對象的元數據的集合 —— 這就是 SocketManager 類所要做的工作。因為這是弱引用最常見的用法,WeakHashMap 也被添加到 JDK 1.2 的類庫中,它對鍵(而不是對值)使用弱引用。如果在一個普通 HashMap 中用一個對象作為鍵,那么這個對象在映射從 Map 中刪除之前不能被回收,WeakHashMap 使您可以用一個對象作為 Map 鍵,同時不會阻止這個對象被垃圾收集。清單 5 給出了 WeakHashMapget() 方法的一種可能實現,它展示了弱引用的使用:


    清單 5. WeakReference.get() 的一種可能實現
    												
    														public class WeakHashMap<K,V> implements Map<K,V> {
    
        private static class Entry<K,V> extends WeakReference<K> 
          implements Map.Entry<K,V> {
            private V value;
            private final int hash;
            private Entry<K,V> next;
            ...
        }
    
        public V get(Object key) {
            int hash = getHash(key);
            Entry<K,V> e = getChain(hash);
            while (e != null) {
                K eKey= e.get();
                if (e.hash == hash && (key == eKey || key.equals(eKey)))
                    return e.value;
                e = e.next;
            }
            return null;
        }
    
    												
    										

    調用 WeakReference.get() 時,它返回一個對 referent 的強引用(如果它仍然存活的話),因此不需要擔心映射在 while 循環體中消失,因為強引用會防止它被垃圾收集。WeakHashMap 的實現展示了弱引用的一種常見用法 —— 一些內部對象擴展 WeakReference。其原因在下面一節討論引用隊列時會得到解釋。

    在向 WeakHashMap 中添加映射時,請記住映射可能會在以后“脫離”,因為鍵被垃圾收集了。在這種情況下,get() 返回 null,這使得測試 get() 的返回值是否為 null 變得比平時更重要了。

    用 WeakHashMap 堵住泄漏

    SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如清單 6 所示。(如果 SocketManager 需要線程安全,那么可以用 Collections.synchronizedMap() 包裝 WeakHashMap)。當映射的生命周期必須與鍵的生命周期聯系在一起時,可以使用這種方法。不過,應當小心不濫用這種技術,大多數時候還是應當使用普通的 HashMap 作為 Map 的實現。


    清單 6. 用 WeakHashMap 修復 SocketManager
    												
    														public class SocketManager {
        private Map<Socket,User> m = new WeakHashMap<Socket,User>();
        
        public void setUser(Socket s, User u) {
            m.put(s, u);
        }
        public User getUser(Socket s) {
            return m.get(s);
        }
    }
    
    												
    										

    引用隊列

    WeakHashMap 用弱引用承載映射鍵,這使得應用程序不再使用鍵對象時它們可以被垃圾收集,get() 實現可以根據 WeakReference.get() 是否返回 null 來區分死的映射和活的映射。但是這只是防止 Map 的內存消耗在應用程序的生命周期中不斷增加所需要做的工作的一半,還需要做一些工作以便在鍵對象被收集后從 Map 中刪除死項。否則,Map 會充滿對應于死鍵的項。雖然這對于應用程序是不可見的,但是它仍然會造成應用程序耗盡內存,因為即使鍵被收集了,Map.Entry 和值對象也不會被收集。

    可以通過周期性地掃描 Map,對每一個弱引用調用 get(),并在 get() 返回 null 時刪除那個映射而消除死映射。但是如果 Map 有許多活的項,那么這種方法的效率很低。如果有一種方法可以在弱引用的 referent 被垃圾收集時發出通知就好了,這就是引用隊列 的作用。

    引用隊列是垃圾收集器向應用程序返回關于對象生命周期的信息的主要方法。弱引用有兩個構造函數:一個只取 referent 作為參數,另一個還取引用隊列作為參數。如果用關聯的引用隊列創建弱引用,在 referent 成為 GC 候選對象時,這個引用對象(不是 referent)就在引用清除后加入 到引用隊列中。之后,應用程序從引用隊列提取引用并了解到它的 referent 已被收集,因此可以進行相應的清理活動,如去掉已不在弱集合中的對象的項。(引用隊列提供了與 BlockingQueue 同樣的出列模式 —— polled、timed blocking 和 untimed blocking。)

    WeakHashMap 有一個名為 expungeStaleEntries() 的私有方法,大多數 Map 操作中會調用它,它去掉引用隊列中所有失效的引用,并刪除關聯的映射。清單 7 展示了 expungeStaleEntries() 的一種可能實現。用于存儲鍵-值映射的 Entry 類型擴展了 WeakReference,因此當 expungeStaleEntries() 要求下一個失效的弱引用時,它得到一個 Entry。用引用隊列代替定期掃描內容的方法來清理 Map 更有效,因為清理過程不會觸及活的項,只有在有實際加入隊列的引用時它才工作。


    清單 7. WeakHashMap.expungeStaleEntries() 的可能實現
    												
    														    private void expungeStaleEntries() {
    	Entry<K,V> e;
            while ( (e = (Entry<K,V>) queue.poll()) != null) {
                int hash = e.hash;
    
                Entry<K,V> prev = getChain(hash);
                Entry<K,V> cur = prev;
                while (cur != null) {
                    Entry<K,V> next = cur.next;
                    if (cur == e) {
                        if (prev == e)
                            setChain(hash, next);
                        else
                            prev.next = next;
                        break;
                    }
                    prev = cur;
                    cur = next;
                }
            }
        }
    
    												
    										





    回頁首


    結束語

    弱引用和弱集合是對堆進行管理的強大工具,使得應用程序可以使用更復雜的可及性方案,而不只是由普通(強)引用所提供的“要么全部要么沒有”可及性。下個月,我們將分析與弱引用有關的軟引用,將分析在使用弱引用和軟引用時,垃圾收集器的行為。

    posted @ 2006-08-24 17:51 Binary 閱讀(214) | 評論 (0)編輯 收藏

    Java 理論與實踐: 您的小數點到哪里去了?

    許多程序員在其整個開發生涯中都不曾使用定點或浮點數,可能的例外是,偶爾在計時測試或基準測試程序中會用到。Java語言和類庫支持兩類非整數類型 ― IEEE 754 浮點( floatdouble ,包裝類(wrapper class)為 FloatDouble ),以及任意精度的小數( java.math.BigDecimal )。在本月的 Java 理論和實踐中,Brian Goetz 探討了在 Java 程序中使用非整數類型時一些常碰到的陷阱和“gotcha”。請在本文的 論壇上提出您對本文的想法,以饗筆者和其他讀者。(您也可以單擊本文頂部或底部的討論來訪問論壇)。

    雖然幾乎每種處理器和編程語言都支持浮點運算,但大多數程序員很少注意它。這容易理解 ― 我們中大多數很少需要使用非整數類型。除了科學計算和偶爾的計時測試或基準測試程序,其它情況下幾乎都用不著它。同樣,大多數開發人員也容易忽略 java.math.BigDecimal 所提供的任意精度的小數 ― 大多數應用程序不使用它們。然而,在以整數為主的程序中有時確實會出人意料地需要表示非整型數據。例如,JDBC 使用 BigDecimal 作為 SQL DECIMAL 列的首選互換格式。

    IEEE 浮點

    Java 語言支持兩種基本的浮點類型: floatdouble ,以及與它們對應的包裝類 FloatDouble 。它們都依據 IEEE 754 標準,該標準為 32 位浮點和 64 位雙精度浮點二進制小數定義了二進制標準。

    IEEE 754 用科學記數法以底數為 2 的小數來表示浮點數。IEEE 浮點數用 1 位表示數字的符號,用 8 位來表示指數,用 23 位來表示尾數,即小數部分。作為有符號整數的指數可以有正負之分。小數部分用二進制(底數 2)小數來表示,這意味著最高位對應著值 ?(2 -1),第二位對應著 ?(2 -2),依此類推。對于雙精度浮點數,用 11 位表示指數,52 位表示尾數。IEEE 浮點值的格式如圖 1 所示。


    圖 1. IEEE 754 浮點數的格式
    圖 1. IEEE 754 浮點數的格式

    因為用科學記數法可以有多種方式來表示給定數字,所以要規范化浮點數,以便用底數為 2 并且小數點左邊為 1 的小數來表示,按照需要調節指數就可以得到所需的數字。所以,例如,數 1.25 可以表示為尾數為 1.01,指數為 0: (-1) 0*1.01 2*2 0

    數 10.0 可以表示為尾數為 1.01,指數為 3: (-1) 0*1.01 2*2 3

    特殊數字

    除了編碼所允許的值的標準范圍(對于 float ,從 1.4e-45 到 3.4028235e+38),還有一些表示無窮大、負無窮大、 -0 和 NaN(它代表“不是一個數字”)的特殊值。這些值的存在是為了在出現錯誤條件(譬如算術溢出,給負數開平方根,除以 0 等)下,可以用浮點值集合中的數字來表示所產生的結果。

    這些特殊的數字有一些不尋常的特征。例如, 0-0 是不同值,但在比較它們是否相等時,被認為是相等的。用一個非零數去除以無窮大的數,結果等于 0 。特殊數字 NaN 是無序的;使用 == 、 <> 運算符將 NaN 與其它浮點值比較時,結果為 false 。如果 f 為 NaN,則即使 (f == f) 也會得到 false 。如果想將浮點值與 NaN 進行比較,則使用 Float.isNaN() 方法。表 1 顯示了無窮大和 NaN 的一些屬性。

    表 1. 特殊浮點值的屬性

    表達式 結果
    Math.sqrt(-1.0) -> NaN
    0.0 / 0.0 -> NaN
    1.0 / 0.0 -> 無窮大
    -1.0 / 0.0 -> 負無窮大
    NaN + 1.0 -> NaN
    無窮大 + 1.0 -> 無窮大
    無窮大 + 無窮大 -> 無窮大
    NaN > 1.0 -> false
    NaN == 1.0 -> false
    NaN < 1.0 -> false
    NaN == NaN -> false
    0.0 == -0.01 -> true

    基本浮點類型和包裝類浮點有不同的比較行為

    使事情更糟的是,在基本 float 類型和包裝類 Float 之間,用于比較 NaN 和 -0 的規則是不同的。對于 float 值,比較兩個 NaN 值是否相等將會得到 false ,而使用 Float.equals() 來比較兩個 NaN Float 對象會得到 true 。造成這種現象的原因是,如果不這樣的話,就不可能將 NaN Float 對象用作 HashMap 中的鍵。類似的,雖然 0-0 在表示為浮點值時,被認為是相等的,但使用 Float.compareTo() 來比較作為 Float 對象的 0-0 時,會顯示 -0 小于 0





    回頁首


    浮點中的危險

    由于無窮大、NaN 和 0 的特殊行為,當應用浮點數時,可能看似無害的轉換和優化實際上是不正確的。例如,雖然好象 0.0-f 很明顯等于 -f ,但當 f0 時,這是不正確的。還有其它類似的 gotcha,表 2 顯示了其中一些 gotcha。

    表 2. 無效的浮點假定

    這個表達式…… 不一定等于…… 當……
    0.0 - f -f f 為 0
    f < g ! (f >= g) f 或 g 為 NaN
    f == f true f 為 NaN
    f + g - g f g 為無窮大或 NaN

    舍入誤差

    浮點運算很少是精確的。雖然一些數字(譬如 0.5 )可以精確地表示為二進制(底數 2)小數(因為 0.5 等于 2 -1),但其它一些數字(譬如 0.1 )就不能精確的表示。因此,浮點運算可能導致舍入誤差,產生的結果接近 ― 但不等于 ― 您可能希望的結果。例如,下面這個簡單的計算將得到 2.600000000000001 ,而不是 2.6


    												
    														 double s=0;
      for (int i=0; i<26; i++)
        s += 0.1;
      System.out.println(s);
    
    												
    										


    類似的, .1*26 相乘所產生的結果不等于 .1 自身加 26 次所得到的結果。當將浮點數強制轉換成整數時,產生的舍入誤差甚至更嚴重,因為強制轉換成整數類型會舍棄非整數部分,甚至對于那些“看上去似乎”應該得到整數值的計算,也存在此類問題。例如,下面這些語句:


    												
    														 double d = 29.0 * 0.01;
      System.out.println(d);
      System.out.println((int) (d * 100));
    
    												
    										


    將得到以下輸出:


    												
    														 0.29
      28
    
    												
    										

    這可能不是您起初所期望的。





    回頁首


    浮點數比較指南

    由于存在 NaN 的不尋常比較行為和在幾乎所有浮點計算中都不可避免地會出現舍入誤差,解釋浮點值的比較運算符的結果比較麻煩。

    最好完全避免使用浮點數比較。當然,這并不總是可能的,但您應該意識到要限制浮點數比較。如果必須比較浮點數來看它們是否相等,則應該將它們差的絕對值同一些預先選定的小正數進行比較,這樣您所做的就是測試它們是否“足夠接近”。(如果不知道基本的計算范圍,可以使用測試“abs(a/b - 1) < epsilon”,這種方法比簡單地比較兩者之差要更準確)。甚至測試看一個值是比零大還是比零小也存在危險 ―“以為”會生成比零略大值的計算事實上可能由于積累的舍入誤差會生成略微比零小的數字。

    NaN 的無序性質使得在比較浮點數時更容易發生錯誤。當比較浮點數時,圍繞無窮大和 NaN 問題,一種避免 gotcha 的經驗法則是顯式地測試值的有效性,而不是試圖排除無效值。在清單 1 中,有兩個可能的用于特性的 setter 的實現,該特性只能接受非負數值。第一個實現會接受 NaN,第二個不會。第二種形式比較好,因為它顯式地檢測了您認為有效的值的范圍。


    清單 1. 需要非負浮點值的較好辦法和較差辦法
    												
    														   // Trying to test by exclusion -- this doesn't catch NaN or infinity
        public void setFoo(float foo) {
          if (foo < 0)
              throw new IllegalArgumentException(Float.toString(f));
            this.foo = foo;
        }
        // Testing by inclusion -- this does catch NaN
        public void setFoo(float foo) {
          if (foo >= 0 && foo < Float.INFINITY)
            this.foo = foo;
      else
            throw new IllegalArgumentException(Float.toString(f));
        }
    
    												
    										

    不要用浮點值表示精確值

    一些非整數值(如幾美元和幾美分這樣的小數)需要很精確。浮點數不是精確值,所以使用它們會導致舍入誤差。因此,使用浮點數來試圖表示象貨幣量這樣的精確數量不是一個好的想法。使用浮點數來進行美元和美分計算會得到災難性的后果。浮點數最好用來表示象測量值這類數值,這類值從一開始就不怎么精確。





    回頁首


    用于較小數的 BigDecimal

    從 JDK 1.3 起,Java 開發人員就有了另一種數值表示法來表示非整數: BigDecimal 。 BigDecimal 是標準的類,在編譯器中不需要特殊支持,它可以表示任意精度的小數,并對它們進行計算。在內部,可以用任意精度任何范圍的值和一個換算因子來表示 BigDecimal ,換算因子表示左移小數點多少位,從而得到所期望范圍內的值。因此,用 BigDecimal 表示的數的形式為 unscaledValue*10 -scale。

    用于加、減、乘和除的方法給 BigDecimal 值提供了算術運算。由于 BigDecimal 對象是不可變的,這些方法中的每一個都會產生新的 BigDecimal 對象。因此,因為創建對象的開銷, BigDecimal 不適合于大量的數學計算,但設計它的目的是用來精確地表示小數。如果您正在尋找一種能精確表示如貨幣量這樣的數值,則 BigDecimal 可以很好地勝任該任務。

    所有的 equals 方法都不能真正測試相等

    如浮點類型一樣, BigDecimal 也有一些令人奇怪的行為。尤其在使用 equals() 方法來檢測數值之間是否相等時要小心。 equals() 方法認為,兩個表示同一個數但換算值不同(例如, 100.00100.000 )的 BigDecimal 值是不相等的。然而, compareTo() 方法會認為這兩個數是相等的,所以在從數值上比較兩個 BigDecimal 值時,應該使用 compareTo() 而不是 equals() 。

    另外還有一些情形,任意精度的小數運算仍不能表示精確結果。例如, 1 除以 9 會產生無限循環的小數 .111111... 。出于這個原因,在進行除法運算時, BigDecimal 可以讓您顯式地控制舍入。 movePointLeft() 方法支持 10 的冪次方的精確除法。

    使用 BigDecimal 作為互換類型

    SQL-92 包括 DECIMAL 數據類型,它是用于表示定點小數的精確數字類型,它可以對小數進行基本的算術運算。一些 SQL 語言喜歡稱此類型為 NUMERIC 類型,其它一些 SQL 語言則引入了 MONEY 數據類型,MONEY 數據類型被定義為小數點右側帶有兩位的小數。

    如果希望將數字存儲到數據庫中的 DECIMAL 字段,或從 DECIMAL 字段檢索值,則如何確保精確地轉換該數字?您可能不希望使用由 JDBC PreparedStatementResultSet 類所提供的 setFloat()getFloat() 方法,因為浮點數與小數之間的轉換可能會喪失精確性。相反,請使用 PreparedStatementResultSetsetBigDecimal()getBigDecimal() 方法。

    對于 BigDecimal ,有幾個可用的構造函數。其中一個構造函數以雙精度浮點數作為輸入,另一個以整數和換算因子作為輸入,還有一個以小數的 String 表示作為輸入。要小心使用 BigDecimal(double) 構造函數,因為如果不了解它,會在計算過程中產生舍入誤差。請使用基于整數或 String 的構造函數。

    構造 BigDecimal 數

    對于 BigDecimal ,有幾個可用的構造函數。其中一個構造函數以雙精度浮點數作為輸入,另一個以整數和換算因子作為輸入,還有一個以小數的 String 表示作為輸入。要小心使用 BigDecimal(double) 構造函數,因為如果不了解它,會在計算過程中產生舍入誤差。請使用基于整數或 String 的構造函數。

    如果使用 BigDecimal(double) 構造函數不恰當,在傳遞給 JDBC setBigDecimal() 方法時,會造成似乎很奇怪的 JDBC 驅動程序中的異常。例如,考慮以下 JDBC 代碼,該代碼希望將數字 0.01 存儲到小數字段:

    												
    														 PreparedStatement ps =
        connection.prepareStatement("INSERT INTO Foo SET name=?, value=?");
      ps.setString(1, "penny");
      ps.setBigDecimal(2, new BigDecimal(0.01));
      ps.executeUpdate();
    
    												
    										

    在執行這段似乎無害的代碼時會拋出一些令人迷惑不解的異常(這取決于具體的 JDBC 驅動程序),因為 0.01 的雙精度近似值會導致大的換算值,這可能會使 JDBC 驅動程序或數據庫感到迷惑。JDBC 驅動程序會產生異常,但可能不會說明代碼實際上錯在哪里,除非意識到二進制浮點數的局限性。相反,使用 BigDecimal("0.01")BigDecimal(1, 2) 構造 BigDecimal 來避免這類問題,因為這兩種方法都可以精確地表示小數。





    回頁首


    結束語

    在 Java 程序中使用浮點數和小數充滿著陷阱。浮點數和小數不象整數一樣“循規蹈矩”,不能假定浮點計算一定產生整型或精確的結果,雖然它們的確“應該”那樣做。最好將浮點運算保留用作計算本來就不精確的數值,譬如測量。如果需要表示定點數(譬如,幾美元和幾美分),則使用 BigDecimal 。

    posted @ 2006-08-24 17:51 Binary 閱讀(251) | 評論 (0)編輯 收藏

    Java 理論與實踐: 變還是不變?

    不變對象具有許多能更方便地使用它們的特性,包括不嚴格的同步需求和不必考慮數據訛誤就能自由地共享和高速緩存對象引用。盡管不變性可能未必對于所有類都有意義,但大多數程序中至少有一些類將受益于不可變。在本月的 Java 理論與實踐中,Brian Goetz 說明了不變性的一些長處和構造不變類的一些準則。請在附帶的 論壇中與作者和其他讀者分享您關于本文的心得。(也可以單擊文章頂部或底部的“討論”來訪問論壇。)

    不變對象是指在實例化后其外部可見狀態無法更改的對象。Java 類庫中的 String 、 IntegerBigDecimal 類就是不變對象的示例 ― 它們表示在對象的生命期內無法更改的單個值。

    不變性的長處

    如果正確使用不變類,它們會極大地簡化編程。因為它們只能處于一種狀態,所以只要正確構造了它們,就決不會陷入不一致的狀態。您不必復制或克隆不變對象,就能自由地共享和高速緩存對它們的引用;您可以高速緩存它們的字段或其方法的結果,而不用擔心值會不會變成失效的或與對象的其它狀態不一致。不變類通常產生最好的映射鍵。而且,它們本來就是線程安全的,所以不必在線程間同步對它們的訪問。

    自由高速緩存

    因為不變對象的值沒有更改的危險,所以可以自由地高速緩存對它們的引用,而且可以肯定以后的引用仍將引用同一個值。同樣地,因為它們的特性無法更改,所以您可以高速緩存它們的字段和其方法的結果。

    如果對象是可變的,就必須在存儲對其的引用時引起注意。請考慮清單 1 中的代碼,其中排列了兩個由調度程序執行的任務。目的是:現在啟動第一個任務,而在某一天啟動第二個任務。


    清單 1. 可變的 Date 對象的潛在問題
    												
    														 Date d = new Date();
      Scheduler.scheduleTask(task1, d);
      d.setTime(d.getTime() + ONE_DAY);
      scheduler.scheduleTask(task2, d);
    
    												
    										

    因為 Date 是可變的,所以 scheduleTask 方法必須小心地用防范措施將日期參數復制(可能通過 clone() )到它的內部數據結構中。不然, task1task2 可能都在明天執行,這可不是所期望的。更糟的是,任務調度程序所用的內部數據結構會變成訛誤。在編寫象 scheduleTask() 這樣的方法時,極其容易忘記用防范措施復制日期參數。如果忘記這樣做,您就制造了一個難以捕捉的錯誤,這個錯誤不會馬上顯現出來,而且當它暴露時人們要花較長的時間才會捕捉到。不變的 Date 類不可能發生這類錯誤。

    固有的線程安全

    大多數的線程安全問題發生在當多個線程正在試圖并發地修改一個對象的狀態(寫-寫沖突)時,或當一個線程正試圖訪問一個對象的狀態,而另一個線程正在修改它(讀-寫沖突)時。要防止這樣的沖突,必須同步對共享對象的訪問,以便在對象處于不一致狀態時其它線程不能訪問它們。正確地做到這一點會很難,需要大量文檔來確保正確地擴展程序,還可能對性能產生不利后果。只要正確構造了不變對象(這意味著不讓對象引用從構造函數中轉義),就使它們免除了同步訪問的要求,因為無法更改它們的狀態,從而就不可能存在寫-寫沖突或讀-寫沖突。

    不用同步就能自由地在線程間共享對不變對象的引用,可以極大地簡化編寫并發程序的過程,并減少程序可能存在的潛在并發錯誤的數量。

    在惡意運行的代碼面前是安全的

    把對象當作參數的方法不應變更那些對象的狀態,除非文檔明確說明可以這樣做,或者實際上這些方法具有該對象的所有權。當我們將一個對象傳遞給普通方法時,通常不希望對象返回時已被更改。但是,使用可變對象時,完全會是這樣的。如果將 java.awt.Point 傳遞給諸如 Component.setLocation() 的方法,根本不會阻止 setLocation 修改我們傳入的 Point 的位置,也不會阻止 setLocation 存儲對該點的引用并稍后在另一個方法中更改它。(當然, Component 不這樣做,因為它不魯莽,但是并不是所有類都那么客氣。)現在, Point 的狀態已在我們不知道的情況下更改了,其結果具有潛在危險 ― 當點實際上在另一個位置時,我們仍認為它在原來的位置。然而,如果 Point 是不變的,那么這種惡意的代碼就不能以如此令人混亂而危險的方法修改我們的程序狀態了。

    良好的鍵

    不變對象產生最好的 HashMapHashSet 鍵。有些可變對象根據其狀態會更改它們的 hashCode() 值(如清單 2 中的 StringHolder 示例類)。如果使用這種可變對象作為 HashSet 鍵,然后對象更改了其狀態,那么就會對 HashSet 實現引起混亂 ― 如果枚舉集合,該對象仍將出現,但如果用 contains() 查詢集合,它就可能不出現。無需多說,這會引起某些混亂的行為。說明這一情況的清單 2 中的代碼將打印“false”、“1”和“moo”。


    清單 2. 可變 StringHolder 類,不適合用作鍵
    												
    														   public class StringHolder {
            private String string;
            public StringHolder(String s) {
                this.string = s;
            }
            public String getString() {
                return string;
            }
            public void setString(String string) {
                this.string = string;
            }
            public boolean equals(Object o) {
                if (this == o)
                    return true;
                else if (o == null || !(o instanceof StringHolder))
                    return false;
                else {
                    final StringHolder other = (StringHolder) o;
                    if (string == null)
                        return (other.string == null);
                    else
                        return string.equals(other.string);
                }
            }
            public int hashCode() {
                return (string != null ? string.hashCode() : 0);
            }
            public String toString() {
                return string;
            }
            ...
            StringHolder sh = new StringHolder("blert");
            HashSet h = new HashSet();
            h.add(sh);
            sh.setString("moo");
            System.out.println(h.contains(sh));
            System.out.println(h.size());
            System.out.println(h.iterator().next());
        }
    
    												
    										





    回頁首


    何時使用不變類

    不變類最適合表示抽象數據類型(如數字、枚舉類型或顏色)的值。Java 類庫中的基本數字類(如 Integer 、 LongFloat )都是不變的,其它標準數字類型(如 BigIntegerBigDecimal )也是不變的。表示復數或精度任意的有理數的類將比較適合于不變性。甚至包含許多離散值的抽象類型(如向量或矩陣)也很適合實現為不變類,這取決于您的應用程序。

    Flyweight 模式

    不變性啟用了 Flyweight 模式,該模式利用共享使得用對象有效地表示大量細顆粒度的對象變得容易。例如,您可能希望用一個對象來表示字處理文檔中的每個字符或圖像中的每個像素,但這一策略的幼稚實現將會對內存使用和內存管理開銷產生高得驚人的花費。Flyweight 模式采用工廠方法來分配對不變的細顆粒度對象的引用,并通過僅使一個對象實例與字母“a”對應來利用共享縮減對象數。有關 Flyweight 模式的更多信息,請參閱經典書籍 Design Patterns(Gamma 等著;請參閱 參考資料)。

    Java 類庫中不變性的另一個不錯的示例是 java.awt.Color 。在某些顏色表示法(如 RGB、HSB 或 CMYK)中,顏色通常表示為一組有序的數字值,但把一種顏色當作顏色空間中的一個特異值,而不是一組有序的獨立可尋址的值更有意義,因此將 Color 作為不變類實現是有道理的。

    如果要表示的對象是多個基本值的容器(如:點、向量、矩陣或 RGB 顏色),是用可變對象還是用不變對象表示?答案是……要看情況而定。要如何使用它們?它們主要用來表示多維值(如像素的顏色),還是僅僅用作其它對象的一組相關特性集合(如窗口的高度和寬度)的容器?這些特性多久更改一次?如果更改它們,那么各個組件值在應用程序中是否有其自己的含義呢?

    事件是另一個適合用不變類實現的好示例。事件的生命期較短,而且常常會在創建它們的線程以外的線程中消耗,所以使它們成為不變的是利大于弊。大多數 AWT 事件類都沒有作為嚴格的不變類來實現,而是可以有小小的修改。同樣地,在使用一定形式的消息傳遞以在組件間通信的系統中,使消息對象成為不變的或許是明智的。





    回頁首


    編寫不變類的準則

    編寫不變類很容易。如果以下幾點都為真,那么類就是不變的:

    • 它的所有字段都是 final
    • 該類聲明為 final
    • 不允許 this 引用在構造期間轉義
    • 任何包含對可變對象(如數組、集合或類似 Date 的可變類)引用的字段:
      • 是私有的
      • 從不被返回,也不以其它方式公開給調用程序
      • 是對它們所引用對象的唯一引用
      • 構造后不會更改被引用對象的狀態

    最后一組要求似乎挺復雜的,但其基本上意味著如果要存儲對數組或其它可變對象的引用,就必須確保您的類對該可變對象擁有獨占訪問權(因為不然的話,其它類能夠更改其狀態),而且在構造后您不修改其狀態。為允許不變對象存儲對數組的引用,這種復雜性是必要的,因為 Java 語言沒有辦法強制不對 final 數組的元素進行修改。注:如果從傳遞給構造函數的參數中初始化數組引用或其它可變字段,您必須用防范措施將調用程序提供的參數或您無法確保具有獨占訪問權的其它信息復制到數組。否則,調用程序會在調用構造函數之后,修改數組的狀態。清單 3 顯示了編寫一個存儲調用程序提供的數組的不變對象的構造函數的正確方法(和錯誤方法)。


    清單 3. 對不變對象編碼的正確和錯誤方法
    												
    														class ImmutableArrayHolder {
      private final int[] theArray;
      // Right way to write a constructor -- copy the array
      public ImmutableArrayHolder(int[] anArray) {
        this.theArray = (int[]) anArray.clone();
      }
      // Wrong way to write a constructor -- copy the reference
      // The caller could change the array after the call to the constructor
      public ImmutableArrayHolder(int[] anArray) {
        this.theArray = anArray;
      }
      // Right way to write an accessor -- don't expose the array reference
      public int getArrayLength() { return theArray.length }
      public int getArray(int n)  { return theArray[n]; }
      // Right way to write an accessor -- use clone()
      public int[] getArray()       { return (int[]) theArray.clone(); }
      // Wrong way to write an accessor -- expose the array reference
      // A caller could get the array reference and then change the contents
      public int[] getArray()       { return theArray }
    }
    
    												
    										

    通過一些其它工作,可以編寫使用一些非 final 字段的不變類(例如, String 的標準實現使用 hashCode 值的惰性計算),這樣可能比嚴格的 final 類執行得更好。如果類表示抽象類型(如數字類型或顏色)的值,那么您還會想實現 hashCode()equals() 方法,這樣對象將作為 HashMapHashSet 中的一個鍵工作良好。要保持線程安全,不允許 this 引用從構造函數中轉義是很重要的。





    回頁首


    偶爾更改的數據

    有些數據項在程序生命期中一直保持常量,而有些會頻繁更改。常量數據顯然符合不變性,而狀態復雜且頻繁更改的對象通常不適合用不變類來實現。那么有時會更改,但更改又不太頻繁的數據呢?有什么方法能讓 有時更改的數據獲得不變性的便利和線程安全的長處呢?

    util.concurrent 包中的 CopyOnWriteArrayList 類是如何既利用不變性的能力,又仍允許偶爾修改的一個良好示例。它最適合于支持事件監聽程序的類(如用戶界面組件)使用。雖然事件監聽程序的列表可以更改,但通常它更改的頻繁性要比事件的生成少得多。

    除了在修改列表時, CopyOnWriteArrayList 并不變更基本數組,而是創建新數組且廢棄舊數組之外,它的行為與 ArrayList 類非常相似。這意味著當調用程序獲得迭代器(迭代器在內部保存對基本數組的引用)時,迭代器引用的數組實際上是不變的,從而可以無需同步或冒并發修改的風險進行遍歷。這消除了在遍歷前克隆列表或在遍歷期間對列表進行同步的需要,這兩個操作都很麻煩、易于出錯,而且完全使性能惡化。如果遍歷比插入或除去更加頻繁(這在某些情況下是常有的事), CopyOnWriteArrayList 會提供更佳的性能和更方便的訪問。





    回頁首


    結束語

    使用不變對象比使用可變對象要容易得多。它們只能處于一種狀態,所以始終是一致的,它們本來就是線程安全的,可以被自由地共享。使用不變對象可以徹底消除許多容易發生但難以檢測的編程錯誤,如無法在線程間同步訪問或在存儲對數組或對象的引用前無法克隆該數組或對象。在編寫類時,問問自己這個類是否可以作為不變類有效地實現,總是值得的。您可能會對回答常常是肯定的而感到吃驚。

    posted @ 2006-08-24 17:50 Binary 閱讀(211) | 評論 (0)編輯 收藏

    JDBC 查詢日志變得簡單

    JDBC java.sql.PreparedStatement接口的簡單擴展可以使查詢記錄更少犯錯,同時整理您的代碼。在本文中,IBM電子商務顧問Jens Wyke向您介紹如何應用基本的封裝技術(“通過封裝來實現擴展”也稱為Decorator設計模式)來獲得最滿意的結果。

    在大多數情況下,JDBC PreparedStatements 使執行數據庫查詢更簡便并可以顯著提升您整體應用程序的性能。當談到日志查詢語句時 PreparedStatement 接口就顯得有些不足了。 PreparedStatement 的優勢在于其可變性,但是一個好的日志條目必須正確描述如何將SQL發送到數據庫,它將密切關注用實際的參數值來替換所有參數占位符。雖然有多種方法可以解決這一難題,但沒有任何一種易于大規模實施并且大部分將擾亂您的程序代碼。

    在本文中,您將了解到如何擴展JDBC PreparedStatement 接口來進行查詢日志。 LoggableStatement 類實現 PreparedStatement 接口,但添加用于獲得查詢字符串的方法,使用一種適用于記錄的格式。使用 LoggableStatement 類可以減少日志代碼中發生錯誤的幾率,生成簡單且易于管理的代碼。

    注意:本文假設您有豐富的JDBC和 PreparedStatement 類經驗。

    典型日志解決方案

    表1介紹了數據庫查詢時通常是如何使用 PreparedStatement (雖然忽略了初始化和錯誤處理)。在本文中,我們將使用SQL query SELECT 做為例子,但討論使用其它類型的SQL語句,如 DELETE 、 UPDATEINSERT 。


    表1:一個典型的SQL數據庫查詢
    												
    														String sql = "select foo, bar from foobar where foo < ? and bar = ?";
        String fooValue = new Long(99);
        String barValue = "christmas";
    
        Connection conn = dataSource.getConnection();
        PreparedStatement pstmt = conn.prepareStatement(sql);
    
        pstmt.setLong(1,fooValue);
        pstmt.setString(2,barValue);
    
        ResultSet rs = pstmt.executeQuery();
    
        // parse result...
    
    
    												
    										

    表1中一個好的查詢日志條目看起來應與下面有幾分類似:

    												
    														Executing query: select foo,bar from foobar where foo < 99 and 
    bar='christmas'
    
    												
    										

    下面是查詢的日志代碼的一個例子。注意:表1中的問號已經被每個參數的值替換。

    												
    														System.out.println("Executing query: select foo, bar from foobar where foo
    < "+fooValue+" and bar = '+barValue+"'")
    
    												
    										

    一種更好的方法是創建方法,我們稱之為 replaceFirstQuestionMark ,它讀取查詢字符串并用參數值替換問號,如表2所示。這類方法的使用無需創建復制的字符串來描述SQL語句。


    表 2:使用replaceFirstQuestionMark來進行字符串替換
    												
    														      // listing 1 goes here
    
         sql = replaceFirstQuestionMark(sql, fooValue);
         sql = replaceFirstQuestionMark(sql, barValue);
         System.out.println("Executing query: "+sql);
    
    												
    										

    雖然這些解決方案都易于實施,但沒有一種是完美的。問題是在更改SQL模板的同時也必須更改日志代碼。您將在某一點上犯錯幾乎是不可避免的。查詢將更改但您忘記了更新日志代碼,您將結束與將發送到數據庫的查詢不匹配的日志條目 -- 調試惡夢。

    我們真正需要的是一種使我們能夠一次性使用每個參數變量(在我們的實例中為 fooValuebarValue )的設計方案。我們希望有一種方法,它使我們能夠獲得查詢字符串,并用實際的參數值替換參數占位符。由于 java.sql.PreparedStatement 沒有此類方法,我們必須自己實現。





    回頁首


    定制解決方案

    我們的 PreparedStatement 定制實施將做為圍繞JDBC驅動器提供的“真實語句(real statement)”的封裝器(Wrapper)。封裝器語句將轉發所有方法調用(例如 setLong(int, long)setString(int,String) ) 到“真實語句”。在這樣做之前它將保存相關的參數值,從而它們可以用于生成日志輸出結果。

    表3介紹了 LoggableStatement 類如何實現 java.sql.PreparedStatement ,以及它如何使用JDBC連接和SQL模板作為輸入來構建。


    表3:LoggableStatement實現java.sql.PreparedStatement
    												
    														  public class LoggableStatement implements java.sql.PreparedStatement {
    
         // used for storing parameter values needed
          // for producing log
         private ArrayList parameterValues;     
              
         // the query string with question marks as  
         // parameter placeholders
         private String sqlTemplate;       
                   
         // a statement created from a real database     
         // connection                                       
         private PreparedStatement wrappedStatement; 
                                                     
    
        public LoggableStatement(Connection connection, String sql) 
          throws SQLException {
          // use connection to make a prepared statement
          wrappedStatement = connection.prepareStatement(sql);
          sqlTemplate = sql;
          parameterValues = new ArrayList();
        }
         }
    
    												
    										





    回頁首


    LoggableStatement如何工作

    表4介紹了 LoggableStatement 如何向 saveQueryParamValue() 方法添加一個調用,以及在方法 setLongsetString 的“真實語句”上調用相應的方法。我們采用與用于參數設置的所有方法(例如 setChar 、 setLong 、 setRefsetObj )相同的方式來增加 saveQueryParamValue() 調用。表4還顯示了在不調用 saveQueryParamValue() 的情況下如何封裝方法 executeQuery ,因為它不是一個“參數設置”方法。


    表4:LoggableStatement 方法
    												
    														     public void setLong(int parameterIndex, long x) 
             throws java.sql.SQLException {
          wrappedStatement.setLong(parameterIndex, x);
          saveQueryParamValue(parameterIndex, new Long(x));
       }
    
       public void setString(int parameterIndex, String x) 
           throws java.sql.SQLException {
          wrappedStatement.setString(parameterIndex, x);
          saveQueryParamValue(parameterIndex, x);
       }
    
      public ResultSet executeQuery() throws java.sql.SQLException {
         return wrappedStatement.executeQuery();
       }
    
    												
    										

    表5中顯示了 saveQueryParamValue() 方法。它把每個參數值轉換成 String 表示,保存以便 getQueryString 方法日后使用。缺省情況下,一個對象使用其 toString 方法將被轉換成 String ,但如果對象是 StringDate ,它將用單引號('')表示。 getQueryString() 方法使您能夠從日志復制大多數查詢并進行粘貼,無需修改交互式SQL處理器就可進行測試和調試。您可以根據需要修訂該方法來轉換其它類的參數值。


    表5:saveQueryParamValue()方法
    												
    														  private void saveQueryParamValue(int position, Object obj) {
          String strValue;
          if (obj instanceof String || obj instanceof Date) {
               // if we have a String, include '' in the saved value
               strValue = "'" + obj + "'";
          } else {
               if (obj == null) {
                    // convert null to the string null
                     strValue = "null";
               } else {
                    // unknown object (includes all Numbers), just call toString
                    strValue = obj.toString();
               }
          }
          // if we are setting a position larger than current size of 
          // parameterValues, first make it larger
          while (position >= parameterValues.size()) {
               parameterValues.add(null);
          }
          // save the parameter
          parameterValues.set(position, strValue);
     }
    
    												
    										

    當我們使用標準方法來設置所有參數時,我們在 LoggableStatement 中簡單調用 getQueryString() 方法來獲得查詢字符串。所有問號都將被真正的參數值替換,它準備輸出到我們選定的日志目的地。





    回頁首


    使用LoggableStatement

    表6顯示如何更改表1和表2中的代碼來使用 LoggableStatement 。將 LoggableStatement 引入到我們的應用程序代碼中可以解決復制的參數變量問題。如果改變了SQL模板,我們只需更新 PreparedStatement 上的參數設置調用(例如添加一個 pstmt.setString(3,"new-param-value") )。這一更改將在日志輸出結果中反映出,無需任何記錄代碼的手工更新。


    表6:使用LoggableStatement&#160
    												
    														    String sql = "select foo, bar from foobar where foo < ? and bar = ?";
        long fooValue = 99;
        String barValue = "christmas";
    
        Connection conn = dataSource.getConnection();
        PreparedStatement pstmt;
    
        if(logEnabled) // use a switch to toggle logging.
            pstmt = new LoggableStatement(conn,sql);
        else
            pstmt = conn.prepareStatement(sql);
    
        pstmt.setLong(1,fooValue);
        pstmt.setString(2,barValue);
    
        if(logEnabled)
           System.out.println("Executing query: "+
             ((LoggableStatement)pstmt).getQueryString());
    
        ResultSet rs = pstmt.executeQuery();
    
    												
    										





    回頁首


    結束語

    使用本文介紹的非常簡單的步驟,您可以為查詢記錄擴展JDBC PreparedStatement 接口。我們在此處使用的技術可以被視為“通過封裝來實現擴展”,或作為Decorator設計模式的一個實例(見 參考資料)。通過封裝來實現擴展在當您必須擴展API但subclassing不是一項可選功能時極其有用。

    posted @ 2006-08-24 17:50 Binary 閱讀(247) | 評論 (0)編輯 收藏

    使用Jakarta Commons Pool處理對象池化

         摘要: 恰當地使用對象池化技術,可以有效地減少對象生成和初始化時的消耗,提高系統的運行效率。Jakarta Commons Pool組件提供了一整套用于實現對象池化的框架,以及若干種各具特色的對象池實現,可以有效地減少處理對象池化時的工作量,為其它重要的工作留下更多的精力和時間。 創建新的對象并初始化的操作,可能會消耗很多的時間。在這種對象的初始化工作包含了一些費時的操作(例...  閱讀全文

    posted @ 2006-08-24 17:49 Binary 閱讀(244) | 評論 (0)編輯 收藏

    Java 理論和實踐: 理解 JTS ― 幕后魔術

    在這個系列的 第 1 部分,我們討論了事務并研究了它們的基本屬性 ― 原子性(atomicity)、一致性(consistency)、孤立性(isolation)和持久性(durability)。事務是企業應用程序的基本構件;沒有它們,幾乎不可能構建有容錯能力的企業應用程序。幸運的是,Java 事務服務(Java Transaction Service,JTS)和 J2EE 容器自動為您做了大量的事務管理工作,這樣您就不必將事務意識直接集成到組件代碼中。結果簡直是一種魔術 ― 通過遵守幾條簡單的規則,J2EE 應用程序就可以自動獲得事務性語義,只需極少或根本不需要額外的組件代碼。本文旨在通過展示事務管理如何發生,以及發生在何處來揭開這個魔術的神秘面紗。

    什么是 JTS?

    JTS 是一個 組件事務監視器(component transaction monitor)。這是什么意思?在第 1 部分,我們介紹了 事務處理監視器(TPM)這個概念,TPM 是一個程序,它代表應用程序協調分布式事務的執行。TPM 與數據庫出現的時間長短差不多;在 60 年代后期,IBM 首先開發了 CICS,至今人們仍在使用。經典的(或者說 程序化)TPM 管理被程序化定義為針對事務性資源(比如數據庫)的操作序列的事務。隨著分布式對象協議,如 CORBA、DCOM 和 RMI 的出現,人們希望看到事務更面向對象的前景。將事務性語義告知面向對象的組件要求對 TPM 模型進行擴展 ― 在這個模型中事務是按照事務性對象的調用方法定義的。JTS 只是一個組件事務監視器(有時也稱為 對象事務監視器(object transaction monitor)),或稱為 CTM。

    JTS 和 J2EE 的事務支持設計受 CORBA 對象事務服務(CORBA Object Transaction Service,OTS)的影響很大。實際上,JTS 實現 OTS 并充當 Java 事務 API(Java Transaction API)― 一種用來定義事務邊界的低級 API ― 和 OTS 之間的接口。使用 OTS 代替創建一個新對象事務協議遵循了現有標準,并使 J2EE 和 CORBA 能夠互相兼容。

    乍一看,從程序化事務監視器到 CTM 的轉變好像只是術語名稱改變了一下。然而,差別不止這一點。當 CTM 中的事務提交或回滾時,與事務相關的對象所做的全部更改都一起被提交或取消。但 CTM 怎么知道對象在事務期間做了什么事?象 EJB 組件之類的事務性組件并沒有 commit()rollback() 方法,它們也沒向事務監視器注冊自己做了什么事。那么 J2EE 組件執行的操作如何變成事務的一部分呢?





    回頁首


    透明的資源征用

    當應用程序狀態被組件操縱時,它仍然存儲在事務性資源管理器(例如,數據庫和消息隊列服務器)中,這些事務性資源管理器可以注冊為分布式事務中的資源管理器。在第 1 部分中,我們討論了如何在單個事務中征用多個資源管理器,事務管理器如何協調這些資源管理器。資源管理器知道如何把應用程序狀態中的變化與特定的事務關聯起來。

    但這只是把問題的焦點從組件轉移到了資源管理器 ― 容器如何斷定什么資源與該事務有關,可以供它征用?請考慮下面的代碼,在典型的 EJB 會話 bean 中您可能會發現這樣的代碼:


    清單 1. bean 管理的事務的透明資源征用
    												
    														  InitialContext ic = new InitialContext();
      UserTransaction ut = ejbContext.getUserTransaction();
      ut.begin();
      DataSource db1 = (DataSource) ic.lookup("java:comp/env/OrdersDB");
      DataSource db2 = (DataSource) ic.lookup("java:comp/env/InventoryDB");
      Connection con1 = db1.getConnection();
      Connection con2 = db2.getConnection();
      // perform updates to OrdersDB using connection con1
      // perform updates to InventoryDB using connection con2
      ut.commit();
    
    												
    										

    注意,這個示例中沒有征用當前事務中 JDBC 連接的代碼 ― 容器會為我們完成這個任務。我們來看一下它是如何發生的。

    資源管理器的三種類型

    當一個 EJB 組件想訪問數據庫、消息隊列服務器或者其它一些事務性資源時,它需要到資源管理器的連接(通常是使用 JNDI)。而且,J2EE 規范只認可三種類型的事務性資源 ― JDBC 數據庫、JMS 消息隊列服務器和“其它通過 JCA 訪問的事務性服務”。后面一種服務(比如 ERP 系統)必須通過 JCA(J2EE Connector Architecture,J2EE 連接器體系結構)訪問。對于這些類型資源中的每一種,容器或提供者都會幫我們把資源征調到事務中。

    在清單 1 中, con1con2 好象是普通的 JDBC 連接,比如那些從 DriverManager.getConnection() 返回的連接。我們從一個 JDBC DataSource 得到這些連接,JDBC DataSource 可以通過查找 JNDI 中的數據源名稱得到。EJB 組件中被用來查找數據源( java:comp/env/OrdersDB )的名稱是特定于組件的;組件的部署描述符的 resource-ref 部分將其映射為容器管理的一些應用程序級 DataSource 的 JNDI 名稱。

    隱藏的 JDBC 驅動器

    每個 J2EE 容器都可以創建有事務意識的池態 DataSource 對象,但 J2EE 規范并不向您展示如何創建,因為這不在 J2EE 規范內。瀏覽 J2EE 文檔時,您找不到任何關于如何創建 JDBC 數據源的內容。相反,您不得不為您的容器查閱該文檔。創建一個數據源可能需要向屬性或配置文件添加一個數據源定義,或者也可以通過 GUI 管理工具完成,這取決于您的容器。

    每個容器(或連接池管理器,如 PoolMan)都提供它自己的創建 DataSource 機制,JTA 魔術就隱藏在這個機制中。連接池管理器從指定的 JDBC 驅動器得到一個 Connection ,但在將它返回到應用程序之前,將它與一個也實現 Connection 的虛包包在一起,將自己置入應用程序和底層連接之間。當創建連接或者執行 JDBC 操作時,包裝器詢問事務管理器當前線程是不是正在事務的上下文中執行,如果事務中有 Connection 的話,就自動征用它。

    其它類型的事務性資源,JMS 消息隊列和 JCA 連接器,依靠相似的機制將資源征用隱藏起來,使用戶看不到。如果要使 JMS 隊列在部署時對 J2EE 應用程序可用,您就要再次使用特定于提供者的機制來創建受管 JMS 對象(隊列連接工廠和目標),然后在 JNDI 名稱空間內發布這些對象。提供者創建的受管對象包含與 JDBC 包裝器(由容器提供的連接池管理器添加)相似的自動征用代碼。





    回頁首


    透明的事務控制

    兩種類型的 J2EE 事務 ― 容器管理的和 bean 管理的 ― 在如何啟動和結束事務上是不同的。事務啟動和結束的地方被稱為 事務劃分(transaction demarcation)。清單 1 中的示例代碼演示了 bean 管理的事務(有時也稱為 編程(programmatic)事務)。Bean 管理的事務是由組件使用 UserTransaction 類顯式啟動和結束的。通過 ejbContext 使 UserTransaction 對 EJB 組件可用,通過 JNDI 使其對其它 J2EE 組件可用。

    容器根據組件的部署描述符中的事務屬性代表應用程序透明地啟動和結束容器管理的事務(或稱為 宣告式事務(declarative transaction))。通過將 transaction-type 屬性設置為 ContainerBean 您可以指出 EJB 組件是使用 bean 管理的事務性支持還是容器管理的事務性支持。

    使用容器管理的事務,您可以在 EJB 類或方法級別上指定事務性屬性;您可以為 EJB 類指定缺省的事務性屬性,如果不同的方法會有不同的事務性語義,您還可以為每個方法指定屬性。這些事務性屬性在裝配描述符(assembly descriptor)的 container-transaction 部分被指定。清單 2 顯示了一個裝配描述符示例。 trans-attribute 的受支持的值有:

    • Supports
    • Required
    • RequiresNew
    • Mandatory
    • NotSupported
    • Never

    trans-attribute 決定方法是否支持在事務內部執行、當在事務內部調用方法時容器會執行什么操作以及在事務外部調用方法時容器會執行什么操作。最常用的容器管理的事務屬性是 Required 。如果設置了 Required ,過程中的事務將在該事務中征用您的 bean,但如果沒有正在運行的事務,容器將為您啟動一個。在這個系列的第 3 部分,當您可能想使用每個事務屬性時,我們將研究各個事務屬性之間的區別。


    清單 2. EJB 裝配描述符樣本
    												
    														<assembly-descriptor>
      ...
      <container-transaction>
        <method>
          <ejb-name>MyBean</ejb-name>
          <method-name>*</method-name>
        </method>
        <trans-attribute>Required</trans-attribute>
      </container-transaction>
      <container-transaction>
        <method>
          <ejb-name>MyBean</ejb-name>
          <method-name>updateName</method-name>
          </method>
       <trans-attribute>RequiresNew</trans-attribute>
      </container-transaction>
      ...
    </assembly-descriptor>
    
    												
    										

    功能強大,但很危險

    與清單 1 中的示例不同,由于有宣告式事務劃分,這段組件代碼中沒有事務管理代碼。這不僅使結果組件代碼更加易讀(因為它不與事務管理代碼混在一起),而且它還有另一個更重要的優點 ― 不必修改,甚至不必訪問組件的源代碼,就可以在應用程序裝配時改變組件的事務性語義。

    盡管能夠指定與代碼分開的事務劃分是一種非常強大的功能,但在裝配時做出不好的決定會使應用程序變得不穩定,或者嚴重影響它的性能。對容器管理的事務進行正確分界的責任由組件開發者和應用程序裝配人員共同擔當。組件開發者需要提供足夠的文檔說明組件是做什么的,這樣應用程序部署者就能夠明智地決定如何構建應用程序的事務。應用程序裝配人員需要理解應用程序中的組件是怎樣相互作用的,這樣就可以用一種既強制應用程序保持一致又不削弱性能的方法對事務進行分界。在這個系列的第 3 部分中我們將討論這些問題。





    回頁首


    透明的事務傳播

    在任何類型的事務中,資源征用都是透明的;容器自動將事務過程中使用的任意事務性資源征調到當前事務中。這個過程不僅擴展到事務性方法使用的資源(比如在清單 1 中獲得的數據庫連接),還擴展到它調用的方法(甚至遠程方法)使用的資源。我們來看一下這是如何發生的。

    容器用線程與事務相關聯

    我們假設對象 AmethodA() 啟動一個事務,然后調用對象 BmethodB() (對象 B 將得到一個 JDBC 連接并更新數據庫)。 B 獲得的連接將被自動征調到 A 創建的事務中。容器怎么知道要做這件事?

    當事務啟動時,事務上下文與執行線程關聯在一起。當 A 創建事務時, A 在其中執行的線程與該事務關聯在一起。由于本地方法調用與主調程序(caller)在同一個線程內執行,所以 A 調用的每個方法也都在該事務的上下文中。

    櫥中骸骨

    如果對象 B 其實是在另一個線程,甚至另一個 JVM 中執行的 EJB 組件的存根,情況會怎樣?令人吃驚的是,遠程對象 B 訪問的資源仍將在當前事務中被征用。EJB 對象存根(在主調程序的上下文中執行的那部分)、EJB 協議(IIOP 上的 RMI)和遠端的骨架對象協力要使其透明地發生。存根確定調用者是不是正在執行一個事務。如果是,事務標識,或者說 Xid,被作為 IIOP 調用的一部分與方法參數一起傳播到遠程對象。(IIOP 是 CORBA 遠程-調用協議,它為傳播執行上下文(比如事務上下文和安全性上下文)的各種元素而備;關于 RMI over IIOP 的更多信息,請參閱 參考資料。)如果調用是事務的一部分,那么遠程系統上的骨架對象自動設置遠程線程的事務上下文,這樣,當調用實際的遠程方法時,它已經是事務的一部分了。(存根和骨架對象還負責開始和提交容器管理的事務。)

    事務可以由任何 J2EE 組件來啟動 ― 一個 EJB 組件、一個 servlet 或者一個 JSP 頁面(如果容器支持的話,還可以是一個應用程序客戶機)。這意味著,應用程序可以在請求到達時在 servlet 或者 JSP 頁面中啟動事務、在 servlet 或者 JSP 頁面中執行一些處理、作為頁面邏輯的一部分訪問多個服務器上的實體 bean 和會話 bean 并使所有這些工作透明地成為一個事務的一部分。圖 1 演示了事務上下文怎樣遵守從 servlet 到 EJB,再到 EJB 的執行路徑。


    圖 1.單個事務中的多個組件
    單個事務中的多個組件




    回頁首


    最優化

    讓容器來管理事務允許容器為我們做出某些最優化決定。在圖 1 中,我們看到一個 servlet 和多個 EJB 組件在單個事務的上下文中訪問一個數據庫。每個組件都獲得到數據庫的 Connection ;很可能每個組件都在訪問同一個數據庫。即使多個連接是從不同的組件到同一個資源,JTS 也可以檢測出多個資源是否和事務有關,并最優化該事務的執行。您可以從第 1 部分回憶起來,單個事務要包含多個資源管理器需要使用兩階段提交協議,這比單個資源管理器使用的單階段提交代價要高。JTS 能夠確定事務中是不是只征用了一個資源管理器。如果它檢測出所有與事務相關的資源都一樣,它可以跳過兩階段提交并讓資源管理器自己來處理事務。





    回頁首


    結束語

    這個慮及透明事務控制、資源征用和透明傳播的魔術不是 JTS 的一部分,而是 J2EE 容器如何在幕后代表 J2EE 應用程序使用 JTA 和 JTS 服務的一部分。在幕后有許多實體合力使這個魔術透明地發生;EJB 存根和骨架、容器廠商提供的 JDBC 驅動器包裝器、數據庫廠商提供的 JDBC 驅動器、JMS 提供器和 JCA 連接器。所有這些實體都與事務管理器進行交互,于是應用程序代碼就不必與之交互了。

    在第 3 部分,我們將看一下關于管理 J2EE 上下文中事務的一些實際問題 ― 事務劃分和孤立 ― 以及它們對應用程序一致性、穩定性和性能的影響。

    posted @ 2006-08-24 17:47 Binary 閱讀(255) | 評論 (0)編輯 收藏

    僅列出標題
    共8頁: 上一頁 1 2 3 4 5 6 7 8 下一頁 
    主站蜘蛛池模板: 国产成A人亚洲精V品无码| 亚洲Av无码国产情品久久| 在线观看人成视频免费| 国产成人免费手机在线观看视频 | 理论片在线观看免费| 精品国产免费人成网站| 久久福利青草精品资源站免费| 最近免费中文字幕高清大全 | 国产免费观看黄AV片| 久久精品国产亚洲精品| 亚洲国产天堂在线观看| 亚洲中文字幕日本无线码| 美女一级毛片免费观看| 久草免费福利视频| 成年女性特黄午夜视频免费看| 亚洲 自拍 另类小说综合图区| 亚洲国产精华液网站w| 亚洲熟妇色自偷自拍另类| 在线看亚洲十八禁网站| 182tv免费视频在线观看| 久久久久久国产a免费观看黄色大片 | 黄页视频在线观看免费| 可以免费观看的国产视频| 猫咪社区免费资源在线观看 | 1区1区3区4区产品亚洲| 欧洲亚洲国产精华液| 黄色网址在线免费| 波多野结衣久久高清免费 | 久久精品国产免费观看三人同眠| 日韩免费无砖专区2020狼| 久久91亚洲人成电影网站| 亚洲精品综合在线影院| 香蕉视频在线免费看| 免费无码AV电影在线观看| 亚洲欧洲日产国码无码网站| 亚洲一本到无码av中文字幕| 在线观看免费无码视频| 日韩成人免费视频播放| 精品亚洲国产成AV人片传媒| 黄网站在线播放视频免费观看| 亚洲黄色免费电影|