Java 日誌系列篇一 原生 Java.util.logging

本文網大多網絡整理所得,出處太多,不一一列舉java

簡介

Java 中的 Logging API 讓 Java 應用能夠記錄不一樣級別的信息,它在debug過程當中很是有用,若是系統由於各類各樣的緣由而崩潰,崩潰緣由能夠在日誌中清晰地追溯,下面讓咱們來看看 Java 原生的 Logging 功能。
從1.4.2開始,Java 經過 Java.util.logging 包爲應用程序提供了記錄消息的可能,在 API 中的核心類爲 Logger 類。理解在記錄消息中的日誌的不一樣級別是很是重要的。Java 爲此定時了8個級別,它們是分別SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST 以及 ALL. 它們按照優先級降序排列,在應用運行的任什麼時候間點,日誌級別能夠被更改。
一般來講,當爲 Logger 指定了一個 Level, 該 Logger 會包含當前指定級別以及更高級別的日誌。舉例而言,若是 Level 被設置成了 WARNING, 全部的 warning 消息以及 SERVER 消息會被記錄。應用能夠用下列方法記錄日誌:Logger.warning(), Logger.info(), Logger.config() ...node

工做原理和日誌處理流程

幾個重要類的說明

  • Logger 對外發布的日誌記錄器,應用系統能夠經過該對象完成日誌記錄的功能數據庫

  • Level 日誌的記錄級別緩存

  • LoggingMXBean 接口對象,對外發布的日誌管理器網絡

  • LogRecord 日誌信息描述對象框架

  • LoggerManager 日誌管理器函數

  • Filter 日誌過濾器,接口對象,在日誌被 Handler 處理以前,起過濾做用this

  • Handler 日誌處理器,接口對象,決定日誌的輸出方式編碼

  • Formatter 日誌格式化轉換器,接口對象,決定日誌的輸出格式spa

工做原理

首先經過LoggerManager進行日誌框架的初始化,生成Logger的根節點RootLogger. 這裏須要注意的是LoggerManager的初始化工做,並無將構建配置文件中全部的日誌對象,而僅僅是構建了根節點,這種方式就是咱們多例模式中常常用到的懶加載,對象只有在真正被時候的時候,再進行構建。 
經過Logger.getLogger(String name) 獲取一個已有的Logger對象或者是新建一個Logger對象。Logger,日誌記錄器,這就是在應用程序中須要調用的對象了,經過Logger對象的一系列log方法,

Logger的大體處理流程

圖片描述
收到應用程序的記錄請求,將參數中的日誌信息和運行時的信息構建出LogRecord對象,然後經過Logger對象自己設置的記錄級別和調用者傳遞進來的日誌級別,若是傳遞進來的日誌級別低於Logger對象自己設置的記錄級別(從語義上的理解,而實際上語義級別越高的級別其內部用數字表示的標誌的數值越小),那麼Logger對象將直接返回,由於他認爲這條日誌信息,在當前運行環境中,沒有必要記錄。 
 而知足以上條件的日誌信息,將會經過Logger對象的filter元素的過濾校驗,filter是動態的,在運行時是能夠隨意設置的,若是有filter對象,那麼將調用filter對象,對日誌對象LogRecord進行校驗,只有校驗經過的LogRecord對象,纔會繼續往下執行。 
 經過filter校驗後,Logger對象將依次調用其配置的處理器,經過處理器來真正實現日誌的記錄功能,一個Logger對象能夠配置多個處理器handler,因此一條日誌記錄能夠被多個處理器處理,同時Logger對象的實現是樹形結構,若是Logger對象設置其能夠繼承其父節點的處理器(默認),一條日誌記錄還會被其父節點的Logger對象處理。  而handler的處理方式就會是形形色色了,可是歸根節點,會有如下幾個大的步驟: 
1. 級別的斷定和比較,決定某條具體的日誌記錄是否應該繼續處理 
2. 將日誌記錄作格式化處理,以達到輸出的日誌在格式上統一,美觀,可讀性高。 3. 資源的釋放,不論是以何種方式記錄日誌,老是會消耗一些方面的資源,因此
會涉及到資源的釋放問題。好比以文件方式記錄的日誌的,在必定的時候須要作文件關閉操做,以報文方式發送日誌的,在和遠程通話的過程當中,也須要涉及到網絡IO的關閉操做,或者是存儲在數據庫等等,資源釋放在程序開發過程當中,是個不變的主題。

從一個示例講起

public class TestLogger {
    public static void main(String[] args) {
        Logger log = Logger.getLogger("lavasoft");
        log.info("aaa");
    }
}

console output:
>>> aaa

以上簡單的代碼背後發生那些事

  1. LoggerManager 將會返回一個新的或者已經存在的同名的 Logger , 首先會查找是否有同名 Logger 被 namedLoggers 維護有則返回, 可是在咱們這個示例中大可能是從新生成一個 Logger,首先 LoggerManager 會讀取系統配置,設定一個默認的的 INFO 級別的 Logger, 而後也許跟其餘線程搶到一個 Logger 後返回
    tips:
    默認的Java日誌框架將其配置存儲到一個名爲 logging.properties 的文件中。
    在這個文件中,每行是一個配置項,配置項使用點標記(dot notation)的形式。
    Java在其安裝目錄的lib文件夾下面安裝了一個全局配置文件,但在啓動一個Java程序時,
    你能夠經過指定 java.util.logging.config.file 屬性的方式來使用一個單獨的日誌配置文件,
    一樣也能夠在我的項目中建立和存儲 logging.properties 文件。

    Logger 中召喚 LoggerManager 片斷
    ---------------------------
    
    public static Logger getLogger(String name) {
        LogManager manager = LogManager.getLogManager();
        return manager.demandLogger(name);
    }
    
    
    LoggerManager 中 產生 Logger 的片斷
    -----------------------------
    
    Logger demandLogger(String name) {
        Logger result = getLogger(name);
        if (result == null) {
            Logger newLogger = new Logger(name, null);
            do {
                if (addLogger(newLogger)) {
                    return newLogger;
                }
                result = getLogger(name);
            } while (result == null);
        }
        return result;
    }
    
    
    
    LoggerManager 中維護了一個有繼承關係的含有弱引用的 LoggerWeakRef
    -------------------------------
    private Hashtable<String,LoggerWeakRef> namedLoggers = new Hashtable<>();
    
    LoggerWeakRef 類結構
    -----------------
    
    final class LoggerWeakRef extends WeakReference<Logger> {
        private String                name;       // for namedLoggers cleanup
        private LogNode               node;       // for loggerRef cleanup
        private WeakReference<Logger> parentRef;  // for kids cleanup
    
    以上二者維護了JVM中弱引用的 Loggers 父子結構
  2. log.info()

    Logger 中的 info(String msg) 方法
    -----------------------------
    
       public void info(String msg) {
     if (Level.INFO.intValue() < levelValue) {
         return;
     }
     log(Level.INFO, msg);
       }
       
       上面說過默認 LoggerManager 產生的 Logger 日誌級別默認爲 INFO ,因此這裏默認的
       levelValue 爲 Level.INFO.intValue()
       
       若是這裏 Level.INFO.intValue() 低於 levelValue 的 , 將 do nothing
       
    
    調用 log(Level level, String msg) 方法
    ----------------------------------
    
       public void log(Level level, String msg) {
     if (level.intValue() < levelValue || levelValue == offValue) {
         return;
     }
     LogRecord lr = new LogRecord(level, msg);
     doLog(lr);
       }
       上面的 log.info 方法只是 log(Level level, String msg) 方法簡單封裝,在這裏日誌級別
       爲 Level.OFF.intValue() 也 do nothing  了,不然建立真正的 LogRecord 對象
       
    
    調用 doLog(LogRecord lr) 方法
    -------------------------
       private void doLog(LogRecord lr) {
     lr.setLoggerName(name);
     String ebname = getEffectiveResourceBundleName();
     if (ebname != null) {
         lr.setResourceBundleName(ebname);
         lr.setResourceBundle(findResourceBundle(ebname));
     }
     log(lr);
       }
       
       getEffectiveResourceBundleName() 將一直上溯查找有效的 resourceBundleName , 有可能返回 null
       
    
    調用 log(LogRecord lr) 方法
    -----------------------
      public void log(LogRecord record) {
     if (record.getLevel().intValue() < levelValue || levelValue == offValue) {
         return;
     }
     Filter theFilter = filter;
     if (theFilter != null && !theFilter.isLoggable(record)) {
         return;
     }
    
     // Post the LogRecord to all our Handlers, and then to
     // our parents' handlers, all the way up the tree.
    
     Logger logger = this;
     while (logger != null) {
         for (Handler handler : logger.getHandlers()) {
             handler.publish(record);
         }
    
         if (!logger.getUseParentHandlers()) {
             break;
         }
    
         logger = logger.getParent();
     }
       }
     
       在這裏咱們能夠看到了 Filter 與 Handler 的出現,咱們可使用 setFilter(Filter newFilter)
       與 addHandler(Handler handler) 來爲 Logger 添加 Filter 與 Handler
       這裏咱們能夠看出在 while 循環中會先對當前全部 handler 輸出,在上溯全部父 Logger 全部 Handler
       輸出,至此兩句代碼解析結束。

話說 Filter

做爲一個接口, Filter:爲所記錄的日誌提供日誌級別控制之外的細粒度控制。

public interface Filter {

    /**
     * Check if a given log record should be published.
     * @param record  a LogRecord
     * @return true if the log record should be published.
     */
    public boolean isLoggable(LogRecord record);

}
咱們能夠實現一個 Filter 接口的的對象來使用,下面是示例代碼

public class MyFilter implements Filter {
    public boolean isLoggable(LogRecord record) {
        // TODO: 在這裏咱們能夠添加本身的一些邏輯進去
        return false;
        // 返回 false 則不被記錄日誌, true 則被記錄日誌
    }
}

而後咱們爲 Logger 對象設定 Filter 對象

Filter filter = new MyFilter();
logger1.setFilter(filter); 

或者咱們也能夠爲 Handler 對象設定 Filter 對象

Filter filter = new MyFilter();
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(Level.ALL);
consoleHandler.setFilter(filter);

話說 Handler

先上一張 java.util.logging 包中有關 Handler 的類圖
圖片描述
Handler負責從Logger中取出日誌消息並將消息發送出去,好比發送到控制檯、文件、網絡上的其餘日誌服務或操做系統日誌等。
Handler也具備級別概念,用於判斷當前Logger中的消息是否應該被髮送出去,可使用定義好的各類日誌級別(如Level.OFF表示關閉等)。
除了級別概念,一個Handler還能夠具備本身的過濾器(Filter)、格式化器(Formatter)、錯誤管理器(ErrorManager)以及編碼字符集等,這些屬性藉助LogManager中的配置信息進行設置。
Handler是一個抽象類,須要根據實際狀況建立真正使用的具體Handler(如ConsoleHandler、FileHandler等),實現各自的publish、flush以及close等方法。
對幾種具體實現 Handler 類的類作簡單說明

  1. MemoryHandler,將當前日誌信息寫入內存緩衝區中同時丟棄緩存中之前的內容。將內存緩衝區中的信息轉發至另外一個Handler

  2. StreamHandler全部基於I/O流的Handler的基類,將日誌信息發送至給定的java.io.OutputStream中

  3. ConsoleHandler,將消息發送至System.err(而非System.out),默認配置與其父類StreamHandler相同。

  4. FileHandler,將消息發送至單個通常文件或一個可回滾的文件集合。可回滾文件集中的文件依據文件大小進行回滾,久文件名稱經過當前文件名附加編號0、一、2等方式依次進行標示。默認狀況下日誌信息都存放在I/O緩衝中,但若是一條完整的日誌信息會觸發清空緩衝的動做。與其父類StramHandler不一樣的是,FileHandler的默認格式器是java.util.logging.XMLFormatter:

  5. SocketHandler,負責將日誌信息發送至網絡,默認狀況下也採用java.util.logging.XMLFormatter格式。

關於 MemoryHandler

MemoryHandler 使用了典型的「註冊 - 通知」的觀察者模式。MemoryHandler 先註冊到對本身感興趣的 Logger 中(logger.addHandler(handler)),在這些 Logger 調用發佈日誌的 API:log()、logp()、logrb() 等,遍歷這些 Logger 下綁定的全部 Handlers 時,通知觸發自身 publish(LogRecord)方法的調用,將日誌寫入 buffer,當轉儲到下一個日誌發佈平臺的條件成立,轉儲日誌並清空 buffer。

這裏的 buffer 是 MemoryHandler 自身維護一個可自定義大小的循環緩衝隊列,來保存全部運行時觸發的 Exception 日誌條目。同時在構造函數中要求指定一個 Target Handler,用於承接輸出;在知足特定 flush buffer 的條件下,如日誌條目等級高於 MemoryHandler 設定的 push level 等級(實例中定義爲 SEVERE)等,將日誌移交至下一步輸出平臺。從而造成以下日誌轉儲輸出鏈:
圖片描述

MemoryHandler 使用方式

以上是記錄產品 Exception 錯誤日誌,以及如何轉儲的 MemoryHandler 處理的內部細節;接下來給出 MemoryHandler 的一些使用方式。

  1. 直接使用 java.util.logging 中的 MemoryHandler

    // 在 buffer 中維護 5 條日誌信息
    // 僅記錄 Level 大於等於 Warning 的日誌條目並
    // 刷新 buffer 中的日誌條目到 fileHandler 中處理
    int bufferSize = 5; 
    f = new FileHandler("testMemoryHandler.log"); 
    m = new MemoryHandler(f, bufferSize, Level.WARNING); 
    myLogger = Logger.getLogger("com.ibm.test"); 
    myLogger.addHandler(m); 
    myLogger.log(Level.WARNING, 「this is a WARNING log」);
  2. 自定義(反射)
    思考自定義 MyHandler 繼承自 MemoryHandler 的場景,因爲沒法直接使用做爲父類私有屬性的 size、buffer 及 buffer 中的 cursor,若是在 MyHandler 中有獲取和改變這些屬性的需求,一個途徑是使用反射。清單 5 展現了使用反射讀取用戶配置並設置私有屬性。

    int m_size; 
      String sizeString = manager.getProperty(loggerName + ".size"); 
      if (null != sizeString) { 
             try { 
              m_size = Integer.parseInt(sizeString); 
              if (m_size <= 0) { 
                 m_size = BUFFER_SIZE; // default 1000 
              } 
     // 經過 java 反射機制獲取私有屬性
              Field f; 
              f = getClass().getSuperclass().getDeclaredField("size"); 
              f.setAccessible(true); 
              f.setInt(this, m_size); 
              f = getClass().getSuperclass().getDeclaredField("buffer"); 
              f.setAccessible(true); 
              f.set(this, new LogRecord[m_size]); 
             } catch (Exception e) { 
             } 
      }
  3. 自定義(重寫)
    直接使用反射方便快捷,適用於對父類私有屬性無頻繁訪問的場景。思考這樣一種場景,默認環形隊列沒法知足咱們存儲需求,此時不妨令自定義的 MyMemoryHandler 直接繼承 Handler,直接對存儲結構進行操做,能夠經過清單 6 實現。

    public class MyMemoryHandler extends Handler{ 
      // 默認存儲 LogRecord 的緩衝區容量
      private static final int DEFAULT_SIZE = 1000; 
      // 設置緩衝區大小
      private int size = DEFAULT_SIZE; 
      // 設置緩衝區
      private LogRecord[] buffer; 
      // 參考 java.util.logging.MemoryHandler 實現其它部分
      ... 
     }

logging.properties 文件

默認的 logging.properties 存放在 jre/lib/logging.properties,截取有效的配置項

handlers= java.util.logging.ConsoleHandler
.level= INFO
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
com.xyz.foo.level = SEVERE
相關文章
相關標籤/搜索