承接前文log4j源碼解析,前文主要介紹了log4j的文件加載方式以及Logger對象創建。本文將在此基礎上具體看下log4j是如何解析文件並輸出我們所常見的日志格式
附例
文件的加載方式,我們就選舉log4j.properties
作為分析的文件例子,並附上相應的通用配置
log4j.rootLogger=info,stdout,logfile,errorfile
log4j.logger.org.apache=DEBUG
log4j.logger.java.sql.Connection=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
log4j.logger.java.sql.ResultSet=INFO
log4j.logger.freemarker.core=error
#standout log appender #
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
#common log appender #
log4j.appender.logfile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.logfile.File=../logs/appender-test/info.log
log4j.appender.logfile.append=true
log4j.appender.logfile.encoding=GB18030
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
#error log appender #
log4j.appender.errorfile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.errorfile.File=../logs/appender-test/error.log
log4j.appender.errorfile.Threshold=WARN
log4j.appender.errorfile.append=true
log4j.appender.errorfile.encoding=GB18030
log4j.appender.errorfile.layout=org.apache.log4j.PatternLayout
log4j.appender.errorfile.layout.ConversionPattern=%d %p [%c] - %m%n
此處不詳解,我們直接看源碼方面是如何處理,從代碼層面來通用理解下上述的配置
OptionConverter
操作log4j配置的主要工具類,所有的讀取配置並封裝成對象均以此類作為入口,其在LogManager的調用方式為
OptionConverter.selectAndConfigure(url,configuratorClassName,LogManager.getLoggerRepository());
入參作下簡單的展示
url - 文件路徑
clazz - log4j.configuratorClass屬性對應的class,默認為null
hierarchy - log4j的層級管理類,存儲log4j的通用配置,默認為Hierarchy類
OptionConverter#selectAndConfigure-解析配置入口
簡單看下源碼
static public void selectAndConfigure(URL url, String clazz, LoggerRepository hierarchy) {
Configurator configurator = null;
String filename = url.getFile();
//xml格式的文件則采用DOMConfigurator解析類,表明默認采用xml格式的解析方式
if(clazz == null && filename != null && filename.endsWith(".xml")) {
clazz = "org.apache.log4j.xml.DOMConfigurator";
}
if(clazz != null) {
LogLog.debug("Preferred configurator class: " + clazz);
configurator = (Configurator) instantiateByClassName(clazz,
Configurator.class,
null);
if(configurator == null) {
LogLog.error("Could not instantiate configurator ["+clazz+"].");
return;
}
} else {
//最后一種方式則為properties解析方式
configurator = new PropertyConfigurator();
}
configurator.doConfigure(url, hierarchy);
}
從簡單的注釋中我們可以得出log4j
只支持兩種方式的解析方式
1.DOMConfigurator-xml格式的解析器,默認
2.PropertyConfigurator-properties格式的解析器
本文則着重講解.properties
配置文件的解析,即關注PropertyConfigurator
解析器
PropertyConfigurator#doConfigure-解析properties配置文件
讀取文件的方式就不分析了,很常見的采用Properties
類來存儲數據,遞上重要的邏輯片段代碼
public void doConfigure(Properties properties, LoggerRepository hierarchy) {
repository = hierarchy;
// 讀取log4j.debug配置,值為boolean型,表明內部log是否支持debug模式
String value = properties.getProperty(LogLog.DEBUG_KEY);
if(value == null) {
value = properties.getProperty("log4j.configDebug");
if(value != null)
LogLog.warn("[log4j.configDebug] is deprecated. Use [log4j.debug] instead.");
}
if(value != null) {
LogLog.setInternalDebugging(OptionConverter.toBoolean(value, true));
}
//讀取log4j.reset的boolean值,true代表使用默認的配置
String reset = properties.getProperty(RESET_KEY);
if (reset != null && OptionConverter.toBoolean(reset, false)) {
hierarchy.resetConfiguration();
}
//log4j.threshold閾值配置,也就是告警級別配置
String thresholdStr = OptionConverter.findAndSubst(THRESHOLD_PREFIX,
properties);
if(thresholdStr != null) {
hierarchy.setThreshold(OptionConverter.toLevel(thresholdStr,
(Level) Level.ALL));
LogLog.debug("Hierarchy threshold set to ["+hierarchy.getThreshold()+"].");
}
// 配置根分類,也就是rootLogger
configureRootCategory(properties, hierarchy);
// 配置Logger工廠
configureLoggerFactory(properties);
// 解析非root的其他配置
parseCatsAndRenderers(properties, hierarchy);
LogLog.debug("Finished configuring.");
// 清空下緩存
registry.clear();
}
按照上面的解析順序作下備注
1.解析
log4j.debug/log4j.configDebug(boolean)
是否讓log4j的內部輸出源支持debug模式,其實也就是是否調用System.out.println()方法,默認不支持debug模式,支持warn/error模式。(支持System.err.println()方法)
2.
解析log4j.reset(boolean)
是否重新設置log4j配置,默認不重新設置(Optional,作用微小)
3.
解析log4j.threshold(String)
trace/debug/info/warn/error/fatal配置告警級別,表明對所有的輸出源,低於該等級則不輸出
4.解析根節點rootLogger
5.解析日志工廠
6.解析非根節點
我們對后三步的操作作下簡單的分析,加深我們對通用配置的理解
PropertyConfigurator#configureRootCategory-解析根節點
首先簡單的看下里面的操作邏輯
void configureRootCategory(Properties props, LoggerRepository hierarchy) {
// log4j.rootLogger或者log4j.rootCategory,支持${}系統變量取值
String effectiveFrefix = ROOT_LOGGER_PREFIX;
String value = OptionConverter.findAndSubst(ROOT_LOGGER_PREFIX, props);
if(value == null) {
value = OptionConverter.findAndSubst(ROOT_CATEGORY_PREFIX, props);
effectiveFrefix = ROOT_CATEGORY_PREFIX;
}
if(value == null)
LogLog.debug("Could not find root logger information. Is this OK?");
else {
Logger root = hierarchy.getRootLogger();
synchronized(root) {
// 關鍵代碼
parseCategory(props, root, effectiveFrefix, INTERNAL_ROOT_NAME, value);
}
}
}
承接上述的關鍵代碼分析,此處的logger
參數為rootLogger
/**
** This method must work for the root category as well.
*/
void parseCategory(Properties props, Logger logger, String optionKey,
String loggerName, String value) {
LogLog.debug("Parsing for [" +loggerName +"] with value=[" + value+"].");
// ,分隔符解析
StringTokenizer st = new StringTokenizer(value, ",");
if(!(value.startsWith(",") || value.equals(""))) {
if(!st.hasMoreTokens())
return;
String levelStr = st.nextToken();
LogLog.debug("Level token is [" + levelStr + "].");
// If the level value is inherited, set category level value to
// null. We also check that the user has not specified inherited for the
// root category.
if(INHERITED.equalsIgnoreCase(levelStr) ||
NULL.equalsIgnoreCase(levelStr)) {
if(loggerName.equals(INTERNAL_ROOT_NAME)) {
LogLog.warn("The root logger cannot be set to null.");
} else {
logger.setLevel(null);
}
} else {
logger.setLevel(OptionConverter.toLevel(levelStr, (Level) Level.DEBUG));
}
LogLog.debug("Category " + loggerName + " set to " + logger.getLevel());
}
// 刪除所有的輸出源對象
logger.removeAllAppenders();
Appender appender;
String appenderName;
while(st.hasMoreTokens()) {
appenderName = st.nextToken().trim();
if(appenderName == null || appenderName.equals(","))
continue;
LogLog.debug("Parsing appender named \"" + appenderName +"\".");
appender = parseAppender(props, appenderName);
if(appender != null) {
logger.addAppender(appender);
}
}
}
由以上的代碼可以簡單的得知log4j.rootLogger對應的配置項為{level},{appenderNames}
1.{level} - 日志等級,設置根日志的日志等級,應用於所有的輸出源
2.{appenderNames} - 可配置多個輸出源,以
,
為分隔符。並由此屬性解析log4j.appender開頭的配置項
再而分析了解下PropertyConfigurator#parseAppender()
方法解析輸出源,為了防止代碼展示過多,我們截取主要的代碼片段進行分析
Appender parseAppender(Properties props, String appenderName) {
....
// log4j.appender.{appenderName}
String prefix = APPENDER_PREFIX + appenderName;
// log4j.appender.{appenderName}.layout
String layoutPrefix = prefix + ".layout";
// 首先根據log4j.appender.{appenderName}解析得到Appender對象
appender = (Appender) OptionConverter.instantiateByKey(props, prefix,
org.apache.log4j.Appender.class,
null);
...
if(appender instanceof OptionHandler) {
if(appender.requiresLayout()) {
// 解析得到Layout對象,代表該輸出源的輸出格式
Layout layout = (Layout) OptionConverter.instantiateByKey(props,
layoutPrefix,
Layout.class,
null);
....
// 解析log4j.appender.{appenderName}.errorhandler
final String errorHandlerPrefix = prefix + ".errorhandler";
String errorHandlerClass = OptionConverter.findAndSubst(errorHandlerPrefix, props);
if (errorHandlerClass != null) {
ErrorHandler eh = (ErrorHandler) OptionConverter.instantiateByKey(props,
errorHandlerPrefix,
ErrorHandler.class,
null);
if (eh != null) {
appender.setErrorHandler(eh);
LogLog.debug("Parsing errorhandler options for \"" + appenderName +"\".");
// 解析ErrorHandler對象
parseErrorHandler(eh, errorHandlerPrefix, props, repository);
...
}
}
...
}
// 解析log4j.appender.{appenderName}.filter配置
parseAppenderFilters(props, appenderName, appender);
registryPut(appender);
return appender;
}
具體解析的過程就不講解了,只在此處作下羅列,此處假定appenderName為
console
1.log4j.appender.console - 對應的輸出源的類名
2.log4j.appender.console.layout - 對應輸出源的日志展示類名,通用為
log4j.appender.{appenderName}.layout
3.log4j.appender.console.errorhandler-對應輸出源的錯誤信息處理類名
4.log4j.appender.console.filter-輸出源過濾類,支持配置多個。格式為log4j.appender.console.filter.{filterName}={filterClass}
5.log4j.appender.console.encoding/threshold-此類的額外參數,其會利用反射的機制調用相應的setter方法進行設置
PropertyConfigurator#configureLoggerFactory-解析日志工廠
解析的為log4j.loggerFactory配置,其可以指定logger工廠的實現類,默認為
DefaultCategoryFactory
,其內部就一個方法makeNewLoggerInstance()
用於創建日志類Logger。用戶可自定義實現
PropertyConfigurator#parseCatsAndRenderers-解析非根節點
解析的為log4j.logger/log4j.category/log4j.additivity/log4j.renderer/log4j.throwableRenderer
配置,具體解析讀者可自行分析,此處作下總結
1.
log4j.logger/log4j.category
以此為開頭的配置,其會解析為Logger對象,方式與log4j.rootLogger
配置一致,多用於對指定的類進行特定級別的輸出,默認繼承根Logger對象的輸出源配置2.
log4j.additivity
以此開頭的配置,表明對特定的Logger對象只輸出自己所擁有的Appenders,不采用根Logger對象的Appenders3.
log4j.renderer/log4j.throwableRenderer
開頭的配置,前者主要配置對普通輸出信息的渲染處理,后者對異常信息的渲染處理。默認均有實現,一般不指定4.對輸出源的日志級別輸出與否比較規則作下總結(以基於
com.jing.test.Application
類調用info()方法輸出舉個例子):
首先判斷是否>=
log4j.threshold
屬性指定的日志級別Level,如果不滿足,則不輸出,反之繼續往下走。eg.log4j.threshold=WARN
則輸出源無法輸出然后根據loggerName獲取
log4j.category
/log4j.logger
對應的Level(如果loggerName以.分隔,則當前loggerName找不到會向父級獲取,沒定義則應用rootLogger),判斷是否>=Level,否則不輸出,反之繼續往下走
eg. 比如定義了log4j.logger.com.jing.test=INFO
,但沒定義log4j.logger.com.jing.test.Application
則其會應用com.jing.test
中的Level,即Level=INFO最后獲取輸出源Appender指定的日志級別Level,即
${appenderName}.Threshold
屬性,如果>=指定的Level,則進行輸出,反之不輸出
總結
見本文的分析,通過源碼加深我們對配置的理解,心中多一份踏實