即获取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,以从资源文件中获取消息内容。为了使用这两个方法,需要设置资源文件。同样,资源文件也是可以从父节点中继承的。
 
另外,在实际开发中经常会遇到要把日志信息同时写到不同地方,如同时写入文件和控制台,因而一个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 }
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);