log4j学习笔记之打印日志
log4j日志级别共有7种:分别是ALL,DEBUG,INFO,WARN,ERROR,FATAL,OFF。看Priority类中的定义
public final static int OFF_INT = Integer.MAX_VALUE;public final static int FATAL_INT = 50000;public final static int ERROR_INT = 40000;public final static int WARN_INT = 30000;public final static int INFO_INT = 20000;public final static int DEBUG_INT = 10000;public final static int ALL_INT = Integer.MIN_VALUE;
显然是ALL < DEBUG < INFO < WARN < ERROR < FATAL < OFF
在log4j中建议使用DEBUG,INFO,WARN,ERROR这四种级别。
在Category类中有很多打印日志的方法
public class Category implements AppenderAttachable { public void debug(Object message) {} public void info(Object message) {} public void warn(Object message) {} public void error(Object message) {}}由于org.apache.log4j.Logger继承自Category,所以只要我们获取到一个logger,然后在这个logger上调用以上方法即可打印日志,在Logger类中有很多获取logger的方法,下面以debug方法为例分析一下打印日志的过程。
在完成log4j初始化后,调用logger.debug(""),以下是Category中的方法
public void debug(Object message) { if(repository.isDisabled(Level.DEBUG_INT)) return; if(Level.DEBUG.isGreaterOrEqual(this.getEffectiveLevel())) { forcedLog(FQCN, Level.DEBUG, message, null); } }1、首先判断debug这种级别的日志是否可以显示,Threshold是个全局的过滤器,它将把低于所设置的level的信息过滤不显示出来。
public boolean isDisabled(int level) { return thresholdInt > level; }2、获取有效Level,调用getEffectiveLevel()方法遍历Logger 的等级结构,找出本Logger 的等级。并和本层次的Log等级比较,判断本Logger能否打印这个层次的Log。如果可以打印就调用forcedLog()方法。forcedLog()方法通过传入得参数生成一个LoggingEvent实例,然后调用callAppenders()方法。
public Level getEffectiveLevel() { for(Category c = this; c != null; c=c.parent) { if(c.level != null)return c.level; } return null; // If reached will cause an NullPointerException. } protected void forcedLog(String fqcn, Priority level, Object message, Throwable t) { callAppenders(new LoggingEvent(fqcn, this, level, message, t)); } public void callAppenders(LoggingEvent event) { int writes = 0; for(Category c = this; c != null; c=c.parent) { synchronized(c) {if(c.aai != null) { writes += c.aai.appendLoopOnAppenders(event);}if(!c.additive) { break;} } } if(writes == 0) { repository.emitNoAppenderWarning(this); } }在Logger的继承结构上,每层都有可能有appender,callAppenders(LoggingEvent)方法通过遍历logger继承树,如果本Logger关闭了继承开关,就直接退出循环,否则依次遍历所有祖先的Appender。最后判断写Log的次数,如果等于0就打印没有Appender错误提示。
public int appendLoopOnAppenders(LoggingEvent event) { int size = 0; Appender appender; if(appenderList != null) { size = appenderList.size(); for(int i = 0; i < size; i++) {appender = (Appender) appenderList.elementAt(i);appender.doAppend(event); } } return size; }每个Logger可以有多个Appender,先遍历本Logger中所有的Appender,并调用相应的doAppend()方法。
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 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); }上面是AppenderSkeleton中的方法,AppenderSkeleton是实例Appender接口所有类的父类。append(event)在WriterAppender中有实现
public void append(LoggingEvent event) { if(!checkEntryConditions()) { return; } subAppend(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.format(event)将ConvisionPattern中的格式化字符串转换为真正需要打印的信息。下面看看PatternLayout中的format方法
public String format(LoggingEvent event) { 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(); }在初始化中提到ConvisionPattern被解析为一个链表,这里就是遍历这个链表,并作转换,将最终需要打印的信息存放到sbuf中。
分析一下 c.format(sbuf, event)这个方法
public void format(StringBuffer sbuf, LoggingEvent e) { String s = convert(e); if(s == null) { if(0 < min)spacePad(sbuf, min); return; } int len = s.length(); if(len > max) sbuf.append(s.substring(len-max)); else if(len < min) { if(leftAlign) {sbuf.append(s);spacePad(sbuf, min-len); } else {spacePad(sbuf, min-len);sbuf.append(s); } } else sbuf.append(s); }该方法的重点是第一句:String s = convert(e),来看转换过程,PatternConverter的子类有很多,挑选BasicPatternConverter来作分析
public String convert(LoggingEvent event) { switch(type) { case RELATIVE_TIME_CONVERTER:return (Long.toString(event.timeStamp - LoggingEvent.getStartTime())); case THREAD_CONVERTER:return event.getThreadName(); case LEVEL_CONVERTER:return event.getLevel().toString(); case NDC_CONVERTER:return event.getNDC(); case MESSAGE_CONVERTER: {return event.getRenderedMessage(); } default: return null; } } }以上方法中使用了switch(type),在log4j初始化时,ConvisionPattern类型的链表中的每一个节点并不是存放的%p,%c这种字符串,而是将它们转换成了一系列整型常量。
private static final int LITERAL_STATE = 0; private static final int CONVERTER_STATE = 1; private static final int DOT_STATE = 3; private static final int MIN_STATE = 4; private static final int MAX_STATE = 5; static final int FULL_LOCATION_CONVERTER = 1000; static final int METHOD_LOCATION_CONVERTER = 1001; static final int CLASS_LOCATION_CONVERTER = 1002; static final int LINE_LOCATION_CONVERTER = 1003; static final int FILE_LOCATION_CONVERTER = 1004; static final int RELATIVE_TIME_CONVERTER = 2000; static final int THREAD_CONVERTER = 2001; static final int LEVEL_CONVERTER = 2002; static final int NDC_CONVERTER = 2003; static final int MESSAGE_CONVERTER = 2004;
在遍历链表时,根据这些常量的取值,采取不同的替换方案。例如:RELATIVE_TIME_CONVERTER,会在sbuf中增加一个时间值;LEVEL_CONVERTER在sbuf中记录日志级别等。
总结:日志打印,首先判断日志级别,是否可以打印该种级别的日志,然后遍历logger继承树,对每层的n个appender也进行遍历,最后根据初始化时生成的链表构建打印信息进行转换,生成最终需要打印的信息,在每个appender上打印日志。