JDK Logging 深刻分析

日誌輸出是全部系統必備的,不少開發人員可能由於經常使用log4j而忽視了JDK logging模塊,二者之間是否有聯繫?是怎樣的聯繫?JDK logging處理細節是怎麼樣的?java

#0 系列目錄#web

#1 從例子開始# JDK Logging的使用很簡單,以下代碼所示,先使用Logger類的靜態方法getLogger就能夠獲取到一個logger,而後在任何地方均可以經過獲取到的logger進行日誌輸入。好比相似logger.info("Main running.")的調用。apache

package com.bes.logging;

import java.util.logging.Level;
import java.util.logging.Logger;

public class LoggerTest {
      private static Loggerlogger = Logger.getLogger("com.bes.logging");
      public static void main(String argv[]) {
               // Log a FINEtracing message
               logger.info("Main running.");
               logger.fine("doingstuff");
               try {
                         Thread.currentThread().sleep(1000);// do some work
               } catch(Exception ex) {
                         logger.log(Level.WARNING,"trouble sneezing", ex);
               }
               logger.fine("done");
      }
}

不作任何代碼修改和JDK配置修改的話,運行上面的例子,你會發現,控制檯只會出現【Main running.】這一句日誌。以下問題應該呈如今你的大腦裏…架構

  1. 【Main running.】之外的日誌爲何沒有輸出?怎麼讓它們也可以出現?
  2. 日誌中出現的時間、類名、方法名等是從哪裏輸出的?
  3. 爲何日誌就會出如今控制檯?
  4. 大型的系統可能有不少子模塊(可簡單理解爲有不少包名),如何對這些子模塊進行單獨的日誌級別控制?
  5. apache那個流行的log4j項目和JDK的logging有聯繫嗎,怎麼實現本身的LoggerManager?

帶着這些問題,可能你更有興趣瞭解一下JDK的logging機制,本章爲你分析這個簡單模塊的機制。 #2 術語解答# ##2.1 Logger##app

  1. 代碼須要輸入日誌的地方都會用到Logger,這幾乎是一個JDK logging模塊的代言人,咱們經常用Logger.getLogger("com.aaa.bbb");得到一個logger,而後使用logger作日誌的輸出
  2. logger其實只是一個邏輯管理單元,其多數操做都只是做爲一箇中繼者傳遞別的<角色>,好比說:Logger.getLogger(「xxx」)的調用將會依賴於LogManager類,使用logger輸入日誌信息的時候會調用logger中的全部handler進行日誌的輸入
  3. logger是有層次關係的,咱們可通常性的理解爲包名之間的父子繼承關係。每一個logger一般以java包名爲其名稱。子logger一般會從父logger繼承logger級別、handler、ResourceBundle名(與國際化信息有關)等
  4. 整個JVM會存在一個名稱爲空的root logger,全部匿名的logger都會把root logger做爲其父

##2.2 LogManager## LogManager:整個JVM內部全部logger的管理,logger的生成、獲取等操做都依賴於它,也包括配置文件的讀取。LogManager中會有一個Hashtable【private Hashtable<String,WeakReference<Logger>> loggers】用於存儲目前全部的logger,若是須要獲取logger的時候,Hashtable已經有存在logger的話就直接返回Hashtable中的,若是hashtable中沒有logger,則新建一個同時放入Hashtable進行保存。框架

##2.3 Handler## Handler:用來控制日誌輸出的,好比JDK自帶的ConsoleHanlder把輸出流重定向到System.err輸出,每次調用Logger的方法進行輸出時都會調用Handler的publish方法,每一個logger有多個handler。咱們能夠利用handler來把日誌輸入到不一樣的地方(好比文件系統或者是遠程Socket鏈接)。tcp

##2.4 Formatter## Formatter:日誌在真正輸出前須要進行必定的格式化,好比是否輸出時間?時間格式?是否輸入線程名?是否使用國際化信息等都依賴於Formatter。函數

##2.5 Log Level## Log Level:沒必要說,這是作容易理解的一個,也是logging爲何能幫助咱們適應從開發調試到部署上線等不一樣階段對日誌輸出粒度的不一樣需求。JDK Log級別從高到低爲OFF(2^31-1)—>SEVERE(1000)—>WARNING(900)—>INFO(800)—>CONFIG(700)—>FINE(500)—>FINER(400)—>FINEST(300)—>ALL(-2^31),每一個級別分別對應一個數字,輸出日誌時級別的比較就依賴於數字大小的比較。可是須要注意的是:不只是logger具備級別,handler也是有級別,也就是說若是某個logger級別是FINE,客戶但願輸入FINE級別的日誌,若是此時logger對應的handler級別爲INFO,那麼FINE級別日誌仍然是不能輸出的。源碼分析

##2.6 對應關係##this

  1. LogManager與logger是1對多關係,整個JVM運行時只有一個LogManager,且全部的logger均在LogManager中
  2. logger與handler是多對多關係,logger在進行日誌輸出的時候會調用全部的hanlder進行日誌的處理。
  3. handler與formatter是一對一關係,一個handler有一個formatter進行日誌的格式化處理。
  4. 很明顯:logger與level是一對一關係hanlder與level也是一對一關係

#3 Logging配置# JDK默認的logging配置文件爲:$JAVA_HOME/jre/lib/logging.properties,可使用系統屬性java.util.logging.config.file指定相應的配置文件對默認的配置文件進行覆蓋,配置文件中一般包含如下幾部分定義:

  1. handlers:用逗號分隔每一個Handler,這些handler將會被加到root logger中。也就是說即便咱們不給其餘logger配置handler屬性,在輸出日誌的時候logger會一直找到root logger,從而找到handler進行日誌的輸入。

  2. level是root logger的日誌級別。

  3. <handler>.xxx是配置具體某個handler的屬性,好比java.util.logging.ConsoleHandler.formatter即是爲ConsoleHandler配置相應的日誌Formatter。

  4. logger的配置,全部以[.level]結尾的屬性皆被認爲是對某個logger的級別的定義,如com.bes.server.level=FINE是給名爲[com.bes.server]的logger定義級別爲FINE。順便說下,前邊提到過logger的繼承關係,若是還有com.bes.server.webcontainer這個logger,且在配置文件中沒有定義該logger的任何屬性,那麼其將會從[com.bes.server]這個logger進行屬性繼承。除了級別以外,還能夠爲logger定義handler和useParentHandlers(默認是爲true)屬性,如com.bes.server.handler=com.bes.test.ServerFileHandler(須要是一個extends java.util.logging.Handler的類),com.bes.server.useParentHandlers=false(意味着com.bes.server這個logger進行日誌輸出時,日誌僅僅被處理一次,用本身的handler輸出,不會傳遞到父logger的handler)。如下是JDK配置文件示例:

handlers= java.util.logging.FileHandler,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
sun.rmi.transport.tcp.logLevel = FINE;

#4 Logging執行原理# ##4.1 Logger的獲取##

  1. 首先是調用Logger的以下方法得到一個logger:
public static synchronized Logger getLogger(String name) {
        LogManager manager =LogManager.getLogManager();
        return manager.demandLogger(name);
    }
  1. 上面的調用會觸發java.util.logging.LoggerManager的類初始化工做,LoggerManager有一個靜態化初始化塊(這是會先於LoggerManager的構造函數調用的~_~):
static {  
    AccessController.doPrivileged(newPrivilegedAction<Object>() {  
        public Object run() {  
            String cname =null;  
            try {  
                cname =System.getProperty("java.util.logging.manager");  
                if (cname !=null) {  
                    try {  
                        Class clz =ClassLoader.getSystemClassLoader().loadClass(cname);  
                        manager= (LogManager) clz.newInstance();  
                    } catch(ClassNotFoundException ex) {  
                        Class clz =Thread.currentThread().getContextClassLoader().loadClass(cname);  
                        manager= (LogManager) clz.newInstance();  
                    }  
                }  
            } catch (Exceptionex) {  
                System.err.println("Could not load Logmanager \"" + cname+ "\"");  
                ex.printStackTrace();  
            }  
            if (manager ==null) {
                manager = newLogManager();
            }  
            manager.rootLogger= manager.new RootLogger();
            manager.addLogger(manager.rootLogger);
            Logger.global.setLogManager(manager);
            manager.addLogger(Logger.global);
            return null;  
        }  
    });  
}

從靜態初始化塊中能夠看出LoggerManager是可使用系統屬性java.util.logging.manager指定一個繼承自java.util.logging.LoggerManager的類進行替換的,好比Tomcat啓動腳本中就使用該機制以使用本身的LoggerManager。

不論是JDK默認的java.util.logging.LoggerManager仍是自定義的LoggerManager,初始化工做中均會給LoggerManager添加兩個logger,一個是名稱爲」」的root logger,且logger級別設置爲默認的INFO;另外一個是名稱爲global的全局logger,級別仍然爲INFO

LogManager」類」初始化完成以後就會讀取配置文件(默認爲$JAVA_HOME/jre/lib/logging.properties),把配置文件的屬性名的屬性值這樣的鍵值對保存在內存中,方便以後初始化logger的時候使用。

  1. 1步驟中Logger類發起的getLogger操做將會調用java.util.logging.LoggerManager的以下方法:
Logger demandLogger(String name) {
        Logger result =getLogger(name);
        if (result == null) {
            result = newLogger(name, null);
            addLogger(result);
            result =getLogger(name);
        }
        return result;
    }

能夠看出,LoggerManager首先從現有的logger列表中查找,若是找不到的話,會新建一個looger並加入到列表中。固然很重要的是新建looger以後須要對logger進行初始化,這個初始化詳見java.util.logging.LoggerManager#addLogger()方法中,改方法會根據配置文件設置logger的級別以及給logger添加handler等操做。

到此爲止logger已經獲取到了,你同時也須要知道此時你的logger中已經有級別、handler等重要信息,下面將分析輸出日誌時的邏輯。

##4.2 日誌的輸出## 首先咱們一般會調用Logger類下面的方法,傳入日誌級別以及日誌內容。

public void log(Levellevel, String msg) {
        if (level.intValue() < levelValue ||levelValue == offValue) {
            return;
        }
        LogRecord lr = new LogRecord(level, msg);
        doLog(lr);
    }

該方法能夠看出,Logger類首先是進行級別的校驗,若是級別校驗經過,則會新建一個LogRecord對象,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);
    }

doLog(LogRecord lr)方法中設置了ResourceBundle信息(這個與國際化有關)以後便直接調用log(LogRecord record)方法

public void log(LogRecord record) {
        if (record.getLevel().intValue() <levelValue || levelValue == offValue) {
            return;
        }
        synchronized (this) {
            if (filter != null &&!filter.isLoggable(record)) {
                return;
            }
        }
        Logger logger = this;
        while (logger != null) {
            Handler targets[] = logger.getHandlers();
            if(targets != null) {
                for (int i = 0; i < targets.length; i++) {
                    targets[i].publish(record);
                }
            }
            if(!logger.getUseParentHandlers()) {
                break;
            }
            logger= logger.getParent();
        }
    }

很清晰,while循環是重中之重,首先從logger中獲取handler,而後分別調用handler的publish(LogRecordrecord)方法。while循環證實了前面提到的會一直把日誌委託給父logger處理的說法,固然也證實了可使用logger的useParentHandlers屬性控制日誌不進行往上層logger傳遞的說法。到此爲止logger對日誌的控制差很少算是完成,接下來的工做就是看handler的了,這裏咱們以java.util.logging.ConsoleHandler爲例說明日誌的輸出。

public class ConsoleHandler extends StreamHandler {
        public ConsoleHandler() {
            sealed = false;
            configure();
            setOutputStream(System.err);
            sealed = true;
        }
    }

ConsoleHandler構造函數中除了須要調用自身的configure()方法進行級別、filter、formatter等的設置以外,最重要的咱們最關心的是setOutputStream(System.err)這一句,把系統錯誤流做爲其輸出。而ConsoleHandler的publish(LogRecordrecord)是繼承自java.util.logging.StreamHandler的,以下所示:

public synchronized void publish(LogRecord record) {
        if(!isLoggable(record)) {
            return;
        }
        String msg;
        try {
            msg =getFormatter().format(record);
        } catch (Exception ex){
            // We don't want tothrow an exception here, but we
            // report theexception to any registered ErrorManager.
            reportError(null,ex, ErrorManager.FORMAT_FAILURE);
            return;
        }       
        try {
            if (!doneHeader) {
                writer.write(getFormatter().getHead(this));
                doneHeader =true;
            }
            writer.write(msg);
        } catch (Exception ex){
            // We don't want tothrow an exception here, but we
            // report theexception to any registered ErrorManager.
            reportError(null,ex, ErrorManager.WRITE_FAILURE);
        }
    }

方法邏輯也很清晰,首先是調用Formatter對消息進行格式化,說明一下:格式化實際上是進行國際化處理的重要契機。而後直接把消息輸出到對應的輸出流中。須要注意的是handler也會用本身的level和LogRecord中的level進行比較,看是否真正輸出日誌。

#5 總結# 至此,整個日誌輸出過程已經分析完成。細心的讀者應該能夠解答以下四個問題了。

  1. 【Main running.】之外的日誌爲何沒有輸出?怎麼讓它們也可以出現?

這就是JDK默認的logging.properties文件中配置的handler級別和跟級別均爲info致使的,若是但願看到FINE級別日誌,須要修改logging.properties文件,同時進行以下兩個修改

java.util.logging.ConsoleHandler.level= FINE//修改
com.bes.logging.level=FINE//添加
  1. 日誌中出現的時間、類名、方法名等是從哪裏輸出的?

請參照[java.util.logging.ConsoleHandler.formatter= java.util.logging.SimpleFormatter]配置中指定的java.util.logging.SimpleFormatter類,其public synchronized String format(LogRecord record) 方法說明了一切。

public synchronized String format(LogRecord record) {
        StringBuffer sb = new StringBuffer();
        // Minimize memory allocations here.
        dat.setTime(record.getMillis());
        args[0] = dat;
        StringBuffer text = new StringBuffer();
        if (formatter == null) {
            formatter = new MessageFormat(format);
        }
        formatter.format(args, text, null);
        sb.append(text);
        sb.append(" ");
        if (record.getSourceClassName() != null) {     
            sb.append(record.getSourceClassName());
        } else {
            sb.append(record.getLoggerName());
        }
        if (record.getSourceMethodName() != null) {
            sb.append(" ");
            sb.append(record.getSourceMethodName());
        }
        sb.append(lineSeparator);
        String message = formatMessage(record);
        sb.append(record.getLevel().getLocalizedName());
        sb.append(": ");
        sb.append(message);
        sb.append(lineSeparator);
        if (record.getThrown() != null) {
            try {
                StringWriter sw = newStringWriter();
                PrintWriter pw = newPrintWriter(sw);
                record.getThrown().printStackTrace(pw);
                pw.close();
                sb.append(sw.toString());
            } catch (Exception ex) {
            }
        }
        return sb.toString();
    }
  1. 爲何日誌就會出如今控制檯?

看到java.util.logging.ConsoleHandler 類構造方法中的[setOutputStream(System.err)]語句,相信你已經明白。

  1. 大型的系統可能有不少子模塊(可簡單理解爲有不少包名),如何對這些子模塊進行單獨的日誌級別控制?

在logging.properties文件中分別對各個logger的級別進行定義,且最好使用java.util.logging.config.file屬性指定本身的配置文件。

  1. 擴充:apache那個流行的log4j項目和JDK的logging有聯繫嗎,怎麼實現本身的LoggerManager?

沒聯繫,兩個都是日誌框架具體實現,二者的LoggerManager實現邏輯大致一致,只是具體細節不一樣。Log4j相關內容,具體可參見Log4j架構分析與實戰 & slf4j + log4j原理實現及源碼分析

相關文章
相關標籤/搜索