java底層JDK?Logging日志模塊處理細(xì)節(jié)深入分析
日志輸出是所有系統(tǒng)必備的,很多開(kāi)發(fā)人員可能因?yàn)槌3J褂胠og4j而忽視了JDK logging模塊,兩者之間是否有聯(lián)系?是怎樣的聯(lián)系?JDK logging處理細(xì)節(jié)是怎么樣的?本周拋磚引玉,先分析JDK logging機(jī)制。
從例子開(kāi)始
JDK Logging的使用很簡(jiǎn)單,如下代碼所示,先使用Logger類(lèi)的靜態(tài)方法getLogger就可以獲取到一個(gè)logger,然后在任何地方都可以通過(guò)獲取到的logger進(jìn)行日志輸入。
比如類(lèi)似logger.info("Main running.")的調(diào)用。
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配置修改的話,運(yùn)行上面的例子,你會(huì)發(fā)現(xiàn),控制臺(tái)只會(huì)出現(xiàn)【Main running.】這一句日志。如下問(wèn)題應(yīng)該呈現(xiàn)在你的大腦里…
1,【Main running.】以外的日志為什么沒(méi)有輸出?怎么讓它們也能夠出現(xiàn)?
2,日志中出現(xiàn)的時(shí)間、類(lèi)名、方法名等是從哪里輸出的?
3,為什么日志就會(huì)出現(xiàn)在控制臺(tái)?
4,大型的系統(tǒng)可能有很多子模塊(可簡(jiǎn)單理解為有很多包名),如何對(duì)這些子模塊進(jìn)行單獨(dú)的日志級(jí)別控制?
5,擴(kuò)充:apache那個(gè)流行的log4j項(xiàng)目和JDK的logging有聯(lián)系嗎,怎么實(shí)現(xiàn)自己的LoggerManager?
帶著這些問(wèn)題,可能你更有興趣了解一下JDK的logging機(jī)制,本章為你分析這個(gè)簡(jiǎn)單模塊的機(jī)制。
術(shù)語(yǔ)解答
在深入分析之前,需要掌握以下術(shù)語(yǔ)
logger: 對(duì)于logger,需要知道其下幾個(gè)方面
1,代碼需要輸入日志的地方都會(huì)用到Logger,這幾乎是一個(gè)JDK logging模塊的代言人,我們常常用Logger.getLogger("com.aaa.bbb");獲得一個(gè)logger,然后使用logger做日志的輸出。
2,logger其實(shí)只是一個(gè)邏輯管理單元,其多數(shù)操作都只是作為一個(gè)中繼者傳遞別的<角色>,比如說(shuō):Logger.getLogger(“xxx”)的調(diào)用將會(huì)依賴(lài)于LogManager類(lèi),使用logger輸入日志信息的時(shí)候會(huì)調(diào)用logger中的所有handler進(jìn)行日志的輸入。
3,logger是有層次關(guān)系的,我們可一般性的理解為包名之間的父子繼承關(guān)系。每個(gè)logger通常以java包名為其名稱(chēng)。子logger通常會(huì)從父logger繼承l(wèi)ogger級(jí)別、handler、ResourceBundle名(與國(guó)際化信息有關(guān))等。
4,整個(gè)JVM會(huì)存在一個(gè)名稱(chēng)為空的root logger,所有匿名的logger都會(huì)把root logger作為其父
LogManager
:整個(gè)JVM內(nèi)部所有l(wèi)ogger的管理,logger的生成、獲取等操作都依賴(lài)于它,也包括配置文件的讀取。LogManager中會(huì)有一個(gè)Hashtable
private Hashtable<String,WeakReference<Logger>> loggers
用于存儲(chǔ)目前所有的logger,如果需要獲取logger的時(shí)候,Hashtable已經(jīng)有存在logger的話就直接返回Hashtable中的,如果hashtable中沒(méi)有l(wèi)ogger,則新建一個(gè)同時(shí)放入Hashtable進(jìn)行保存。
Handler
:用來(lái)控制日志輸出的,比如JDK自帶的ConsoleHanlder把輸出流重定向到System.err輸出,每次調(diào)用Logger的方法進(jìn)行輸出時(shí)都會(huì)調(diào)用Handler的publish方法,每個(gè)logger有多個(gè)handler。我們可以利用handler來(lái)把日志輸入到不同的地方(比如文件系統(tǒng)或者是遠(yuǎn)程Socket連接).
Formatter
:日志在真正輸出前需要進(jìn)行一定的格式話:比如是否輸出時(shí)間?時(shí)間格式?是否輸入線程名?是否使用國(guó)際化信息等都依賴(lài)于Formatter。
Log Level
:不必說(shuō),這是做容易理解的一個(gè),也是logging為什么能幫助我們適應(yīng)從開(kāi)發(fā)調(diào)試到部署上線等不同階段對(duì)日志輸出粒度的不同需求。
JDK Log級(jí)別從高到低為
OFF(231-1)—>SEVERE(1000)—>WARNING(900)—>INFO(800)—>CONFIG(700)—>FINE(500)—>FINER(400)—>FINEST(300)—>ALL(-231)
每個(gè)級(jí)別分別對(duì)應(yīng)一個(gè)數(shù)字,輸出日志時(shí)級(jí)別的比較就依賴(lài)于數(shù)字大小的比較。但是需要注意的是:不僅是logger具有級(jí)別,handler也是有級(jí)別,也就是說(shuō)如果某個(gè)logger級(jí)別是FINE,客戶(hù)希望輸入FINE級(jí)別的日志,如果此時(shí)logger對(duì)應(yīng)的handler級(jí)別為INFO,那么FINE級(jí)別日志仍然是不能輸出的。
總結(jié)對(duì)應(yīng)關(guān)系
LogManager與logger是1對(duì)多關(guān)系,整個(gè)JVM運(yùn)行時(shí)只有一個(gè)LogManager,且所有的logger均在LogManager中
logger與handler是多對(duì)多關(guān)系,logger在進(jìn)行日志輸出的時(shí)候會(huì)調(diào)用所有的hanlder進(jìn)行日志的處理
handler與formatter是一對(duì)一關(guān)系,一個(gè)handler有一個(gè)formatter進(jìn)行日志的格式化處理
很明顯:logger與level是一對(duì)一關(guān)系,hanlder與level也是一對(duì)一關(guān)系
Logging配置:
JDK默認(rèn)的logging配置文件為:$JAVA_HOME/jre/lib/logging.properties,可以使用系統(tǒng)屬性java.util.logging.config.file指定相應(yīng)的配置文件對(duì)默認(rèn)的配置文件進(jìn)行覆蓋,配置文件中通常包含以下幾部分定義:
1, handlers
:用逗號(hào)分隔每個(gè)Handler,這些handler將會(huì)被加到root logger中。也就是說(shuō)即使我們不給其他logger配置handler屬性,在輸出日志的時(shí)候logger會(huì)一直找到root logger,從而找到handler進(jìn)行日志的輸入。
2, .level
是root logger的日志級(jí)別
3, <handler>.xxx是配置具體某個(gè)handler的屬性,比如java.util.logging.ConsoleHandler.formatter便是為ConsoleHandler配置相應(yīng)的日志Formatter.
4, logger的配置,所有以[.level]結(jié)尾的屬性皆被認(rèn)為是對(duì)某個(gè)logger的級(jí)別的定義
如com.bes.server.level=FINE是給名為[com.bes.server]的logger定義級(jí)別為FINE。
順便說(shuō)下,前邊提到過(guò)logger的繼承關(guān)系,如果還有com.bes.server.webcontainer這個(gè)logger,且在配置文件中沒(méi)有定義該logger的任何屬性,那么其將會(huì)從[com.bes.server]這個(gè)logger進(jìn)行屬性繼承。除了級(jí)別之外,還可以為logger定義handler和useParentHandlers(默認(rèn)是為true)屬性,
如com.bes.server.handler=com.bes.test.ServerFileHandler(需要是一個(gè)extends java.util.logging.Handler的類(lèi)),
com.bes.server.useParentHandlers=false
(意味著com.bes.server這個(gè)logger進(jìn)行日志輸出時(shí),日志僅僅被處理一次,用自己的handler輸出,不會(huì)傳遞到父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
Logging執(zhí)行原理
Logger的獲取
A,首先是調(diào)用Logger的如下方法獲得一個(gè)logger
public static synchronized Logger getLogger(String name) { LogManager manager =LogManager.getLogManager(); returnmanager.demandLogger(name); }
B,上面的調(diào)用會(huì)觸發(fā)java.util.logging.LoggerManager的類(lèi)初始化工作,LoggerManager有一個(gè)靜態(tài)化初始化塊(這是會(huì)先于LoggerManager的構(gòu)造函數(shù)調(diào)用的):
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; } }); }
從靜態(tài)初始化塊中可以看出LoggerManager是可以使用系統(tǒng)屬性java.util.logging.manager指定一個(gè)繼承自java.util.logging.LoggerManager的類(lèi)進(jìn)行替換的,比如Tomcat啟動(dòng)腳本中就使用該機(jī)制以使用自己的LoggerManager。
不管是JDK默認(rèn)的java.util.logging.LoggerManager還是自定義的LoggerManager,初始化工作中均會(huì)給LoggerManager添加兩個(gè)logger,一個(gè)是名稱(chēng)為””的root logger,且logger級(jí)別設(shè)置為默認(rèn)的INFO;另一個(gè)是名稱(chēng)為global的全局logger,級(jí)別仍然為INFO。
LogManager”類(lèi)”初始化完成之后就會(huì)讀取配置文件(默認(rèn)為$JAVA_HOME/jre/lib/logging.properties),把配置文件的屬性名<->屬性值這樣的鍵值對(duì)保存在內(nèi)存中,方便之后初始化logger的時(shí)候使用。
C,A步驟中Logger類(lèi)發(fā)起的getLogger操作將會(huì)調(diào)用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首先從現(xiàn)有的logger列表中查找,如果找不到的話,會(huì)新建一個(gè)looger并加入到列表中。當(dāng)然很重要的是新建looger之后需要對(duì)logger進(jìn)行初始化,這個(gè)初始化詳見(jiàn)java.util.logging.LoggerManager#addLogger()方法中,改方法會(huì)根據(jù)配置文件設(shè)置logger的級(jí)別以及給logger添加handler等操作。
到此為止logger已經(jīng)獲取到了,你同時(shí)也需要知道此時(shí)你的logger中已經(jīng)有級(jí)別、handler等重要信息,下面將分析輸出日志時(shí)的邏輯。
日志的輸出
首先我們通常會(huì)調(diào)用Logger類(lèi)下面的方法,傳入日志級(jí)別以及日志內(nèi)容。
public void log(Levellevel, String msg) { if (level.intValue() < levelValue ||levelValue == offValue) { return; } LogRecord lr = new LogRecord(level, msg); doLog(lr); }
該方法可以看出,Logger類(lèi)首先是進(jìn)行級(jí)別的校驗(yàn),如果級(jí)別校驗(yàn)通過(guò),則會(huì)新建一個(gè)LogRecord對(duì)象,LogRecord中除了日志級(jí)別,日志內(nèi)容之外還會(huì)包含調(diào)用線程信息,日志時(shí)刻等;之后調(diào)用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)方法中設(shè)置了ResourceBundle信息(這個(gè)與國(guó)際化有關(guān))之后便直接調(diào)用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循環(huán)是重中之重,首先從logger中獲取handler,然后分別調(diào)用handler的publish(LogRecordrecord)方法。while循環(huán)證明了前面提到的會(huì)一直把日志委托給父logger處理的說(shuō)法,當(dāng)然也證明了可以使用logger的useParentHandlers屬性控制日志不進(jìn)行往上層logger傳遞的說(shuō)法。到此為止logger對(duì)日志的控制差不多算是完成,接下來(lái)的工作就是看handler的了,這里我們以java.util.logging.ConsoleHandler為例說(shuō)明日志的輸出。
public class ConsoleHandler extends StreamHandler { public ConsoleHandler() { sealed = false; configure(); setOutputStream(System.err); sealed = true; }
ConsoleHandler構(gòu)造函數(shù)中除了需要調(diào)用自身的configure()方法進(jìn)行級(jí)別、filter、formatter等的設(shè)置之外,最重要的我們最關(guān)心的是setOutputStream(System.err)這一句,把系統(tǒng)錯(cuò)誤流作為其輸出。而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); } }
方法邏輯也很清晰,首先是調(diào)用Formatter對(duì)消息進(jìn)行格式化,說(shuō)明一下:格式化其實(shí)是進(jìn)行國(guó)際化處理的重要契機(jī)。然后直接把消息輸出到對(duì)應(yīng)的輸出流中。需要注意的是handler也會(huì)用自己的level和LogRecord中的level進(jìn)行比較,看是否真正輸出日志。
總結(jié)
至此,整個(gè)日志輸出過(guò)程已經(jīng)分析完成。細(xì)心的讀者應(yīng)該可以解答如下四個(gè)問(wèn)題了。
1,【Main running.】以外的日志為什么沒(méi)有輸出?怎么讓它們也能夠出現(xiàn)?
這就是JDK默認(rèn)的logging.properties文件中配置的handler級(jí)別和跟級(jí)別均為info導(dǎo)致的,如果希望看到FINE級(jí)別日志,需要修改logging.properties文件,同時(shí)進(jìn)行如下兩個(gè)修改
??? java.util.logging.ConsoleHandler.level= FINE//修改 ??? com.bes.logging.level=FINE//添加
2,日志中出現(xiàn)的時(shí)間、類(lèi)名、方法名等是從哪里輸出的?
請(qǐng)參照
java.util.logging.ConsoleHandler.formatter= java.util.logging.SimpleFormatter
配置中指定的java.util.logging.SimpleFormatter類(lèi)
其publicsynchronized String format(LogRecord record) 方法說(shuō)明了一切。
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,為什么日志就會(huì)出現(xiàn)在控制臺(tái)?
看到j(luò)ava.util.logging.ConsoleHandler 類(lèi)構(gòu)造方法中的[setOutputStream(System.err)]語(yǔ)句,相信你已經(jīng)明白。
4,大型的系統(tǒng)可能有很多子模塊(可簡(jiǎn)單理解為有很多包名),如何對(duì)這些子模塊進(jìn)行單獨(dú)的日志級(jí)別控制?
在logging.properties文件中分別對(duì)各個(gè)logger的級(jí)別進(jìn)行定義,且最好使用java.util.logging.config.file屬性指定自己的配置文件。
第5個(gè)問(wèn)題暫時(shí)還解答不了,請(qǐng)繼續(xù)期待,下一遍博文將講述log4j和JDK logging的關(guān)系,以及怎么實(shí)現(xiàn)自己的LoggerManager以使得我們完全定制化logger、handler、formatter,掌控日志的國(guó)際化信息等,更多關(guān)于java JDK Logging日志模塊處理細(xì)節(jié)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java中對(duì)象和JSON格式的轉(zhuǎn)換方法代碼
JSON格式可以輕松地以面向?qū)ο蟮姆绞睫D(zhuǎn)換為Java對(duì)象,下面這篇文章主要給大家介紹了關(guān)于java中對(duì)象和JSON格式的轉(zhuǎn)換方法,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12Spring Boot 自動(dòng)配置的實(shí)現(xiàn)
這篇文章主要介紹了Spring Boot 自動(dòng)配置的實(shí)現(xiàn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08深入理解Java嵌套類(lèi)和內(nèi)部類(lèi)
本篇文章主要介紹了深入理解Java嵌套類(lèi)和內(nèi)部類(lèi),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05SpringBoot自動(dòng)裝配原理詳細(xì)解析
這篇文章主要介紹了SpringBoot自動(dòng)裝配原理詳細(xì)解析,一個(gè)對(duì)象交給Spring來(lái)管理的三種方式 @Bean @Compoment @Import,2024-01-01
@Bean主要在@Configuration中,通過(guò)方法進(jìn)行注入相關(guān)的Bean,@Compoent與@Service歸為一類(lèi),在類(lèi)上加注入對(duì)應(yīng)的類(lèi),需要的朋友可以參考下帶你用Java方法輕松實(shí)現(xiàn)樹(shù)的同構(gòu)
給定兩棵樹(shù)T1和T2。如果T1可以通過(guò)若干次左右孩子互換就變成T2,則我們稱(chēng)兩棵樹(shù)是“同構(gòu)”的。例如圖1給出的兩棵樹(shù)就是同構(gòu)的,因?yàn)槲覀儼哑渲幸豢脴?shù)的結(jié)點(diǎn)A、B、G的左右孩子互換后,就得到另外一棵樹(shù)2021-06-06Mybatis 中Mapper使用package方式配置報(bào)錯(cuò)的解決方案
這篇文章主要介紹了Mybatis 中Mapper使用package方式配置報(bào)錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07java數(shù)學(xué)歸納法非遞歸求斐波那契數(shù)列的方法
這篇文章主要介紹了java數(shù)學(xué)歸納法非遞歸求斐波那契數(shù)列的方法,涉及java非遞歸算法的使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07