即获取logger实例->判断logger实例对应的日志记录级别是否要比请求的级别低->若是调用forcelog记录日志->创建loggingevent实例->将loggingevent实例传递给appender->appender调用layout实例格式化日志消息->appender将格式化后的日志信息写入该appender对应的日志输出中。
包含log4j其他模块类的更详细序列图如下:
在简单的介绍了log4j各个模块类的作用后,以下将详细的介绍各个模块的具体作用以及代码实现。
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是对日志级别的抽象,目前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从概念以及字面上来说它就是一个logger实例的容器:一方面相同名字的logger实例只需要创建一次,在后面的使用中,只需要从这个容器中取即可;另一方面,logger容器可以存放从配置文件中解析出来的信息,从而使配置信息可以无缝的应用到log4j内部系统中;最后logger容器还为维护logger的树状层次结构提供了方面,每个logger只维护父节点的信息,有了logger容器的存在则可以很容易的找到一个新的logger实例的父节点;关于logger容器将在下一节中详细讲解。
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);