JDK Logging的使用很簡單,如下程式碼所示,先使用Logger類別的靜態方法getLogger就可以取得到一個logger,然後在任何地方都可以透過取得到的logger進行日誌輸入。例如類似logger.info("Main running.")的呼叫。
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機制,本章為你分析這個簡單模組的機制。
在深入分析之前,需要掌握以下術語
logger
:對於logger,需要知道其下幾個面向
1,程式碼需要輸入日誌的地方都會用到Logger,這幾乎是一個JDK logging模組的代言人,我們常常用Logger.getLogger("com.aaa.bbb");取得一個logger,然後使用logger做日誌的輸出。
2,logger其實只是一個邏輯管理單元,其多數運算都只是作為一個中繼者傳遞別的6d646f3bd37b02dfe0ce571e5054fe2f,比如說:Logger.getLogger(“xxx”)的呼叫將會依賴LogManager類,使用logger輸入日誌資訊的時候會呼叫logger中的所有handler進行日誌的輸入。
3,logger是有層次關係的,我們可一般性的理解為包名之間的父子繼承關係。每個logger通常以java套件名為其名稱。子logger通常會從父logger繼承logger等級、handler、ResourceBundle名稱(與國際化資訊有關)等。
4,整個JVM會存在一個名稱為空的root logger,所有匿名的logger都會把root logger作為其父
LogManager
:整個JVM內部所有logger的管理,logger的生成、獲取等操作都依賴它,也包括設定檔的讀取。 LogManager中會有一個Hashtable【private Hashtable35bf2dffdda6a7e3e41e02bf88c5368c> loggers】用來儲存目前所有的logger,如果需要取得logger的時候,Hashtable已經有存在logger的話就直接回傳Hashtable中的,如果hashtable中沒有logger,則新建一個同時放入Hashtable保存。
Handler
:用來控制日誌輸出的,例如JDK自帶的ConsoleHanlder把輸出流重定向到System.err輸出,每次呼叫Logger的方法進行輸出時都會呼叫Handler的publish方法,每個logger有多個handler。我們可以利用handler來把日誌輸入到不同的地方(例如檔案系統或是遠端Socket連線).
#Formatter
:日誌在真正輸出前需要進行一定的格式話:例如是否輸出時間?時間格式?是否輸入線程名?是否使用國際化資訊等都依賴Formatter。
Log Level
:不必說,這是要做容易理解的一個,也是logging為什麼能幫助我們適應從開發調試到部署上線等不同階段對日誌輸出粒度的不同需求。 JDK Log等級由高到低為OFF(231-1)—>SEVERE(1000)—>WARNING(900)—>INFO(800)—>CONFIG(700)—INFO(800)—>CONFIG(700)—>> FINE(500)—>FINER(400)—>FINEST(300)—>ALL(-231),每個等級分別對應一個數字,輸出日誌時層級的比較就依賴數字大小的比較。但是要注意的是:不僅是logger具有級別,handler也是有級別,也就是說如果某個logger級別是FINE,客戶希望輸入FINE級別的日誌,如果此時logger對應的handler級別為INFO,那麼FINE級別日誌仍然是不能輸出的。
LogManager與logger是1對多關係,整個JVM執行時只有一個LogManager,且所有的logger都在LogManager中
logger與handler是多對多關係,logger在進行日誌輸出的時候會呼叫所有的hanlder進行日誌的處理
handler與formatter是一對一關係,一個handler有一個formatter進行日誌的格式化處理
很明顯:logger與level是一對一關係,hanlder與level也是一對一關係
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, 0827ab9817f5e2ad2bd92ebb7f6baa6b.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
A,首先是调用Logger的如下方法获得一个logger
public static synchronized Logger getLogger(String name) { LogManager manager =LogManager.getLogManager(); returnmanager.demandLogger(name); }
B,上面的调用会触发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),把配置文件的属性名e09be6022d700e04aeaa85a5f42fdcb2属性值这样的键值对保存在内存中,方便之后初始化logger的时候使用。
C,A步骤中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等重要信息,下面将分析输出日志时的逻辑。
首先我们通常会调用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进行比较,看是否真正输出日志。
至此,整个日志输出过程已经分析完成。细心的读者应该可以解答如下四个问题了。
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();}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属性指定自己的配置文件。
以上是java底層JDK Logging日誌模組怎麼處理的詳細內容。更多資訊請關注PHP中文網其他相關文章!