log4j源碼解析-文件解析

承接前文log4j源碼解析,前文主要介紹了log4j的文件加載方式以及Logger對象建立。本文將在此基礎上具體看下log4j是如何解析文件並輸出咱們所常見的日誌格式html

附例

文件的加載方式,咱們就選舉log4j.properties做爲分析的文件例子,並附上相應的通用配置java

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

此處不詳解,咱們直接看源碼方面是如何處理,從代碼層面來通用理解下上述的配置sql

OptionConverter

操做log4j配置的主要工具類,全部的讀取配置並封裝成對象均以此類做爲入口,其在LogManager的調用方式爲
OptionConverter.selectAndConfigure(url,configuratorClassName,LogManager.getLoggerRepository());
入參做下簡單的展現apache

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只支持兩種方式的解析方式app

1.DOMConfigurator-xml格式的解析器,默認

2.PropertyConfigurator-properties格式的解析器

本文則着重講解.properties配置文件的解析,即關注PropertyConfigurator解析器工具

PropertyConfigurator#doConfigure-解析properties配置文件

讀取文件的方式就不分析了,很常見的採用Properties類來存儲數據,遞上重要的邏輯片斷代碼ui

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();
  }

按照上面的解析順序做下備註this

1.解析log4j.debug/log4j.configDebug(boolean)url

是否讓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對象的Appenders

3.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,則進行輸出,反之不輸出

總結

見本文的分析,經過源碼加深咱們對配置的理解,心中多一份踏實

相關文章
相關標籤/搜索