zoukankan      html  css  js  c++  java
  • log4j源码解析-文件解析

    承接前文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对象的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,则进行输出,反之不输出

    总结

    见本文的分析,通过源码加深我们对配置的理解,心中多一份踏实

  • 相关阅读:
    【IDEA】创建Maven工程
    【Java】Input,Output,Stream I/O流 03 系统标准流 & 打印流
    【Java】Reflection 反射机制 03调用
    【Java】JDBC Part2 工具类封装实现
    【郝斌C ST】 指针入门
    【Java】Input,Output,Stream I/O流 02 文件流 & 缓冲流
    margin和padding的区别
    JAVA反射机制
    HTTP错误代码详细介绍
    关于Java继承问题
  • 原文地址:https://www.cnblogs.com/question-sky/p/8436366.html
Copyright © 2011-2022 走看看