您好,登錄后才能下訂單哦!
這篇文章主要介紹“java底層JDK Logging日志模塊怎么處理”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“java底層JDK Logging日志模塊怎么處理”文章能幫助大家解決問題。
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其實只是一個邏輯管理單元,其多數操作都只是作為一個中繼者傳遞別的<角色>,比如說: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 Hashtable<String,WeakReference<Logger>> 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)—>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, <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
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),把配置文件的屬性名<->屬性值這樣的鍵值對保存在內存中,方便之后初始化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日志模塊怎么處理”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。