深入Log4J源码之Log4J Core

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介:

毕业又赶上本科的同学会,还去骑车环了趟崇明岛,六月貌似就没消停过,不过终于这些事情基本上都结束了,我也可以好好的看些书、读些源码、写点博客了。

Log4J将写日志功能抽象成七个核心类/接口:LoggerLoggerRepositoryLevelLoggingEventAppenderLayoutObjectRender。其类图如下:


更详细的,实现Log4J主要功能相关的类图:

其实Log4J最核心的也就5个类:Logger用于对日志记录行为的抽象,提供记录不同级别日志的接口;Level对日志级别的抽象;Appender是对记录日志形式的抽象;Layout是对日志行格式的抽象;而LoggingEvent是对一次日志记录过程中所能取到信息的抽象。另外两个LoggerRepository是Logger实例的容器,而ObjectRender是对日志实例的解析接口,它们主要提供了一种扩展支持。

简单的一次记录日志过程的序列图如下:


即获取Logger实例->判断Logger实例对应的日志记录级别是否要比请求的级别低->若是调用forceLog记录日志->创建LoggingEvent实例->将LoggingEvent实例传递给Appender->Appender调用Layout实例格式化日志消息->Appender将格式化后的日志信息写入该Appender对应的日志输出中。

包含Log4J其他模块类的更详细序列图如下:

在简单的介绍了Log4J各个模块类的作用后,以下将详细的介绍各个模块的具体作用以及代码实现。

Logger类

Logger是对记录日志动作的抽象,它提供了记录不同级别日志的接口,日志信息可以包含异常信息也可以不包含:

 1  public   void  debug(Object message) {
 2       if (isLevelEnabled(Level.DEBUG)) {
 3          forceLog(FQCN, Level.DEBUG, message,  null );
 4      }
 5  }
 6  public   void  debug(Object message, Throwable cause) {
 7       if (isLevelEnabled(Level.DEBUG)) {
 8          forceLog(FQCN, Level.DEBUG, message, cause);
 9      }
10  }
11  protected   void  forceLog(String fqcn, Level level, Object message, Throwable t) {
12      callAppenders( new  LoggingEvent(fqcn,  this , level, message, t));
13 }

Logger类包含Level信息 ,如果当前Logger未设置Level值,它也可以中父节点中继承下来,该值可以用来控制该Logger可以记录的日志级别:

 1  protected  Level level;
 2  public  Level getEffectiveLevel() {
 3       for (Logger logger  =   this ; logger  !=   null ; logger  =  logger.parent) {
 4           if (logger.level  !=   null ) {
 5               return  logger.level;
 6          }
 7      }
 8       return   null ;
 9  }
10  public   boolean  isLevelEnabled(Level level) {
11       return  level.isGreaterOrEqual( this .getEffectiveLevel());
12  }
13  public   boolean  isDebugEnabled() {
14       return  isLevelEnabled(Level.DEBUG);
15 }

Logger是一个命名的实体,其名字一般用”.”分割以体现不同Logger的层次关系,其中Level和Appender信息可以从父节点中获取,因而Logger类中还具有name和parent属性。

1  private  String name;
2  protected Logger parent;

在某些情况下,我们希望某些Logger只将日志记录到特定的Appender中,而不想记录在父节点中的Appender中,Log4J为这种需求提供了additivity属性,即对当前Logger节点,如果其additivity属性设置为false,则该Logger不会继承父节点的Appender信息,但是其子节点依然会继承该Logger的Appender信息,除非子节点的additivity属性也设置成了false。

 1  private   boolean  additive  =   true ;
 2  public   void  callAppenders(LoggingEvent event) {
 3       int  writes  =   0 ;
 4      
 5       for (Logger logger  =   this ; logger  !=   null ; logger  =  logger.parent) {
 6           synchronized (logger) {
 7               if (logger.appenders  !=   null ) {
 8                  writes  +=  logger.appenders.appendLoopOnAppenders(event);
 9              }
10               if ( ! logger.additive) {
11                   break ;
12              }
13          }
14      }
15      
16       if (writes  ==   0 ) {
17          System.err.println( " No Appender is configed. " );
18      }
19 }

最后,为了支持国际化,Log4J还提供了两个l7dlog()方法,通过指定的key,以从资源文件中获取消息内容。为了使用这两个方法,需要设置资源文件。同样,资源文件也是可以从父节点中继承的。

 1  private  ResourceBundle resourceBundle;
 2  public   void  l7dlog(Level level, String key, Throwable cause) {
 3       if (isLevelEnabled(level)) {
 4          String message  =  getResourceBundleString(key);
 5           if (message  ==   null ) {
 6              message  =  key;
 7          }
 8          forceLog(FQCN, level, message, cause);
 9      }
10  }
11 
12  public   void  l7dlog(Level level, String key, Object[] params, Throwable cause) {
13      
14           if (pattern  ==   null ) {
15              message  =  key;
16          }  else  {
17              message  =  MessageFormat.format(pattern, params);
18          }
19      
20  }
21 
22  protected  String getResourceBundleString(String key) {
23      ResourceBundle rb  =  getResourceBundle();
24      
25       return  rb.getString(key);
26 
27  public  ResourceBundle getResourceBundle() {
28       for (Logger logger  =   this ; logger  !=   null ; logger  =  logger.parent) {
29           if (logger.resourceBundle  !=   null ) {
30               return  logger.resourceBundle;
31          }
32      }
33       return   null ;    
34 }

另外,在实际开发中经常会遇到要把日志信息同时写到不同地方,如同时写入文件和控制台,因而一个Logger实例中可以包含多个Appender,为了管理多个Appender,Log4J抽象出了AppenderAttachable接口,它定义了几个用于管理多个Appender实例的方法,这些方法由AppenderAttachableImpl类实现,而Logger会实例化AppenderAttachableImpl,并将这些方法代理给该实例:

 1  public   interface  AppenderAttachable {
 2       public   void  addAppender(Appender newAppender);
 3       public  Enumeration getAllAppenders();
 4       public  Appender getAppender(String name);
 5       public   boolean  isAttached(Appender appender);
 6       void  removeAllAppenders();
 7       void  removeAppender(Appender appender);
 8       void  removeAppender(String name);
 9 }

RootLogger类

Log4J 中,所有 Logger 实例组成一个单根的树状结构,由于 Logger 实例的根节点有一点特殊:它的名字为“ root ”,它没有父节点,它的 Level 字段必须设值以防止其他 Logger 实例都没有设置 Level 值的情况。基于这些考虑, Log4J 通过继承 Logger 类实现了 RootLogger 类,它用于表达所有 Logger 实例的根节点:
 1  public   final   class  RootLogger  extends  Logger {
 2       public  RootLogger(Level level) {
 3           super ( " root " );
 4          setLevel(level);
 5      }
 6       public   final  Level getChainedLevel() {
 7           return  level;
 8      }
 9       public   final   void  setLevel(Level level) {
10           if  (level  ==   null ) {
11              LogLog.error( " You have tried to set a null level to root. " ,
12                       new  Throwable());
13          }  else  {
14               this .level  =  level;
15          }
16      }
17  }

NOPLogger类

有时候,为了测试等其他需求,我们希望Logger本身不做什么事情,Log4J为这种需求提供了NOPLogger类,它继承自Logger,但是基本上的方法都为空。

Level类

Level是对日志级别的抽象,目前Log4J支持的级别有FATAL、ERROR、WARN、INFO、DEBUG、TRACE,从头到尾一次级别递减,另外Log4J还支持两种特殊的级别:ALL和OFF,它们分别表示打开和关闭日志功能。

 1  public   static   final   int  OFF_INT  =  Integer.MAX_VALUE;
 2  public   static   final   int  FATAL_INT  =   50000 ;
 3  public   static   final   int  ERROR_INT  =   40000 ;
 4  public   static   final   int  WARN_INT   =   30000 ;
 5  public   static   final   int  INFO_INT   =   20000 ;
 6  public   static   final   int  DEBUG_INT  =   10000 ;
 7  public   static   final   int  TRACE_INT  =   5000 ;
 8  public   static   final   int  ALL_INT  =  Integer.MIN_VALUE;
 9 
10  public   static   final  Level OFF  =   new  Level(OFF_INT,  " OFF " 0 );
11  public   static   final  Level FATAL  =   new  Level(FATAL_INT,  " FATAL " 0 );
12  public   static   final  Level ERROR  =   new  Level(ERROR_INT,  " ERROR " 3 );
13  public   static   final  Level WARN  =   new  Level(WARN_INT,  " WARN " 4 );
14  public   static   final  Level INFO  =   new  Level(INFO_INT,  " INFO " 6 );
15  public   static   final  Level DEBUG  =   new  Level(DEBUG_INT,  " DEBUG " 7 );
16  public   static   final  Level TRACE  =   new  Level(TRACE_INT,  " TRACE " 7 );
17  public   static   final  Level ALL  =   new  Level(ALL_INT,  " ALL " 7 );

每个Level实例包含了该Level代表的int值(一般是从级别低到级别高一次增大)、该Level的String表达、该Level和系统Level的对应值。

1  protected   transient   int  level;
2  protected   transient  String levelStr;
3  protected   transient   int  syslogEquivalent;
4  protected  Level( int  level, String levelStr,  int  syslogEquivalent) {
5       this .level  =  level;
6       this .levelStr  =  levelStr;
7       this .syslogEquivalent  =  syslogEquivalent;
8  }

Level类主要提供了判断哪个Level级别更高的方法isGreaterOrEqual()以及将int值或String值转换成Level实例的toLevel()方法:

 1  public   boolean  isGreaterOrEqual(Level level) {
 2       return   this .level  >=  level.level;
 3  }
 4  public   static  Level toLevel( int  level) {
 5       return  toLevel(level, DEBUG);
 6  }
 7  public   static  Level toLevel( int  level, Level defaultLevel) {
 8       switch (level) {
 9           case  OFF_INT:  return  OFF;
10           case  FATAL_INT:  return  FATAL;
11           case  ERROR_INT:  return  ERROR;
12           case  WARN_INT:  return  WARN;
13           case  INFO_INT:  return  INFO;
14           case  DEBUG_INT:  return  DEBUG;
15           case  TRACE_INT:  return  TRACE;
16           case  ALL_INT:  return  ALL;
17      }
18       return  defaultLevel;
19  }

另外,由于对相同级别的Level实例来说,它必须是单例的,因而Log4J对序列化和反序列化做了一些处理。即它的三个成员都是transient,真正序列化和反序列化的代码自己写,并且加入readResolve()方法的支持,以保证反序列化出来的相同级别的Level实例是相同的实例。

 1  private   void  readObject( final  ObjectInputStream input)  throws  IOException, ClassNotFoundException {
 2      input.defaultReadObject();
 3      level  =  input.readInt();
 4      syslogEquivalent  =  input.readInt();
 5      levelStr  =  input.readUTF();
 6       if (levelStr  ==   null ) {
 7          levelStr  =   "" ;
 8      }
 9  }
10  private   void  writeObject( final  ObjectOutputStream output)  throws  IOException {
11      output.defaultWriteObject();
12      output.writeInt(level);
13      output.writeInt(syslogEquivalent);
14      output.writeUTF(levelStr);
15  }
16  private  Object readResolve()  throws  ObjectStreamException {
17       if ( this .getClass()  ==  Level. class ) {
18           return  toLevel(level);
19      }
20       return   this ;
21 }

如果要实现自己的Level类,可以继承自Level,并且实现相应的静态toLevel()方法即可。关于如何实现自己的Level类将会在配置文件相关小节中详细讨论。

LoggerRepository类

LoggerRepository从概念以及字面上来说它就是一个Logger实例的容器:一方面相同名字的Logger实例只需要创建一次,在后面的使用中,只需要从这个容器中取即可;另一方面,Logger容器可以存放从配置文件中解析出来的信息,从而使配置信息可以无缝的应用到Log4J内部系统中;最后Logger容器还为维护Logger的树状层次结构提供了方面,每个Logger只维护父节点的信息,有了Logger容器的存在则可以很容易的找到一个新的Logger实例的父节点;关于Logger容器将在下一节中详细讲解。

LoggingEvent类

LoggingEvent个人感觉用LoggingContext更合适一些,它是对一次日志记录时哪能获取到的数据的封装。它包含了以下信息以提供Layout在format()方法中使用:

1.       fqnOfCategoryClass:日志记录接口(默认为Logger)的类全名,该信息主要用于计算日志记录点的源文件、调用方法以及行号等位置信息。

2.       locationInfo:通过fqnOfCategoryClass计算位置信息,位置信息的计算由LocationInfo类实现,这些信息可以提供给Layout使用。

3.       logger:目前来看主要是通过Logger实例取得LogRepository实例,并通过LogRepository取得注册的ObjectRender实例,如果有的话。

4.       loggerName:当前日志记录的Logger名称,提供给Layout使用。

5.       threadName:当前线程名,提供给Layout使用。

6.       level:当前日志的级别,提供给Layout使用。

7.       message:当前日志类,一般是String类型,但是也可以通过注册ObjectRender,然后传入相应的其他对象类型。

8.       renderedMessage:经过ObjectRender处理后的日志信息,提供给Layout使用。

9.       throwableInfo:异常信息,如果存在的话,提供给Layout使用。

10.   timestamp:创建LoggingEvent实例的时间,提供给Layout使用。

11.   其他相对不常用的信息将会在后面小节中讲解。

LoggingEvent只是一个简单的数据对象(DO),因而其实现还是比较简单的,即在创建实例时将数据提供给它,在其他类(Layout等)使用它时通过getXXX()方法取数据。不过还是有几个方法可以简单的讲解一下。

LocationInfo类计算位置信息

LocationInfo所指的位置信息主要包括记录日志所在的源文件名、类名、方法名、所在源文件的行号。

1       transient  String lineNumber;
2       transient  String fileName;
3       transient  String className;
4       transient  String methodName;
5      // fully.qualified.classname.of.caller.methodName(Filename.java:line)
6       public  String fullInfo;

我们知道在异常栈中每一条记录都包含了方法调用对应的这些信息,Log4J的这些信息正是利用了这个原理,即通过构建一个Throwable实例,而后在该Throwable的栈信息中解析出来的:

1  public  LocationInfo getLocationInformation() {
2       if  (locationInfo  ==   null ) {
3          locationInfo  =   new  LocationInfo( new  Throwable(), 
4  fqnOfCategoryClass);
5      }
6       return  locationInfo;
7  }

以上Throwable一般会产生如下异常栈:

1  java.lang.Throwable
2 
3  at org.apache.log4j.PatternLayout.format(PatternLayout.java: 413 )
4  at org.apache.log4j.FileAppender.doAppend(FileAppender.java: 183 )
5  at org.apache.log4j.Category.callAppenders(Category.java: 131 )
6  at org.apache.log4j.Category.log(Category.java: 512 )
7  at callers.fully.qualified.className.methodName(FileName.java: 74 )
8 

因而我们就可以通过callers.fully.qualified.className信息来找到改行信息,这个className信息即是传入的fqnOfCategoryClass。

如果当前JDK版本是1.4以上,我们就可以通过JDK提供的一些方法来查找:

 1  getStackTraceMethod  =  Throwable. class .getMethod( " getStackTrace " ,
 2          noArgs);
 3  Class stackTraceElementClass  =  Class
 4          .forName( " java.lang.StackTraceElement " );
 5  getClassNameMethod  =  stackTraceElementClass.getMethod(
 6           " getClassName " , noArgs);
 7  getMethodNameMethod  =  stackTraceElementClass.getMethod(
 8           " getMethodName " , noArgs);
 9  getFileNameMethod  =  stackTraceElementClass.getMethod( " getFileName " ,
10          noArgs);
11  getLineNumberMethod  =  stackTraceElementClass.getMethod(
12           " getLineNumber " , noArgs);
13 
14  Object[] noArgs  =   null ;
15  Object[] elements  =  (Object[]) getStackTraceMethod.invoke(t,
16          noArgs);
17  String prevClass  =  NA;
18  for  ( int  i  =  elements.length  -   1 ; i  >=   0 ; i -- ) {
19      String thisClass  =  (String) getClassNameMethod.invoke(
20              elements[i], noArgs);
21       if  (fqnOfCallingClass.equals(thisClass)) {
22           int  caller  =  i  +   1 ;
23           if  (caller  <  elements.length) {
24              className  =  prevClass;
25              methodName  =  (String) getMethodNameMethod.invoke(
26                      elements[caller], noArgs);
27              fileName  =  (String) getFileNameMethod.invoke(
28                      elements[caller], noArgs);
29               if  (fileName  ==   null ) {
30                  fileName  =  NA;
31              }
32               int  line  =  ((Integer) getLineNumberMethod.invoke(
33                      elements[caller], noArgs)).intValue();
34               if  (line  <   0 ) {
35                  lineNumber  =  NA;
36              }  else  {
37                  lineNumber  =  String.valueOf(line);
38              }
39              StringBuffer buf  =   new  StringBuffer();
40              buf.append(className);
41              buf.append( " . " );
42              buf.append(methodName);
43              buf.append( " ( " );
44              buf.append(fileName);
45              buf.append( " : " );
46              buf.append(lineNumber);
47              buf.append( " ) " );
48               this .fullInfo  =  buf.toString();
49          }
50           return ;
51      }
52      prevClass  =  thisClass;
53  }

否则,则需要我们通过字符串查找的方式来查找:

 1  String s;
 2  //  Protect against multiple access to sw.
 3  synchronized  (sw) {
 4      t.printStackTrace(pw);
 5      s  =  sw.toString();
 6      sw.getBuffer().setLength( 0 );
 7  }
 8  int  ibegin, iend;
 9  ibegin  =  s.lastIndexOf(fqnOfCallingClass);
10  if  (ibegin  ==   - 1 )
11       return ;
12  //  See bug 44888.
13  if  (ibegin  +  fqnOfCallingClass.length()  <  s.length()
14           &&  s.charAt(ibegin  +  fqnOfCallingClass.length())  !=   ' . ' ) {
15       int  i  =  s.lastIndexOf(fqnOfCallingClass  +   " . " );
16       if  (i  !=   - 1 ) {
17          ibegin  =  i;
18      }
19  }
20 
21  ibegin  =  s.indexOf(Layout.LINE_SEP, ibegin);
22  if  (ibegin  ==   - 1 )
23       return ;
24  ibegin  +=  Layout.LINE_SEP_LEN;
25 
26  //  determine end of line
27  iend  =  s.indexOf(Layout.LINE_SEP, ibegin);
28  if  (iend  ==   - 1 )
29       return ;
30 
31  //  VA has a different stack trace format which doesn't
32  //  need to skip the inital 'at'
33  if  ( ! inVisualAge) {
34       //  back up to first blank character
35      ibegin  =  s.lastIndexOf( " at  " , iend);
36       if  (ibegin  ==   - 1 )
37           return ;
38       //  Add 3 to skip "at ";
39      ibegin  +=   3 ;
40  }
41  //  everything between is the requested stack item
42  this .fullInfo  =  s.substring(ibegin, iend);

对于通过字符串查找到的fullInfo值,在获取其他单个值时还需要做相应的字符串解析:
className:

 1  //  Starting the search from '(' is safer because there is
 2  //  potentially a dot between the parentheses.
 3  int  iend  =  fullInfo.lastIndexOf( ' ( ' );
 4  if  (iend  ==   - 1 )
 5      className  =  NA;
 6  else  {
 7      iend  =  fullInfo.lastIndexOf( ' . ' , iend);
 8 
 9       //  This is because a stack trace in VisualAge looks like:
10 
11       //  java.lang.RuntimeException
12       //  java.lang.Throwable()
13       //  java.lang.Exception()
14       //  java.lang.RuntimeException()
15       //  void test.test.B.print()
16       //  void test.test.A.printIndirect()
17       //  void test.test.Run.main(java.lang.String [])
18       int  ibegin  =   0 ;
19       if  (inVisualAge) {
20          ibegin  =  fullInfo.lastIndexOf( '   ' , iend)  +   1 ;
21      }
22 
23       if  (iend  ==   - 1 )
24          className  =  NA;
25       else
26          className  =   this .fullInfo.substring(ibegin, iend);

 

fileName:
1 
2  int  iend  =  fullInfo.lastIndexOf( ' : ' );
3  if  (iend  ==   - 1 )
4      fileName  =  NA;
5  else  {
6       int  ibegin  =  fullInfo.lastIndexOf( ' ( ' , iend  -   1 );
7      fileName  =   this .fullInfo.substring(ibegin  +   1 , iend);
8  }
lineNumber:
1  int  iend  =  fullInfo.lastIndexOf( ' ) ' );
2  int  ibegin  =  fullInfo.lastIndexOf( ' : ' , iend  -   1 );
3  if  (ibegin  ==   - 1 )
4      lineNumber  =  NA;
5  else
6      lineNumber  =   this .fullInfo.substring(ibegin  +   1 , iend);
methodName:
1  int  iend  =  fullInfo.lastIndexOf( ' ( ' );
2  int  ibegin  =  fullInfo.lastIndexOf( ' . ' , iend);
3  if  (ibegin  ==   - 1 )
4      methodName  =  NA;
5  else
6      methodName  =   this .fullInfo.substring(ibegin  +   1 , iend);

ObjectRender接口

Log4J中,对传入的message实例,如果是非String类型,会先使用注册的ObjectRender(在LogRepository中查找注册的ObjectRender信息)处理成String后返回,若没有找到相应的ObjectRender,则使用默认的ObjectRender,它只是调用该消息实例的toString()方法。

 1  public  Object getMessage() {
 2       if  (message  !=   null ) {
 3           return  message;
 4      }  else  {
 5           return  getRenderedMessage();
 6      }
 7  }
 8  public  String getRenderedMessage() {
 9       if  (renderedMessage  ==   null   &&  message  !=   null ) {
10           if  (message  instanceof  String)
11              renderedMessage  =  (String) message;
12           else  {
13              LoggerRepository repository  =  logger.getLoggerRepository();
14 
15               if  (repository  instanceof  RendererSupport) {
16                  RendererSupport rs  =  (RendererSupport) repository;
17                  renderedMessage  =  rs.getRendererMap()
18                          .findAndRender(message);
19              }  else  {
20                  renderedMessage  =  message.toString();
21              }
22          }
23      }
24       return  renderedMessage;
25  }

ThrowableInformation类

ThrowableInformation类用以处理异常栈信息,即通过Throwable实例获取异常栈字符串数组。同时还支持自定义的ThrowableRender(在LogRepository中设置),默认的ThrowableRender通过系统printStackTrace()方法来获取信息:

 1  if  (throwable  !=   null ) {
 2       this .throwableInfo  =   new  ThrowableInformation(throwable, logger);
 3  }
 4  ThrowableRenderer renderer  =   null ;
 5  if  (category  !=   null ) {
 6      LoggerRepository repo  =  category.getLoggerRepository();
 7       if  (repo  instanceof  ThrowableRendererSupport) {
 8          renderer  =  ((ThrowableRendererSupport) repo)
 9                  .getThrowableRenderer();
10      }
11  }
12  if  (renderer  ==   null ) {
13      rep  =  DefaultThrowableRenderer.render(throwable);
14  else  {
15      rep  =  renderer.doRender(throwable);
16  }
17  public   static  String[] render( final  Throwable throwable) {
18      StringWriter sw  =   new  StringWriter();
19      PrintWriter pw  =   new  PrintWriter(sw);
20       try  {
21          throwable.printStackTrace(pw);
22      }  catch  (RuntimeException ex) {
23      }
24      pw.flush();
25      LineNumberReader reader  =   new  LineNumberReader( new  StringReader(
26              sw.toString()));
27      ArrayList lines  =   new  ArrayList();
28       try  {
29          String line  =  reader.readLine();
30           while  (line  !=   null ) {
31              lines.add(line);
32              line  =  reader.readLine();
33          }
34      }  catch  (IOException ex) {
35           if  (ex  instanceof  InterruptedIOException) {
36              Thread.currentThread().interrupt();
37          }
38          lines.add(ex.toString());
39      }
40      String[] tempRep  =   new  String[lines.size()];
41      lines.toArray(tempRep);
42       return  tempRep;
43  }

Layout类

Layout负责将LoggingEvent中的信息格式化成一行日志信息。对不同格式的日志可能还需要提供头和尾等信息。另外有些Layout不会处理异常信息,此时ignoresThrowable()方法返回false,并且异常信息需要Appender来处理,如PatternLayout。

 1  public   abstract   class  Layout  implements  OptionHandler {
 2       public   final   static  String LINE_SEP  =  System.getProperty( " line.separator " );
 3       public   final   static   int  LINE_SEP_LEN  =  LINE_SEP.length();
 4       abstract   public  String format(LoggingEvent event);
 5       public  String getContentType() {
 6           return   " text/plain " ;
 7      }
 8       public  String getHeader() {
 9           return   null ;
10      }
11       public  String getFooter() {
12           return   null ;
13      }
14       abstract   public   boolean  ignoresThrowable();
15  }

Layout的实现比较简单,如SimpleLayout对一行日志信息只是打印日志级别信息以及日志信息。

 1  public   class  SimpleLayout  extends  Layout {
 2      StringBuffer sbuf  =   new  StringBuffer( 128 );
 3       public  SimpleLayout() {
 4      }
 5       public   void  activateOptions() {
 6      }
 7       public  String format(LoggingEvent event) {
 8          sbuf.setLength( 0 );
 9          sbuf.append(event.getLevel().toString());
10          sbuf.append( "  -  " );
11          sbuf.append(event.getRenderedMessage());
12          sbuf.append(LINE_SEP);
13           return  sbuf.toString();
14      }
15       public   boolean  ignoresThrowable() {
16           return   true ;
17      }
18  }

关于Layout更详细的信息将会在以后小节中介绍。

Appender接口

Appender负责定义日志输出的目的地,它可以是控制台(ConsoleAppender)、文件(FileAppender)、JMS服务器(JmsLogAppender)、以Email的形式发送出去(SMTPAppender)等。Appender是一个命名的实体,另外它还包含了对Layout、ErrorHandler、Filter等引用:

 1  public   interface  Appender {
 2       void  addFilter(Filter newFilter);
 3       public  Filter getFilter();
 4       public   void  clearFilters();
 5       public   void  close();
 6       public   void  doAppend(LoggingEvent event);
 7       public  String getName();
 8       public   void  setErrorHandler(ErrorHandler errorHandler);
 9       public  ErrorHandler getErrorHandler();
10       public   void  setLayout(Layout layout);
11       public  Layout getLayout();
12       public   void  setName(String name);
13       public   boolean  requiresLayout();
14  }

简单的,在配置文件中,Appender会注册到Logger中,Logger在写日志时,通过继承机制遍历所有注册到它本身和其父节点的Appender(在additivity为true的情况下),调用doAppend()方法,实现日志的写入。在doAppend方法中,若当前Appender注册了Filter,则doAppend还会判断当前日志时候通过了Filter的过滤,通过了Filter的过滤后,如果当前Appender继承自SkeletonAppender,还会检查当前日志级别时候要比当前Appender本身的日志级别阀门要打,所有这些都通过后,才会将LoggingEvent实例传递给Layout实例以格式化成一行日志信息,最后写入相应的目的地,在这些操作中,任何出现的错误都由ErrorHandler字段来处理。

SkeletonAppender类

目前Log4J实现的Appender都继承自SkeletonAppender类,该类对Appender接口提供了最基本的实现,并且引入了Threshold的概念,即所有的比当前Appender定义的日志级别阀指要大的日志才会记录下来。

 1  public   abstract   class  AppenderSkeleton  implements  Appender, OptionHandler {
 2       protected  Layout layout;
 3       protected  String name;
 4       protected  Priority threshold;
 5       protected  ErrorHandler errorHandler  =   new  OnlyOnceErrorHandler();
 6       protected  Filter headFilter;
 7       protected  Filter tailFilter;
 8       protected   boolean  closed  =   false ;
 9       public  AppenderSkeleton() {
10           super ();
11      }
12       public   void  activateOptions() {
13      }
14       abstract   protected   void  append(LoggingEvent event);
15       public   boolean  isAsSevereAsThreshold(Priority priority) {
16           return  ((threshold  ==   null ||  priority.isGreaterOrEqual(threshold));
17      }
18       public   synchronized   void  doAppend(LoggingEvent event) {
19           if  (closed) {
20              LogLog.error( " Attempted to append to closed appender named [ "
21                       +  name  +   " ]. " );
22               return ;
23          }
24           if  ( ! isAsSevereAsThreshold(event.getLevel())) {
25               return ;
26          }
27          Filter f  =   this .headFilter;
28          FILTER_LOOP:  while  (f  !=   null ) {
29               switch  (f.decide(event)) {
30               case  Filter.DENY:
31                   return ;
32               case  Filter.ACCEPT:
33                   break  FILTER_LOOP;
34               case  Filter.NEUTRAL:
35                  f  =  f.getNext();
36              }
37          }
38           this .append(event);
39      }
40  public   void  finalize() {
41           if  ( this .closed)
42               return ;
43          LogLog.debug( " Finalizing appender named [ "   +  name  +   " ]. " );
44          close();
45      }
46  }

SkeletonAppender实现了doAppend()方法,它首先检查日志级别是否要比threshold要大;然后如果注册了Filter,则使用Filter对LoggingEvent实例进行过滤,如果Filter返回Filter.DENY则doAppend()退出,否则执行append()方法,该方法由子类实现。

在Log4J中,Filter组成一条链,它定了以decide()方法,由子类实现,若返回DENY则日志不会被记录、NEUTRAL则继续检查下一个Filter实例、ACCEPT则Filter通过,继续执行后面的写日志操作。使用Filter可以为Appender加入一些出了threshold以外的其他逻辑,由于它本身是链状的,而且它的执行是横跨在Appender的doAppend方法中,因而这也是一个典型的AOP的概念。Filter的实现将会在下一小节中讲解。

SkeletonAppender还重写了finalize()方法,这是因为Log4J本身作为一个组件,它可能还是通过其他组件如commons-logging或slf4j组件间接的引入,因而使用它的程序不应该对它存在依赖的,然而在程序退出之前所有的Appender需要调用close()方法以释放它所占据的资源,为了不在使用Log4J的程序手动的close()的方法,以减少Log4J代码的侵入性,因而Log4J将close()的方法调用加入到finalize()方法中,即在垃圾回收器回收Appender实例时就会调用它的close()方法。

WriterAppender类和ConsoleAppender类

WriterAppender将日志写入Java IO中,它继承自SkeletonAppender类。它引入了三个字段:immediateFlush,指定没写完一条日志后,即将日志内容刷新到设备中,虽然这么做会有一点性能上的损失,但是如果不怎么做,则会出现在程序异常终止的时候无法看到部分日志信息,而经常这些丢失的日志信息要用于分析为什么会出现异常终止的情况,因而一般推荐将该值设置为true,即默认值;econding用于定义日志文本的编码方式;qw定义写日志的writer,它可以是文件或是控制台等Java IO支持的流。

在写日志文本前,WriterAppender还会做其他检查,如该Appender不能已经closed、qw和layout必须有值等,而后才可以将layout格式化后的日志行写入设备中。若layout本身不处理异常问题,则有Appender处理异常问题。最后如果每行日志需要刷新,则调用刷新操作。

 1  public   class  WriterAppender  extends  AppenderSkeleton {
 2       protected   boolean  immediateFlush  =   true ;
 3       protected  String encoding;
 4       protected  QuietWriter qw;
 5       public  WriterAppender() {
 6      }
 7       public  WriterAppender(Layout layout, OutputStream os) {
 8           this (layout,  new  OutputStreamWriter(os));
 9      }
10       public  WriterAppender(Layout layout, Writer writer) {
11           this .layout  =  layout;
12           this .setWriter(writer);
13      }
14       public   void  append(LoggingEvent event) {
15           if  ( ! checkEntryConditions()) {
16               return ;
17          }
18          subAppend(event);
19      }
20       protected   boolean  checkEntryConditions() {
21           if  ( this .closed) {
22              LogLog.warn( " Not allowed to write to a closed appender. " );
23               return   false ;
24          }
25           if  ( this .qw  ==   null ) {
26              errorHandler
27                      .error( " No output stream or file set for the appender named [ "
28                               +  name  +   " ]. " );
29               return   false ;
30          }
31           if  ( this .layout  ==   null ) {
32              errorHandler.error( " No layout set for the appender named [ "   +  name
33                       +   " ]. " );
34               return   false ;
35          }
36           return   true ;
37      }
38       protected   void  subAppend(LoggingEvent event) {
39           this .qw.write( this .layout.format(event));
40           if  (layout.ignoresThrowable()) {
41              String[] s  =  event.getThrowableStrRep();
42               if  (s  !=   null ) {
43                   int  len  =  s.length;
44                   for  ( int  i  =   0 ; i  <  len; i ++ ) {
45                       this .qw.write(s[i]);
46                       this .qw.write(Layout.LINE_SEP);
47                  }
48              }
49          }
50           if  (shouldFlush(event)) {
51               this .qw.flush();
52          }
53      }
54       public   boolean  requiresLayout() {
55           return   true ;
56      }
57  }

ConsoleAppender继承自WriterAppender,它只是简单的将System.out或System.err实例传递给WriterAppender以构建相应的writer,最后实现将日志写入到控制台中。

Filter类

在Log4J中,Filter组成一条链,它定了以decide()方法,由子类实现,若返回DENY则日志不会被记录、NEUTRAL则继续检查下一个Filter实例、ACCEPT则Filter通过,继续执行后面的写日志操作。使用Filter可以为Appender加入一些出了threshold以外的其他逻辑,由于它本身是链状的,而且它的执行是横跨在Appender的doAppend方法中,因而这也是一个典型的AOP的概念。

 1  public   abstract   class  Filter  implements  OptionHandler {
 2       public  Filter next;
 3       public   static   final   int  DENY  =   - 1 ;
 4       public   static   final   int  NEUTRAL  =   0 ;
 5       public   static   final   int  ACCEPT  =   1 ;
 6       public   void  activateOptions() {
 7      }
 8       abstract   public   int  decide(LoggingEvent event);
 9       public   void  setNext(Filter next) {
10           this .next  =  next;
11      }
12       public  Filter getNext() {
13           return  next;
14      }
15  }

Log4J本身提供了四个Filter:DenyAllFilter、LevelMatchFilter、LevelRangeFilter、StringMatchFilter。

DenyAllFilter只是简单的在decide()中返回DENY值,可以将其应用在Filter链尾,实现如果之前的Filter都没有通过,则该LoggingEvent没有通过,类似或的操作:

1  public   class  DenyAllFilter  extends  Filter {
2       public   int  decide(LoggingEvent event) {
3           return  Filter.DENY;
4      }
5  }

StringMatchFilter通过日志消息中的字符串来判断Filter后的状态:

 1  public   class  StringMatchFilter  extends  Filter {
 2       boolean  acceptOnMatch  =   true ;
 3      String stringToMatch;
 4       public   int  decide(LoggingEvent event) {
 5          String msg  =  event.getRenderedMessage();
 6           if  (msg  ==   null   ||  stringToMatch  ==   null )
 7               return  Filter.NEUTRAL;
 8           if  (msg.indexOf(stringToMatch)  ==   - 1 ) {
 9               return  Filter.NEUTRAL;
10          }  else  {  //  we've got a match
11               if  (acceptOnMatch) {
12                   return  Filter.ACCEPT;
13              }  else  {
14                   return  Filter.DENY;
15              }
16          }
17      }
18  }

LevelMatchFilter判断日志级别是否和设置的级别匹配以决定Filter后的状态:

 1  public   class  LevelMatchFilter  extends  Filter {
 2       boolean  acceptOnMatch  =   true ;    
 3  Level levelToMatch;
 4       public   int  decide(LoggingEvent event) {
 5           if  ( this .levelToMatch  ==   null ) {
 6               return  Filter.NEUTRAL;
 7          }
 8           boolean  matchOccured  =   false ;
 9           if  ( this .levelToMatch.equals(event.getLevel())) {
10              matchOccured  =   true ;
11          }
12           if  (matchOccured) {
13               if  ( this .acceptOnMatch)
14                   return  Filter.ACCEPT;
15               else
16                   return  Filter.DENY;
17          }  else  {
18               return  Filter.NEUTRAL;
19          }
20      }
21  }

LevelRangeFilter判断日志级别是否在设置的级别范围内以决定Filter后的状态:

 1  public   class  LevelRangeFilter  extends  Filter {
 2       boolean  acceptOnMatch  =   false ;
 3      Level levelMin;
 4      Level levelMax;
 5       public   int  decide(LoggingEvent event) {
 6           if  ( this .levelMin  !=   null ) {
 7               if  (event.getLevel().isGreaterOrEqual(levelMin)  ==   false ) {
 8                   return  Filter.DENY;
 9              }
10          }
11           if  ( this .levelMax  !=   null ) {
12               if  (event.getLevel().toInt()  >  levelMax.toInt()) {
13                   return  Filter.DENY;
14              }
15          }
16           if  (acceptOnMatch) {
17               return  Filter.ACCEPT;
18          }  else  {
19               return  Filter.NEUTRAL;
20          }
21      }
22  }

总结

这一系列终于是结束了。本文主要介绍了Log4J核心类的实现和他们之间的交互关系。涉及到各个模块本身的其他详细信息将会在接下来的小节中详细介绍,如LogRepository与配置信息、Appender类结构的详细信息、Layout类结构的详细信息以及部分LoggingEvent提供的高级功能。而像Level、Logger本身,由于内容不多,已经在这一小节中全部介绍完了。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1月前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
358 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
|
2月前
|
XML JSON Java
Logback 与 log4j2 性能对比:谁才是日志框架的性能王者?
【10月更文挑战第5天】在Java开发中,日志框架是不可或缺的工具,它们帮助我们记录系统运行时的信息、警告和错误,对于开发人员来说至关重要。在众多日志框架中,Logback和log4j2以其卓越的性能和丰富的功能脱颖而出,成为开发者们的首选。本文将深入探讨Logback与log4j2在性能方面的对比,通过详细的分析和实例,帮助大家理解两者之间的性能差异,以便在实际项目中做出更明智的选择。
335 3
|
3月前
|
Java
日志框架log4j打印异常堆栈信息携带traceId,方便接口异常排查
日常项目运行日志,异常栈打印是不带traceId,导致排查问题查找异常栈很麻烦。
|
4月前
|
XML Java Maven
Spring5入门到实战------16、Spring5新功能 --整合日志框架(Log4j2)
这篇文章是Spring5框架的入门到实战教程,介绍了Spring5的新功能——整合日志框架Log4j2,包括Spring5对日志框架的通用封装、如何在项目中引入Log4j2、编写Log4j2的XML配置文件,并通过测试类展示了如何使用Log4j2进行日志记录。
Spring5入门到实战------16、Spring5新功能 --整合日志框架(Log4j2)
|
5月前
|
Java 测试技术 Apache
《手把手教你》系列基础篇(八十六)-java+ selenium自动化测试-框架设计基础-Log4j实现日志输出(详解教程)
【7月更文挑战第4天】Apache Log4j 是一个广泛使用的 Java 日志框架,它允许开发者控制日志信息的输出目的地、格式和级别。Log4j 包含三个主要组件:Loggers(记录器)负责生成日志信息,Appenders(输出源)确定日志输出的位置(如控制台、文件、数据库等),而 Layouts(布局)则控制日志信息的格式。通过配置 Log4j,可以灵活地定制日志记录行为。
65 4
|
4月前
|
存储 运维 Java
SpringBoot使用log4j2将日志记录到文件及自定义数据库
通过上述步骤,你可以在Spring Boot应用中利用Log4j2将日志输出到文件和数据库中。这不仅促进了良好的日志管理实践,也为应用的监控和故障排查提供了强大的工具。强调一点,配置文件和代码的具体实现可能需要根据应用的实际需求和运行环境进行调优和修改,始终记住测试配置以确保一切运行正常。
723 0
|
5月前
|
运维 Java Apache
Java中的日志框架:Log4j与SLF4J详解
Java中的日志框架:Log4j与SLF4J详解
|
5月前
|
XML Java 测试技术
《手把手教你》系列基础篇(八十八)-java+ selenium自动化测试-框架设计基础-Log4j 2实现日志输出-下篇(详解教程)
【7月更文挑战第6天】本文介绍了如何使用Log4j2将日志输出到文件中,重点在于配置文件的结构和作用。配置文件包含两个主要部分:`appenders`和`loggers`。`appenders`定义了日志输出的目标,如控制台(Console)或其他文件,如RollingFile,设置输出格式和策略。`loggers`定义了日志记录器,通过`name`属性关联到特定的类或包,并通过`appender-ref`引用`appenders`来指定输出位置。`additivity`属性控制是否继承父logger的配置。
51 0
|
5月前
|
XML Java 测试技术
《手把手教你》系列基础篇(八十七)-java+ selenium自动化测试-框架设计基础-Log4j 2实现日志输出-上篇(详解教程)
【7月更文挑战第5天】Apache Log4j 2是一个日志框架,它是Log4j的升级版,提供了显著的性能提升,借鉴并改进了Logback的功能,同时修复了Logback架构中的问题。Log4j2的特点包括API与实现的分离,支持SLF4J,自动重新加载配置,以及高级过滤选项。它还引入了基于lambda表达式的延迟评估,低延迟的异步记录器和无垃圾模式。配置文件通常使用XML,但也可以是JSON或YAML,其中定义了日志级别、输出目的地(Appender)和布局(Layout)。
54 0
|
7月前
|
Java 数据库
log4j:WARN Please initialize the log4j system prop
log4j:WARN Please initialize the log4j system prop
64 1