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

Log4J将写日志功能抽象成七个核心类/接口:Logger、LoggerRepository、Level、LoggingEvent、Appender、Layout、ObjectRender。其类图如下:

更详细的,实现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



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)

因而我们就可以通过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:



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本身,由于内容不多,已经在这一小节中全部介绍完了。

深入Log4J源码之Log4J Core的更多相关文章

  1. log4j源码解析-文件解析

    承接前文log4j源码解析,前文主要介绍了log4j的文件加载方式以及Logger对象创建.本文将在此基础上具体看下log4j是如何解析文件并输出我们所常见的日志格式 附例 文件的加载方式,我们就选举 ...

  2. java 日志体系(四)log4j 源码分析

    java 日志体系(四)log4j 源码分析 logback.log4j2.jul 都是在 log4j 的基础上扩展的,其实现的逻辑都差不多,下面以 log4j 为例剖析一下日志框架的基本组件. 一. ...

  3. 【春华秋实】深入源码理解.NET Core中Startup的注册及运行

      写在前面 开发.NET Core应用,直接映入眼帘的就是Startup类和Program类,它们是.NET Core应用程序的起点.通过使用Startup,可以配置化处理所有向应用程序所做的请求的 ...

  4. log4j源码解析

    前言:本文将在slf4j的基础上解释log4j的应用,阅读本文前可先行阅读SLF4J源码解析-LoggerFactory(二) 前言概要 在前言中提到的slf4j的基础,其主要是通过logback的a ...

  5. Log4j源码解析--Layout类解析

    本文转载上善若水的博客,原文出处:http://www.blogjava.net/DLevin/archive/2012/07/04/382131.html.感谢作者的分享. Layout负责将Log ...

  6. Log4j源码解析--框架流程+核心解析

    OK,现在我们来研究Log4j的源码: 这篇博客有参照上善若水的博客,原文出处:http://www.blogjava.net/DLevin/archive/2012/06/28/381667.htm ...

  7. commons-logging + log4j源码分析

    分析之前先理清楚几个概念 Log4J = Log For Java SLF4J = Simple Logging Facade for Java 看到Facade首先想到的就是设计模式中的门面(Fac ...

  8. log4j源码阅读

    基于log4j1.2.17的源代码阅读 org.apache.log4j.xml.DOMConfigurator 类是log4j的xml配置文件初始化类 org.apache.log4j.Proper ...

  9. Log4j源码解析--LoggerRepository和Configurator解析

    本文转自上善若水的博客,原文出处:http://www.blogjava.net/DLevin/archive/2012/07/10/382678.html.感谢作者的无私分享. LoggerRepo ...

  10. Log4j源码分析

    一.slf4j和log4j的关系: 也就是说slf4j仅仅是一个为Java程序提供日志输出的统一接口,并不是一个具体的日志实现方案,就比如JDBC一样,只是一种规则而已.必须搭配具体的log实现方案比 ...

随机推荐

  1. IDEA 2023.2 最新安装使用教程(附激活码,亲测好用)

    申明:本教程 IDEA 补丁.补丁均收集于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除.若条件允许,希望大家购买正版 ! idea激活码使用教程 Step1 第一步下载IDEA软件 ID ...

  2. Angular 18+ 高级教程 – 目录

    请按顺序阅读 关于本教程 初识 Angular Get Started Angular Compiler (AKA ngc) Quick View Dependency Injection 依赖注入  ...

  3. 网络安全&密码学—python中的各种加密算法

    网络安全&密码学-python中的各种加密算法 一.简介 数据加密是一种保护数据安全的技术,通过将数据(明文)转换为不易被未经授权的人理解的形式(密文),以防止数据泄露.篡改或滥用.加密后的数 ...

  4. 数据库MySQL-安装、卸载、配置、登录、退出

    一.下载 下载链接:MySQL :: Download MySQL Community Server (Archived Versions) 二.安装(解压)  三.配置 1.添加环境变量 我的电脑- ...

  5. MyBatis——案例——查询-多条件查询-动态条件查询(关键字 if where)

    动态条件查询   SQL语句会随着用户的输入或外部条件的变化而变化,我们称为 动态SQL   MyBatis 对动态SQL有很强大的支撑:   if   choose(when,otherwise) ...

  6. C#|.net core 基础 - 深拷贝的五大类N种实现方式

    在实际应用中经常会有这样的需求:获取一个与原对象数据相同但是独立于原对象的精准副本,简单来说就是克隆一份,拷贝一份,复制一份和原对象一样的对象,但是两者各种修改不能互相影响.这一行为也叫深克隆,深拷贝 ...

  7. [OI] 平衡树

    1. 二叉查找树 二叉查找树的思想和优先队列比较像,都是把若干个数据按一定规则插到一棵树里,然后就可以维护特定的信息. 在优先队列的大根堆实现里,我们让每棵子树的根节点都大于它的儿子,这样就可以保证根 ...

  8. 7.flask 源码解析:session

    目录 一.flask 源码解析:session 1.1 session 简介 1.2 解析 1.2.1 请求过程 1.2.2 session 对象 1.2.3 签名算法 1.2.4 应答过程 1.3 ...

  9. MySQL数据的导入

    我们在帖子MySQL数据的导出 - brucexia - 博客园 (cnblogs.com)中讲了MySQL数据的导出,本文讲讲解MySQL数据的导入. MySQL数据的导入包括使用LOAD DATA ...

  10. 某物联网数智化园区行业基于 KubeSphere 的云原生实践

    公司简介 作为物联网 + 数智化园区一体化解决方案提供商,我们致力于为大中型园区.停车场提供软硬件平台,帮助园区运营者实现数字化.智能化运营. 在使用 K8s 之前我们使用传统的方式部署上线,使用 s ...