日誌輸出是全部系統必備的,不少開發人員可能由於經常使用log4j而忽視了JDK logging模塊,二者之間是否有聯繫?是怎樣的聯繫?JDK logging處理細節是怎麼樣的?本週拋磚引玉,先分析JDK logging機制。 java
JDK Logging的使用很簡單,以下代碼所示,先使用Logger類的靜態方法getLogger就能夠獲取到一個logger,而後在任何地方均可以經過獲取到的logger進行日誌輸入。好比相似logger.info("Main running.")的調用。web
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.】之外的日誌爲何沒有輸出?怎麼讓它們也可以出現?shell
2,日誌中出現的時間、類名、方法名等是從哪裏輸出的?apache
3,爲何日誌就會出如今控制檯?app
4,大型的系統可能有不少子模塊(可簡單理解爲有不少包名),如何對這些子模塊進行單獨的日誌級別控制?tcp
5,擴充:apache那個流行的log4j項目和JDK的logging有聯繫嗎,怎麼實現本身的LoggerManager?函數
帶着這些問題,可能你更有興趣瞭解一下JDK的logging機制,本章爲你分析這個簡單模塊的機制。this
a) 代碼須要輸入日誌的地方都會用到Logger,這幾乎是一個JDK logging模塊的代言人,咱們經常用Logger.getLogger("com.aaa.bbb");得到一個logger,而後使用logger作日誌的輸出。spa
b) logger其實只是一個邏輯管理單元,其多數操做都只是做爲一箇中繼者傳遞別的<角色>,好比說:Logger.getLogger(「xxx」)的調用將會依賴於LogManager類,使用logger輸入日誌信息的時候會調用logger中的全部handler進行日誌的輸入。線程
c) logger是有層次關係的,咱們可通常性的理解爲包名之間的父子繼承關係。每一個logger一般以java包名爲其名稱。子logger一般會從父logger繼承logger級別、handler、ResourceBundle名(與國際化信息有關)等。
d) 整個JVM會存在一個名稱爲空的root logger,全部匿名的logger都會把root logger做爲其父
總結對應關係:
JDK默認的logging配置文件爲:$JAVA_HOME/jre/lib/logging.properties,可使用系統屬性java.util.logging.config.file指定相應的配置文件對默認的配置文件進行覆蓋,配置文件中一般包含如下幾部分定義:
# com.bes.test.ServerFileHandler 須要繼承 java.util.logging.Handler類 com.bes.server.handler=com.bes.test.ServerFileHandler # 意味着com.bes.server這個logger進行日誌輸出時,日誌僅僅被處理一次,用本身的handler輸出,不會傳遞到父logger的handler com.bes.server.useParentHandlers=false
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
首先是調用Logger的以下方法得到一個logger
上面的調用會觸發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的時候使用。
Logge.getLogger將會調用java.util.logging.LoggerManager的以下方法:
能夠看出,LoggerManager首先從現有的logger列表中查找,若是找不到的話,會新建一個looger並加入到列表中。固然很重要的是新建looger以後須要對logger進行初始化,這個初始化詳見java.util.logging.LoggerManager#addLogger()方法中,該方法會根據配置文件設置logger的級別以及給logger添加handler等操做。
到此爲止logger已經獲取到了,你同時也須要知道此時你的logger中已經有級別、handler等重要信息,下面將分析輸出日誌時的邏輯。
首先咱們一般會調用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)方法:
doLog(LogRecord lr)方法中設置了ResourceBundle信息(這個與國際化有關)以後便直接調用log(LogRecord record) 方法 :
很清晰,while循環是重中之重,首先從logger中獲取handler,而後分別調用handler的publish(LogRecordrecord)方法。while循環證實了前面提到的會一直把日誌委託給父logger處理的說法,固然也證實了可使用logger的useParentHandlers屬性控制日誌不進行往上層logger傳遞的說法。到此爲止logger對日誌的控制差很少算是完成,接下來的工做就是看handler的了,這裏咱們以java.util.logging.ConsoleHandler爲例說明日誌的輸出:
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進行比較,看是否真正輸出日誌。
至此,整個日誌輸出過程已經分析完成。細心的讀者應該能夠解答以下四個問題了。
1,【Main running.】之外的日誌爲何沒有輸出?怎麼讓它們也可以出現?
這就是JDK默認的logging.properties文件中配置的handler級別和跟級別均爲info致使的,若是但願看到FINE級別日誌,須要修改logging.properties文件,同時進行以下兩個修改
java.util.logging.ConsoleHandler.level= FINE//修改
com.bes.logging.level=FINE//添加
2,日誌中出現的時間、類名、方法名等是從哪裏輸出的?
請參照 java.util.logging.ConsoleHandler.formatter= java.util.logging.SimpleFormatter 配置中指定的java.util.logging.SimpleFormatter類,其publicsynchronized 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(); }
3,爲何日誌就會出如今控制檯?
看到java.util.logging.ConsoleHandler 類構造方法中的[setOutputStream(System.err)]語句,相信你已經明白。
4,大型的系統可能有不少子模塊(可簡單理解爲有不少包名),如何對這些子模塊進行單獨的日誌級別控制?
在logging.properties文件中分別對各個logger的級別進行定義,且最好使用java.util.logging.config.file屬性指定本身的配置文件。