欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【log4j】【2】【log4j的原理】使用eclipse和jvm命令分析log4j的原理(1)

程序员文章站 2022-05-24 15:25:43
...

一、准备工作

安装好java,maven,eclipse.已有集成log4j的maven项目(使用maven可以便于加载和管理jar包,集成插件),log4j源码或者反编译软件(我使用的是反编译插件jad,集成到Eclipse)

二、查看类加载日志,方便寻找核心类

2.1 eclipse打印出日志

首先写好一个测试log4j的测试类

import org.apache.log4j.Logger;
import org.junit.Test;

public class Log4jTest {
	
	@Test
	public  void maintest() {
		Logger log = Logger.getLogger(Log4jTest.class);
		log.debug("Hello world");
		log.info("info test threshold");
	}
}
使用junit4运行,运行之前加上jvm命令 -verbose,具体操作如图

右击>debug as>debug configuratio...

【log4j】【2】【log4j的原理】使用eclipse和jvm命令分析log4j的原理(1)【log4j】【2】【log4j的原理】使用eclipse和jvm命令分析log4j的原理(1)

点击debug运行,可以在console上看到输出的日志

【log4j】【2】【log4j的原理】使用eclipse和jvm命令分析log4j的原理(1)

将输出的日志放到文本编辑器上(我是用的是Notepad++)【log4j】【2】【log4j的原理】使用eclipse和jvm命令分析log4j的原理(1)

根据输出的日志查看分析大概了解到核心类是,Logger,LogManager(与logger同包,所以可以new Logger,Logger的构造器是protected),Level,Appender(设置输出位置),Layout(格式化输出)。

三、根据得到的核心类,画一个UML类图,时序图。

3.1 类图

【log4j】【2】【log4j的原理】使用eclipse和jvm命令分析log4j的原理(1)

3.2  时序图


四、核心代码逐个分析,代码逻辑

4.1 Logger分析

Logger继承Category,Category实现AppenderAttachable,Category和Logger的构造器都是protected,所以构造Logger的实例是用LogManage的方法实现的。

Logger与Appender的关联关系 是通过类AppenderAttachableImpl里面的成员变量Vector容器存放Appender实现的

public class AppenderAttachableImpl implements AppenderAttachable {
  
  /** Array of appenders. */
  protected Vector  appenderList;
......
public
  void addAppender(Appender newAppender) {
    // Null values for newAppender parameter are strictly forbidden.
    if(newAppender == null)
      return;
    
    if(appenderList == null) {
      appenderList = new Vector(1);
    }
    if(!appenderList.contains(newAppender))
      appenderList.addElement(newAppender);
  }


Logger的callAppenders方法

 public
  void callAppenders(LoggingEvent event) {
    int writes = 0;

    for(Category c = this; c != null; c=c.parent) {
      // Protected against simultaneous call to addAppender, removeAppender,...
      synchronized(c) {
	if(c.aai != null) {
	  writes += c.aai.appendLoopOnAppenders(event);
	}
	if(!c.additive) {
	  break;
	}
      }
    }

    if(writes == 0) {
      repository.emitNoAppenderWarning(this);
    }
  }

LoggingEvent携带了日志的所有信息,取出aai中的Appender,将event给到appender让其处理

如果当前类aai为空,就会去找其父节点的Category,直到找到RootLogger。如果没有处理event信息,就会记录没有appender的错误。

 

/**
     Log a localized and parameterized message. First, the user
     supplied <code>key</code> is searched in the resource
     bundle. Next, the resulting pattern is formatted using
     {@link java.text.MessageFormat#format(String,Object[])} method with the
     user supplied object array <code>params</code>.


     @since 0.8.4
  */
  public
  void l7dlog(Priority priority, String key,  Object[] params, Throwable t) {
    if(repository.isDisabled(priority.level)) {
      return;
    }
    if(priority.isGreaterOrEqual(this.getEffectiveLevel())) {
      String pattern = getResourceBundleString(key);
      String msg;
      if(pattern == null)
	msg = key;
      else
	msg = java.text.MessageFormat.format(pattern, params);
      forcedLog(FQCN, priority, msg, t);
    }
  }


l7dlog方法,记录国际化日志,getResourceBundleString(String key)是根据key获取国际化内容的一个方法。

Logger类里面包含了对于Logger的关联,RootLogger继承Logger,也是Logger的根节点。

public final class RootLogger extends Logger {

  public RootLogger(Level level) {
    super("root");
    setLevel(level);
  }
  
  public final Level getChainedLevel() {
    return level;
  }

  public final void setLevel(Level level) {
    if (level == null) {
      LogLog.error(
        "You have tried to set a null level to root.", new Throwable());
    } else {
      this.level = level;
    }
  }

}

RootLogger是final类不可被继承,并且Level一定要有值,避免再向上找父类,而且name和Level都有值。

LogManager在静态代码块将文件加载进来,并且将key=value,存放在Hierarchy中。经OptionConverter.selectAndConfigure将文件解析,并且将配置放到LogManager

中的静态私有属性repositorySelector中。

 static {
    // By default we use a DefaultRepositorySelector which always returns 'h'.
    Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
    repositorySelector = new DefaultRepositorySelector(h);

    /** Search for the properties file log4j.properties in the CLASSPATH.  */
    String override =OptionConverter.getSystemProperty(DEFAULT_INIT_OVERRIDE_KEY,
						       null);

    // if there is no default init override, then get the resource
    // specified by the user or the default config file.
    if(override == null || "false".equalsIgnoreCase(override)) {

      String configurationOptionStr = OptionConverter.getSystemProperty(
							  DEFAULT_CONFIGURATION_KEY, 
							  null);

      String configuratorClassName = OptionConverter.getSystemProperty(
                                                   CONFIGURATOR_CLASS_KEY, 
						   null);

      URL url = null;

      // if the user has not specified the log4j.configuration
      // property, we search first for the file "log4j.xml" and then
      // "log4j.properties"
      if(configurationOptionStr == null) {	
	url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE);
	if(url == null) {
	  url = Loader.getResource(DEFAULT_CONFIGURATION_FILE);
	}
      } else {
	try {
	  url = new URL(configurationOptionStr);
	} catch (MalformedURLException ex) {
	  // so, resource is not a URL:
	  // attempt to get the resource from the class path
	  url = Loader.getResource(configurationOptionStr); 
	}	
      }
      
      // If we have a non-null url, then delegate the rest of the
      // configuration to the OptionConverter.selectAndConfigure
      // method.
      if(url != null) {
	    LogLog.debug("Using URL ["+url+"] for automatic log4j configuration.");
        try {
            OptionConverter.selectAndConfigure(url, configuratorClassName,
					   LogManager.getLoggerRepository());
        } catch (NoClassDefFoundError e) {
            LogLog.warn("Error during default initialization", e);
        }
      } else {
	    LogLog.debug("Could not find resource: ["+configurationOptionStr+"].");
      }
    } else {
        LogLog.debug("Default initialization of overridden by " + 
            DEFAULT_INIT_OVERRIDE_KEY + "property."); 
    }  
  }

Level分析

Level继承Priority,两者的构造器都是protected,Level这几个属性分别代表,级别,名字,对应的系统Level值(我并没有看出来,根据名字直译过来的)。另外,由于对相同级别的Level实例来说,它必须是单例的,因而Log4J对序列化和反序列化做了一些处理。即它的三个成员都是transient,真正序列化和反序列化的代码自己写,并且加入readResolve()方法的支持,以保证反序列化出来的相同级别的Level实例是相同的实例。【摘自 http://www.blogjava.net/DLevin/archive/2012/06/28/381667.html】

  transient int level;
  transient String levelStr;
  transient int syslogEquivalent;
  final static public Level OFF = new Level(OFF_INT, "OFF", 0);
  final static public Level FATAL = new Level(FATAL_INT, "FATAL", 0);
  final static public Level ERROR = new Level(ERROR_INT, "ERROR", 3);
  final static public Level WARN  = new Level(WARN_INT, "WARN",  4);
  final static public Level INFO  = new Level(INFO_INT, "INFO",  6);
  final static public Level DEBUG = new Level(DEBUG_INT, "DEBUG", 7);
  public static final Level TRACE = new Level(TRACE_INT, "TRACE", 7);
  final static public Level ALL = new Level(ALL_INT, "ALL", 7);

/**
     * Resolved deserialized level to one of the stock instances.
     * May be overriden in classes derived from Level.
     * @return resolved object.
     * @throws ObjectStreamException if exception during resolution.
     */
    private Object readResolve() throws ObjectStreamException {
        //
        //  if the deserizalized object is exactly an instance of Level
        //
        if (getClass() == Level.class) {
            return toLevel(level);
        }
        //
        //   extension of Level can't substitute stock item
        //
        return this;
    }


级别从高到低,OFF是不记录,ALL,表示都记录


Appender,可以添加过滤器,错误处理类,输出类,并且对其处理

public interface Appender {

  void addFilter(Filter newFilter);

  public
  Filter getFilter();

  public
  void clearFilters();

  public
  void close();
  
  public
  String getName();

  public
  void setErrorHandler(ErrorHandler errorHandler);

  public
  ErrorHandler getErrorHandler();

  public
  void setLayout(Layout layout);

  public
  Layout getLayout();
  
  public
  void setName(String name);

  public
  boolean requiresLayout();

对其子类AppenderSkeleton进行分析;

  /** The layout variable does not need to be set if the appender
      implementation has its own layout. */
  protected Layout layout;

  /** Appenders are named. */
  protected String name;

  /**
     There is no level threshold filtering by default.  */
  protected Priority threshold;

  /** 
      It is assumed and enforced that errorHandler is never null.
  */
  protected ErrorHandler errorHandler = new OnlyOnceErrorHandler();

  /** The first filter in the filter chain. Set to <code>null</code>
      initially. */
  protected Filter headFilter;
  /** The last filter in the filter chain. */
  protected Filter tailFilter;
属性有,输出类,最低级别,错误处理类,过滤职责链的开始和结束。

Appender的作用,将一个LoggingEvent处理一下,然后丢给Layout

AppenderSkeleton

AppenderSkeleton实现了Appender接口,写日志是通过doAppend(LoggingEvent e)实现的
 public
  synchronized 
  void doAppend(LoggingEvent event) {
    if(closed) {
      LogLog.error("Attempted to append to closed appender named ["+name+"].");
      return;
    }
    
    if(!isAsSevereAsThreshold(event.getLevel())) {
      return;
    }


    Filter f = this.headFilter;
    
    FILTER_LOOP:
    while(f != null) {
      switch(f.decide(event)) {
      case Filter.DENY: return;
      case Filter.ACCEPT: break FILTER_LOOP;
      case Filter.NEUTRAL: f = f.getNext();
      }
    }
    
    this.append(event);    
  }
先判断log是不是关闭状态,如果不是,判断日志界别是否大于阈值,然后再得到过滤器,判断过滤器是否为空,如果不为空,通过Filter的decide(event)方法判断event时候符合要求,如果decide方法返回值为DENY直接返回,如果为ACCEPT,直接跳出循环,记录日志,如果为NEUTRAL ,则得到下一个职责,进行再一次判断循环。append方法是抽象方法,调用子类。
WriterAppender
直接看输出日志的方法,首先检查当前Appender是否符合条件,然后再进行输出。
public
  void append(LoggingEvent event) {

    // Reminder: the nesting of calls is:
    //
    //    doAppend()
    //      - check threshold
    //      - filter
    //      - append();
    //        - checkEntryConditions();
    //        - subAppend();

    if(!checkEntryConditions()) {
      return;
    }
    subAppend(event);
   }
判断Appender是否为关闭,输出流是否为空,输出类是否为空。
protected
  boolean checkEntryConditions() {
    if(this.closed) {
      LogLog.warn("Not allowed to write to a closed appender.");
      return false;
    }

    if(this.qw == null) {
      errorHandler.error("No output stream or file set for the appender named ["+
			name+"].");
      return false;
    }

    if(this.layout == null) {
      errorHandler.error("No layout set for the appender named ["+ name+"].");
      return false;
    }
    return true;
  }
将event格式化,输出到日志,判断是否记录异常信息,如有异常信息就记录,最后刷新流,将流输出到日志文件

protected
  void subAppend(LoggingEvent event) {
    this.qw.write(this.layout.format(event));

    if(layout.ignoresThrowable()) {
      String[] s = event.getThrowableStrRep();
      if (s != null) {
	int len = s.length;
	for(int i = 0; i < len; i++) {
	  this.qw.write(s[i]);
	  this.qw.write(Layout.LINE_SEP);
	}
      }
    }

    if(shouldFlush(event)) {
      this.qw.flush();
    }
  }


Layout

layout是一个抽象类,主要方法有,格式化event,是否忽略异常
  
public abstract class Layout implements OptionHandler {

  // Note that the line.separator property can be looked up even by
  // applets.
  public final static String LINE_SEP = System.getProperty("line.separator");
  public final static int LINE_SEP_LEN  = LINE_SEP.length();


  /**
     Implement this method to create your own layout format.
  */
  abstract
  public
  String format(LoggingEvent event);

  /**
     Returns the content type output by this layout. The base class
     returns "text/plain". 
  */
  public
  String getContentType() {
    return "text/plain";
  }

  /**
     Returns the header for the layout format. The base class returns
     <code>null</code>.  */
  public
  String getHeader() {
    return null;
  }

  /**
     Returns the footer for the layout format. The base class returns
     <code>null</code>.  */
  public
  String getFooter() {
    return null;
  }



  /**
     If the layout handles the throwable object contained within
     {@link LoggingEvent}, then the layout should return
     <code>false</code>. Otherwise, if the layout ignores throwable
     object, then the layout should return <code>true</code>.
     If ignoresThrowable is true, the appender is responsible for
     rendering the throwable.

     <p>The {@link SimpleLayout}, {@link TTCCLayout}, {@link
     PatternLayout} all return <code>true</code>. The {@link
     org.apache.log4j.xml.XMLLayout} returns <code>false</code>.

     @since 0.8.4 */
  abstract
  public
  boolean ignoresThrowable();

}

子类PatternLayout
 /**
     Produces a formatted string as specified by the conversion pattern.
  */
  public String format(LoggingEvent event) {
    // Reset working stringbuffer
    if(sbuf.capacity() > MAX_CAPACITY) {
      sbuf = new StringBuffer(BUF_SIZE);
    } else {
      sbuf.setLength(0);
    }


    PatternConverter c = head;


    while(c != null) {
      c.format(sbuf, event);
      c = c.next;
    }
    return sbuf.toString();
  }

判断stringBuffer 容量是不是大于最大容量,如果是,新建一个StringBuffer,反之将清空StringBuffer。PatternConverter 是一个职责链,将日志信息格户到sbuf,返回格式化的字符串。