1. 程式人生 > 實用技巧 >1-07 異常、斷言和日誌

1-07 異常、斷言和日誌

LAST UPDATE:2020/10/31


參考:

  1. JAVA核心技術卷Ⅰ
  2. 廖雪峰-JAVA教程-異常

第7章 異常、斷言和日誌

  • 異常處理(exception handing)

  • 存在多個catch的時候,catch的順序非常重要:子類必須寫在前面。

  • 捕獲到異常並再次丟擲時,一定要留住原始異常,否則很難定位第一案發現場!

  • 專案中一個常見的做法是自定義一個BaseException作為“根異常”,然後,派生出各種業務型別的異常。

    BaseException需要從一個適合的Exception派生,通常建議從RuntimeException派生:

    其他業務型別的異常就可以從BaseException

    派生

    自定義的BaseException應該提供多個構造方法

7.1 處理錯誤

  • 如果由於出現錯誤而使得某些操作沒有完成,程式應該:
    • 返回到一種安全狀態,並能夠讓使用者執行一些其他的命令
    • 或者允許使用者儲存所有操作的結果,並以妥善的方式終止程式。
  • 異常處理的任務就是將控制權從錯誤產生的地方轉移給能夠處理這種情況的錯誤處理器。
  • 錯誤型別
    • 使用者輸入錯誤
    • 裝置錯誤
    • 物理限制
    • 程式碼錯誤
  • 對於方法中的一個錯誤,傳統的做法是返回一個特殊的錯誤碼。但是,並不是在任何情況下都能返回一個錯誤碼。有可能無法明確地將有效資料與無效資料加以區分。
  • 在Java中,如果某個方法不能夠採用正常的途徑完整它的任務,就可以通過另外一個路徑退出方法。
    • 在這種情況下,方法並不返回任何值,而是丟擲( throw)一個封裝了錯誤資訊的物件
      。需要注意的是,這個方法將會立刻退出,並不返回任何值。此外,呼叫這個方法的程式碼也將無法繼續執行,取而代之的是,異常處理機制開始搜尋能夠處理這種異常狀況的異常處理器(exception handler)。
7.1.1 異常分類
graph TB Z[Object] A[Thowable] B[Error] C[Exception] D[IOException] E[RuntimeException] Z-->A A-->B A-->C C-->D C-->E
  • 在Java程式設計語言中,異常物件都是派生於Throwable類的一個例項。
    • 如果Java中內建的異常類不能夠滿足需求,使用者可以建立自己的異常類。
  • 所有的異常都是由Throwable繼承而來,但在下一層立即分解為兩個分支: Error 和Exception
    • Error類層次結構描述了Java執行時系統的內部錯誤和資源耗盡錯誤。應用程式不應該丟擲這種型別的物件。
    • 在設計Java程式時,需要關注Exception層次結構。這個層次結構又分解為兩個分支:
      • 一個分支派生於RuntimeException;
      • 另一個分支包含其他異常。
      • 派生於RuntimeException的異常包含下面幾種情況:
        • 錯誤的型別轉換。
        • 陣列訪問越界。
        • 訪問null指標。
      • 不是派生於RuntimeException的異常包括:
        • 試圖在檔案尾部後面讀取資料。
        • 試圖開啟一個不存在的檔案。
        • 試圖根據給定的字串查詢Class物件,而這個字串表示的類並不存在。
  • "如果出現RuntimeException異常,那麼就一定是你的問題
  • 派生於Error類或RuntimeException類的所有異常稱為非受查(unchecked)異常,所有其他的異常稱為受查(checked)異常
    • 編譯器將核查是否為所有的受查異常提供了異常處理器。
7.1.2 宣告受查異常
  • 一個方法不僅需要告訴編譯器將要返回什麼值,還要告訴編譯器有可能發生什麼錯誤。

  • 方法應該在其首部宣告所有可能丟擲的異常。

  • 在遇到下面4種情況時應該丟擲異常

    • 呼叫一個丟擲受查異常的方法,例如,FileInputStream 構造器。

    • 程式執行過程中發現錯誤,並且利用throw語句丟擲一個受查異常

    • 程式出現錯誤,例如,a[- 1]=0會丟擲一個ArrayIndexOutOfBoundsException這樣的
      非受查異常。

    • Java虛擬機器和執行時庫出現的內部錯誤。

    如果出現前兩種情況之一,則必須告訴呼叫這個方法的程式設計師有可能丟擲異常如果沒有處理器捕獲這個異常,當前執行的執行緒就會結束。

  • 對於那些可能被他人使用的Java方法,應該根據異常規範( exception specification), 在
    方法的首部宣告這個方法可能丟擲的異常。

  • 如果一個方法有可能丟擲多個受查異常型別,那麼就必須在方法的首部列出所有的異常類。每個異常類之間用逗號隔開。

  • 不需要宣告Java的內部錯誤,即從Error繼承的錯誤

  • 也不應該宣告從RuntimeException繼承的那些非受查異常。這些執行時錯誤完全在我們的控制之下。

  • 總之,一個方法必須宣告所有可能丟擲的受查異常,而非受查異常要麼不可控制(Error),要麼就應該避免發生(RuntimeException)。

  • 除了宣告異常之外,還可以捕獲異常。

  • 如果在子類中覆蓋了超類的一個方法,子類方法中宣告的受查異常不能比超類方法中宣告的異常更通用

    • 特別需要說明的是,如果超類方法沒有丟擲任何受查異常,子類也不能丟擲任何受查異常。
  • 如果類中的一個方法宣告將會丟擲一個異常,而這個異常是某個特定類的例項時,
    則這個方法就有可能丟擲一個這個類的異常,或者這個類的任意一個子類的異常。

7.1.3 如何丟擲異常
  • EOFException異常:“在輸入過程中,遇到了一個未預期的EOF後的訊號”。
  • EOFException類還有一個含有一個字串型引數的構造器。這個構造器可以更加細緻的
    描述異常出現的情況。
  • 在這種情況下:
    • 1)找到一個合適的異常類。
    • 2)建立這個類的一個物件。
    • 3)將物件丟擲。
      一旦方法丟擲了異常,這個方法就不可能返回到呼叫者。
7.1.4 建立異常類
  • 我們需要做的只是定義一個派生於Exception的類,或者派生於Exception 子類的類。

  • 習慣上,定義的類應該包含兩個構造器

    • 一個是預設的構造器;

    • 另一個是帶有詳細描述資訊的構造器

      超類Throwable的toString方法將會打印出這些詳細資訊,這在除錯中非常有用)。

7.2 捕獲異常

7.2.1 捕獲異常
  • 如果某個異常發生的時候沒有在任何地方進行捕獲,那程式就會終止執行,並在控制檯
    上打印出異常資訊,其中包括異常的型別和堆疊的內容。

  • 要想捕獲一個異常,必須設定try/catch語句塊。

  • 如果在try語句塊中的任何程式碼丟擲了一個在catch子句中說明的異常類,那麼

    • 程式將跳過try 語句塊的其餘程式碼。
    • 程式將執行catch子句中的處理器程式碼。

    如果在try 語句塊中的程式碼沒有丟擲任何異常,那麼程式將跳過catch子句。
    如果方法中的任何程式碼丟擲了一個在catch子句中沒有宣告的異常型別,那麼這個方法
    就會立刻退出(希望呼叫者為這種型別的異常設計了catch子句)。

  • 編譯器嚴格地執行throws說明符。如果呼叫了一個丟擲受查異常的方法,就必
    須對它進行處理,或者繼續傳遞。

    • 通常,應該捕獲那些知道如何處理的異常,而將那些不知道怎樣處理的異常繼續進行傳遞。
    • 如果想傳遞一個異常,就必須在方法的首部新增一個throws 說明符,以便告知呼叫者這個方法可能會丟擲異常。
    • 如果編寫一個覆蓋超類的方法,而這個方法又沒有丟擲異常,那麼這個方法就必須捕
      獲方法程式碼中出現的每一個受查異常不允許在子類的throws說明符中出現超過超類方法所列出的異常類範圍。
7.2.2 捕獲多個異常
  • 在一個try語句塊中可以捕獲多個異常型別,並對不同型別的異常做出不同的處理。

  • 異常物件可能包含與異常本身有關的資訊。要想獲得物件的更多資訊,可以試著使用

    //得到詳細的錯誤資訊(如果有的話)
    e.getMessage();
    //得到異常物件的實際型別。
    e.getClass().getName();
    
  • 在Java SE 7中,同一個catch子句中可以捕獲多個異常型別。

    • 只有當捕獲的異常型別彼此之間不存在子類關係時才需要這個特性。

    • 捕獲多個異常時,異常變數隱含為final變數

      • 例如,不能在以下子句體中為e賦不同的值:

        catch (FileNotFoundException | UnknownHostException e){...}
        
  • 捕獲多個異常不僅會讓你的程式碼看起來更簡單,還會更高效。生成的位元組碼只包含一個對應公共catch子句的程式碼塊。

7.2.3 再次丟擲異常與異常鏈
  • 在catch子句中可以丟擲一個異常,這樣做的目的是改變異常的型別。

    try
    {
    	access the database
    }
    catch (SQLException e)
    {
    	Throwable se = new ServletException("database error");
    	se.initCause(e);
    	throw se;
    }
    
    //當捕獲到異常時,就可以使用下面這條語句重新得到原始異常:
    Throwable e = se.getCause();
    
  • 強烈建議使用這種包裝技術。這樣可以讓使用者丟擲子系統中的高階異常,而不會丟失原始異常的細節。

  • 如果在一個方法中發生了一個受查異常,而不允許丟擲它,那麼包裝技術就十分有用。我們可以捕獲這個受查異常,並將它包裝成一個執行時異常。

  • 有時你可能只想記錄一個異常,再將它重新丟擲,而不做任何改變:

    try
    {
    	access the database
    }
    catch (Exception e)
    {
    	logger.log(level,message, e);
    	throw e;
    }
    
7.2.4 finally子句
  • 不管是否有異常被捕獲,finally子句中的程式碼都被執行。
  • try語句可以只有finally子句,而沒有catch子句
  • 事實上,我們認為在需要關閉資源時,使用finally子句是一種不錯的選擇。
  • 當finally子句包含return語句時,將會出現一種意想不到的結果。
    • 假設利用return語句從try 語句塊中退出。在方法返回前,finally子句的內容將被執行。
    • 如果finally子句中也有一個return語句,這個返回值將會覆蓋原始的返回值。
  • 現在,假設在try語句塊中的程式碼丟擲了一些非IOException的異常,這些異常只有這個
    方法的呼叫者才能夠給予處理。執行finally語句塊,並呼叫close方法。而close方法本身也有可能丟擲IOException異常。當出現這種情況時,原始的異常將會丟失,轉而丟擲close方法的異常。
    這會有問題,因為第一個異常很可能更有意思。如果你想做適當的處理,重新丟擲原來
    的異常,程式碼會變得極其繁瑣。
7.2.5 帶資源的try語句
  • 對於以下程式碼模式:

    open a resource
    try
    {
    	work with the resource
    }
    finally
    {
    	close the resource
    }
    

    假設資源屬於一個實現了AutoCloseable介面的類,Java SE 7為這種程式碼模式提供了一
    個很有用的快捷方式。AutoCloseable介面有一個方法:

    void close() throws Exception
    

    另外,還有一個Closeable介面。這是AutoCloseable的子介面,也包含一個close
    方法。不過,這個方法宣告為丟擲一個IOException。

  • 帶資源的try 語句(try-with-resources)的最簡形式為:

    try (Resource res = . . .)
    {
    	work with res
    }
    

    try塊退出時,會自動呼叫res.close()

  • 下面給出一個典型的例子,這裡要讀取一個檔案中的所有單詞:

    try (Scanner in = new Scanner(new Fi1eInputStream(" /usr/share/dict/words")),"UTF-8"")
    {
        while (in.hasNext())
        	out.println(in.next().toUpperCase());
    }
    

    這個塊正常退出時,或者存在一個異常時,都會呼叫in.close()方法,就好像使用了
    finally塊一樣。

  • 還可以指定多個資源。例如:

    try (Scanner in = new Scanner(new FileInputStream(" /usr/share/dict/words"),"UTF-8");
    Printwriter out = new Printwriter("out.txt"))
    {
    	while (in.hasNext())
    		out.println(in.next().toUpperCase());
    }
    

    不論這個塊如何退出,in和 out都會關閉。如果你用常規方式手動程式設計,就需要兩個嵌
    套的 try/finally 語句。

  • 上一節已經看到,如果try塊丟擲一個異常,而且 close方法也丟擲一個異常,這就會帶
    來一個難題。帶資源的try語句可以很好地處理這種情況。原來的異常會重新丟擲,而close方法丟擲的異常會“被抑制”。這些異常將自動捕獲,並由addSuppressed方法增加到原來的異常。如果對這些異常感興趣,可以呼叫getSuppressed方法,它會得到從close方法丟擲並被抑制的異常列表。
    你肯定不想採用這種常規方式程式設計。

  • 只要需要關閉資源,就要儘可能使用帶資源的try語句。

  • 帶資源的try 語句自身也可以有catch子句和一個finally子句。這些子句會在關閉資源之後執行。不過在實際中,一個try語句中加入這麼多內容可能不是一個好主意。

7.2.6 分析堆疊軌跡元素
  • 堆疊軌跡(stack trace)是一個方法呼叫過程的列表,它包含了程式執行過程中呼叫的特定位置。

    • 當JAVA程式正常終止,而沒有捕獲異常時,這個列表哦就會顯示出來。
  • 可以呼叫Throwable類的printStackTrace方法訪問堆疊軌跡的文字描述資訊。

  • 一種更靈活的方法是使用getStackTrace方法,它會得到StackTraceElement物件的一個數組,可以在你的程式中分析這個物件陣列。

    • StackTraceElement類含有能夠獲得檔名和當前執行的程式碼行號的方法,同時,還含有能夠獲得類名和方法名的方法。toString方法將產生一個格式化的字串,其中包含所獲得的資訊。
  • 靜態的Thread.getAllStackTrace方法,它可以產生所有執行緒的堆疊軌跡。

    Map<Thread, StackTraceElement[]> map = Thread.getA11StackTraces();
    for (Thread t : map.keySet())
    {
        StackTraceElement[] frames = map.get(t);
    	analyze frames
    }
    

7.3 使用異常機制的技巧

  • 異常處理不能代替簡單的測試

    • 與執行簡單測試相比,捕獲異常所花費的時間大大超過了前者。
    • 原則:只在異常情況下使用異常機制。
  • 不要過分細化異常

  • 利用異常層次結構

    • 不要只丟擲RuntimcException異常。應該尋找更加適當的子類或建立自己的異常類。
    • 不要只捕獲Thowable異常,否則,會使程式程式碼更難讀、更難維護。
  • 不要壓制異常

  • 在檢測錯誤時,”苛刻“要比放任更好。

  • 不要羞於傳遞異常

    最後兩個可以歸納為:”早丟擲,晚捕獲

7.4 使用斷言

  • 在一個具有自我保護能力的程式中,斷言很常用。
  • Java斷言的特點是:斷言失敗時會丟擲AssertionError,導致程式結束退出。因此,斷言不能用於可恢復的程式錯誤,只應該用於開發和測試階段。
  • 對可恢復的錯誤不能使用斷言,而應該丟擲異常。
  • 斷言很少被使用,更好的方法是編寫單元測試。

7.4.1 斷言的概念
  • 斷言機制允許在測試期間向程式碼中插入一些檢查語句。當代碼釋出時,這些插入的檢測語句將會被自動地移走。

  • Java語言引入了關鍵字assert。這個關鍵字有兩種形式:
    assert 條件;

    assert 條件:表示式;
    這兩種形式都會對條件進行檢測,如果結果為false,則丟擲一個AssertionError異常。
    在第二種形式中,表示式將被傳入AssertionError 的構造器,並轉換成一個訊息字串。

    註釋:“表示式部分的唯一目的是產生一個訊息字串。AssertionError物件並不儲存
    表示式的值,因此,不可能在以後得到它。

7.4.2 啟用和禁用斷言
  • 在預設情況下,斷言被禁用。可以在執行程式時用-enableassertions或-ea選項啟用:

    java -enableassertions MyApp
    

    需要注意的是,在啟用或禁用斷言時不必重新編譯程式。啟用或禁用斷言是類載入器(class loader)的功能。當斷言被禁用時,類載入器將跳過斷言程式碼,因此,不會降低程式執行的速度。

  • 也可以在某個類或整個包中使用斷言,例如:

    java -ea:MyClass -ea:com.mycompany.mylib...MyApp
    

    這條命令將開啟MyClass類以及在com.mycompany.mylib包和它的子包中的所有類的斷
    言。選項-ea將開啟預設包中的所有類的斷言。

  • 也可以用選項-disableassertions或-da禁用某個特定類和包的斷言:

    java -ea:... -da:MyClass MyApp
    
  • 有些類不是由類載入器載入,而是直接由虛擬機器載入。可以使用這些開關有選擇地啟用
    或禁用那些類中的斷言。
    然而,啟用和禁用所有斷言的-ea和-da開關不能應用到那些沒有類載入器的“系統類”
    上。

    對於這些系統類來說,需要使用-enablesystemassertions/-esa開關啟用斷言。
    在程式中也可以控制類載入器的斷言狀態。

7.4.3 使用斷言完成引數檢查
  • 在Java語言中,給出了3種處理系統錯誤的機制:

    • 丟擲一個異常
    • 日誌
    • 使用斷言
  • 什麼時候應該選擇使用斷言呢?請記住下面幾點:

    • 斷言失敗是致命的、不可恢復的錯誤。
    • 斷言檢查只用於開發和測階段

    因此,不應該使用斷言向程式的其他部分通告發生了可恢復性的錯誤,或者,不應該作
    為程式向用戶通告問題的手段。斷言只應該用於在測試階段確定程式內部的錯誤位置。

7.4.4 為文件假設使用斷言

7.5 記錄日誌

  • 斷言是一種測試和除錯階段所使用的戰術性工具,而日誌記錄是一種在程式的整個生命週期都可以使用的策略性工具。
  • Commons Logging
    • Commons Logging是一個第三方日誌庫,它是由Apache建立的日誌模組。
    • 它可以掛接不同的日誌系統,並通過配置檔案指定掛接的日誌系統。預設情況下,Commons Loggin自動搜尋並使用Log4j(Log4j是另一個流行的日誌系統),如果沒有找到Log4j,再使用JDK Logging。
    • 是使用最廣泛的日誌模組;
    • API非常簡單;
    • 可以自動檢測並使用其他日誌模組。
  • 其實SLF4J類似於Commons Logging,也是一個日誌介面,而Logback類似於Log4j,是一個日誌的實現。
    • SLF4J和Logback可以取代Commons Logging和Log4j;
    • 始終使用SLF4J的介面寫入日誌,使用Logback只需要配置,不需要修改程式碼。

  • 記錄日誌API的優點
    • 可以很容易地取消全部日誌記錄,或者僅僅取消某個級別的日誌,而且開啟和關閉這
      個操作也很容易。
    • 可以很簡單地禁止日誌記錄的輸出,因此,將這些日誌程式碼留在程式中的開銷很小。
    • 日誌記錄可以被定向到不同的處理器,用於在控制檯中顯示,用於儲存在檔案中等。
    • 日誌記錄器和處理器都可以對記錄進行過濾。過濾器可以根據過濾實現器制定的標準
      丟棄那些無用的記錄項。
    • 日誌記錄可以採用不同的方式格式化,例如,純文字或XML。
    • 應用程式可以使用多個日誌記錄器,它們使用類似包名的這種具有層次結構的名字,
      例如,com.mycompany.myapp.
    • 在預設情況下,日誌系統的配置由配置檔案控制。如果需要的話,應用程式可以替換
      這個配置。
7.5.1 基本日誌
  • 要生成簡單的日誌記錄,可以使用全域性日誌記錄器(global logger)並呼叫其info方法:

    Logger.getGlobal().info("File->0pen menu item selected");
    

    在預設情況下,這條記錄將會顯示以下內容:

    May 10,2013 10:12:15 PM LogginglmageViewer fileOpen
    INFO: File->Open menu item selected
    

    但是,如果在適當的地方(如main開始)呼叫

    Logger.getGlobal().setLevel(Level.OFF);
    

    將會取消所有的日誌。

7.5.2 高階日誌
  • 可以呼叫getLogger方法建立或獲取記錄器:

    private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
    

    提示:未被任何變數引用的日誌記錄器可能會被垃圾回收。為了防止這種情況發生,要像上面的例子中一樣,用一個靜態變數儲存日誌記錄器的一個引用。)

  • 與包名類似,日誌記錄器名也具有層次結構。

    • 事實上,與包名相比,日誌記錄器的層次性更強。
    • 對於包來說,一個包的名字與其父包的名字之間沒有語義關係,但是日誌記錄器
      的父與子之間將共享某些屬性。
  • 通常,有以下7個日誌記錄器級別:

    • SEVERE
    • WARNING
    • INFO
    • CONFIG
    • FINE
    • FINER
    • FINEST

    在預設情況下,只記錄前三個級別。也可以設定其他的級別。例如,

    //現在,FINE和更高級別的記錄都可以記錄下來。
    logger.setLevel(Level.FINE);
    

    另外,還可以使用Level.ALL開啟所有級別的記錄,或者使用Level.OFF關閉所有級別
    的記錄。
    對於所有的級別有下面幾種記錄方法:

    logger.warning(message);
    logger.fine(message);
    

    同時,還可以使用log方法指定級別,例如:

    logger.log(Level.FINE,message);
    
  • 預設的日誌記錄將顯示包含日誌呼叫的類名和方法名,如同堆疊所顯示的那樣。但是,
    如果虛擬機器對執行過程進行了優化,就得不到準確的呼叫資訊。此時,可以呼叫logp方法獲得呼叫類和方法的確切位置,這個方法的簽名為:

    void logp(Level 1, String className,String methodName, String message)
    
  • //其他重要函式
    entering
    throwing
    log
    
7.5.3 修改日誌管理器配置
  • 可以通過編輯配置檔案來修改日誌系統的各種屬性。在預設情況下,配置檔案存在於:

    jre/lib/logging.properties
    
  • 要想使用另一個配置檔案,就要將java.util.logging.config.file特性設定為配置檔案的存
    儲位置,並用下列命令啟動應用程式:

    java -Djava.util.logging.config.file=configFile MainClass
    
    
  • 要想修改預設的日誌記錄級別,就需要編輯配置檔案,並修改以下命令列

    .level=INFO
    
  • 可以通過新增以下內容來指定自己的日誌記錄級別

    //也就是說,在日誌記錄器名後面新增字尾.level。
    com.mycompany.myapp.level=FINE
    
  • 在稍後可以看到,日誌記錄並不將訊息傳送到控制檯上,這是處理器的任務。另外,
    理器也有級別。要想在控制檯上看到FINE級別的訊息,就需要進行下列設定

    java.util.logging.ConsoleHandler.level=FINE
    
7.5.4 本地化
  • 本地化的應用程式包含資源包(resource bundle)中的本地特定資訊。資源包由各個地區
    (如美國或德國)的對映集合組成。

    • 例如,某個資源包可能將字串“readingFile”對映成英文的“Reading file”或者德文的“Achtung! Datei wird eingelesen”.
  • 一個程式可以包含多個資源包,一個用於選單;其他用於日誌訊息。每個資源包都有一
    個名字(如com.mycompany.logmessages)。

    • 要想將對映新增到一個資源包中,需要為每個地區建立一個檔案。英文訊息對映位於com/mycompany/logmessages_en.properties檔案中;德文訊息對映位於com/mycompany/logmessages_de.properties檔案中。( en和 de是語言編碼)。
      可以將這些檔案與應用程式的類檔案放在一起,以便ResourceBundle類自動地對它們進行定位。這些檔案都是純文字檔案,其組成如下所示:

      readingFile=Achtung!Datei wird eingelesen
      renamingFile=Datei wird umbenannt
      
    • 在請求日誌記錄器時,可以指定一個資源包:

    Logger logger = Logger.getLogger(loggerName,"com.mycompany.logmessages");
    

    然後,為日誌訊息指定資源包的關鍵字,而不是實際的日誌訊息字串。

    logger.info("readingFile");
    

    通常需要在本地化的訊息中增加一些引數,因此,訊息應該包括佔位符{0}、{1}等。例如,要想在日誌訊息中包含檔名,就應該用下列方式包括佔位符:

    Reading file {0}.
    Achtung! Datei {0} wird eingelesen.
    

    然後,通過呼叫下面的一個方法向佔位符傳遞具體的值:

    logger.log(Level.INFO,"readingFile",fileName);
    logger.log(Level.INFO,"renamingFile",new Object[] { oldName,newName };
    
7.5.5 處理器
  • 在預設情況下,日誌記錄器將記錄傳送到ConsoleHandler中,並由它輸出到System.err
    流中。特別是,日誌記錄器還會將記錄傳送到父處理器中,而最終的處理器有一個ConsoleHandler。

  • 與日誌記錄器一樣,處理器也有日誌記錄級別。對於一個要被記錄的日誌記錄,它的日
    志記錄級別必須高於日誌記錄器和處理器的閾值。日誌管理器配置檔案設定的預設控制檯處理器的日誌記錄級別為

    java.util.logging.ConsoleHandler.level=INFO
    

    要想記錄FINE級別的日誌,就必須修改配置檔案中的預設日誌記錄級別和處理器級別。
    另外,還可以繞過配置檔案,安裝自己的處理器。

  • 在預設情況下,日誌記錄器將記錄傳送到自己的處理器和父處理器。我們的日誌記錄
    器是原始日誌記錄器的子類,而原始日誌記錄器將會把所有等於或高於INFO級別的記錄傳送到控制檯。

    • 然而,我們並不想兩次看到這些記錄。鑑於這個原因,應該將useParentHandlers屬性設定為false。
  • 要想將日誌記錄傳送到其他地方,就要新增其他的處理器。日誌API為此提供了兩個很
    有用的處理器,

    • 一個是FileHandler:可以收集檔案中的記錄。

      • 可以像下面這樣直接將記錄傳送到預設檔案的處理器:

        FileHandler handler = new FileHandler();
        logger.addHandler(handler);
        

        這些記錄被髮送到使用者主目錄的javan.log檔案中,n是檔名的唯一編號。
        在預設情況下,記錄被格式化為XML。

    • 另一個是SocketHandler。SocketHandler將記錄傳送到特定的主機和埠。

  • 可以通過設定日誌管理器配置檔案中的不同引數(請參看表7-1),或者利用其他的構造
    器來修改檔案處理器的預設行為。

    也有可能不想使用預設的日誌記錄檔名,因此,應該使用另一種模式,例如,%h/
    myapp.log(有關模式變數的解釋請參看表7-2)。

  • 如果多個應用程式(或者同一個應用程式的多個副本)使用同一個日誌檔案,就應該開
    啟append標誌。另外,應該在檔名模式中使用%u,以便每個應用程式建立日誌的唯一副本。

  • 開啟檔案迴圈功能也是一個不錯的主意。日誌檔案以myapp.log.0,myapp.log.1,myapp.log.2,這種迴圈序列的形式出現。只要檔案超出了大小限制,最舊的檔案就會被刪除,其他的檔案將重新命名,同時建立一個新檔案,其編號為0。

    很多程式設計師將日誌記錄作為輔助文件提供給技術支援員工。如果程式的行為有誤,
    使用者就可以返回檢視日誌檔案以找到錯誤的原因。在這種情況下,應該開啟“append"
    標誌,或使用迴圈日誌,也可以兩個功能同時使用。

  • 還可以通過擴充套件Handler類或StreamHandler類自定義處理器。

7.5.6 過濾器
  • 在預設情況下,過濾器根據日誌記錄的級別進行過濾。每個日誌記錄器和處理器都可以
    有一個可選的過濾器來完成附加的過濾。另外,可以通過實現Filter介面並定義下列方法來
    自定義過濾器。

    boolean isLoggable(LogRecord record)
    

    在這個方法中,可以利用自己喜歡的標準,對日誌記錄進行分析,返回true表示這些記
    錄應該包含在日誌中。例如,某個過濾器可能只對entering方法和exiting方法產生的訊息
    感興趣,這個過濾器可以呼叫record.getMessage()方法,並檢視這個訊息是否用ENTRY或RETURN開頭。

  • 要想將一個過濾器安裝到一個日誌記錄器或處理器中,只需要呼叫setFilter方法就可以
    了。注意,同—時刻最多隻能有一個過濾器。

7.5.7 格式化器
  • ConsoleHandler類和FileHandler類可以生成文字和XML格式的日誌記錄。但是,也可
    以自定義格式。這需要擴充套件Formatter類並覆蓋下面這個方法:

    String format(LogRecord record)
    

    可以根據自己的願望對記錄中的資訊進行格式化,並返回結果字串。在format方法
    中,有可能會呼叫下面這個方法

    String formatMessage(LogRecord record)
    

    這個方法對記錄中的部分訊息進行格式化、引數替換和本地化應用操作。

  • 很多檔案格式(如XML)需要在已格式化的記錄的前後加上一個頭部和尾部。在這個例
    子中,要覆蓋下面兩個方法:

    String getHead(Handler h)
    String getTail(Handler h)
    

    最後,呼叫setFormatter方法將格式化器安裝到處理器中。

7.5.8 日誌記錄說明
  • 為一個簡單的應用程式,選擇一個日誌記錄器,並把日誌記錄器命名為與主應用程
    序包一樣的名字,例如,com.mycompany.myprog,這是一種好的程式設計習慣。另外,可以通過呼叫下列方法得到日誌記錄器。

    Logger logger =Logger.getLogger("com.mycompany.myprog");
    

    為了方便起見,可能希望利用一些日誌操作將下面的靜態域新增到類中:

    private static final Logger logger = Logger.getLogger("com.mycompany.myprog");
    
  • 預設的日誌配置將級別等於或高於INFO級別的所有訊息記錄到控制檯。

  • 所有級別為INFO、WARNING和SEVERE的訊息都將顯示到控制檯上。因此,最好只將對程式使用者有意義的訊息設定為這幾個級別。將程式設計師想要的日誌記錄,設定為FINE是一個很好的選擇。

7.6 除錯技巧

  • 可以用下面的方法列印或記錄任意變數的值:

    System.out.printIn("x="+ x);
    

    Logger.getGlobal().info("x=" + x);
    

    如果x是一個數值,則會被轉換成等價的字串。如果x是一個物件,那麼Java就會調
    用這個物件的toString方法。要想獲得隱式引數物件的狀態,就可以列印this物件的狀態。

  • 日誌代理(logging proxy)是一個子類的物件,它可以截獲方法呼叫,並進行日誌記
    錄,然後呼叫超類中的方法。

  • 利用Throwable類提供的printStackTrace方法,可以從任何一個異常物件中獲得堆疊
    情況。

    • 不一定要通過捕獲異常來生成堆疊軌跡。只要在程式碼的任何位置插入下面這條語句就可以獲得堆疊軌跡:

      Thread.dumpStack();
      
  • 一般來說,堆疊軌跡顯示在System.err上。也可以利用printStackTrace(PrintWriters)
    方法將它傳送到一個檔案中。另外,如果想記錄或顯示堆疊軌跡,就可以採用下面的方式,將它捕獲到一個字串中:

    Stringwriter out = new Stringwriter();
    new Throwable().printStackTrace(new Printwriter(out));
    String description = out.toString();
    
  • 7)通常,將一個程式中的錯誤資訊儲存在一個檔案中是非常有用的。然而,錯誤資訊
    被髮送到System.err中,而不是System.out中。因此,不能夠通過執行下面的語句獲取它們:

    java MyProgram> errors.txt
    

    而是採用下面的方式捕獲錯誤流:

    java MyProgram 2> errors.txt
    

    要想在同一個檔案中同時捕獲System.err和System.out,需要使用下面這條命令

    java MyProgram 1>errors.txt 2>&1
    

    這條命令將工作在bash和Windows shell 中。

  • 讓非捕獲異常的堆疊軌跡出現在System.err中並不是一個很理想的方法。

  • 要想觀察類的載入過程,可以用-verbose標誌啟動Java虛擬機器。

  • -Xlint選項告訴編譯器對一些普遍容易出現的程式碼問題進行檢查。

  • Java虛擬機器增加了對Java應用程式進行監控(monitoring)和管理(management)的支援。它允許利用虛擬機器中的代理裝置跟蹤記憶體消耗、執行緒使用、類載入等情況。

  • 可以使用jmap實用工具獲得一個堆的轉儲,其中顯示了堆中的每個物件。使用命
    令如下:

    jmap -dump:format-b,file=dumpFileName processID
    jhat dumpFileName
    

    然後,通過瀏覽器進入localhost:7000,將會執行一個網路應用程式,藉此探查轉儲物件
    時堆的內容。
    13)如果使用-Xprof標誌執行Java虛擬機器,就會執行一個基本的剖析器來跟蹤那些程式碼中經常被呼叫的方法。剖析資訊將傳送給System.out。輸出結果中還會顯示哪些方法是由
    即時編譯器編譯的。