一、问题的引入
我们在java程序中使用日志功能(jdk log或者log4j)的时候,会发现log系统会自动帮我们打印出丰富的信息,格式一般如下:
[运行时间] [当前类名] [方法名]
info: [用户信息]
具体例子如tomcat启动信息:
jul 9, 2004 11:22:41 am org.apache.coyote.http11.http11protocol start
info: starting coyote http/1.1 on port 8080
看起来这毫无神奇之处,不就是打印了一条信息吗?但如果好奇心重一点,追寻后面的实现原理,会发现这确实很神奇。上面的log信息的[当前类名] [方法名]部分 不是用户自己添加的,而是log系统自动添加的。这意味着log系统能够自动判断当前执行语句是哪个类的哪个方法。这是如何做到的?
我们翻遍java.lang.reflection package,幻想着找到一个statement语句级别的reflection类,通过这个statement对象获得method,然后通过这个method获得declared class。这不就获得对应的class和method信息了吗?这是一个不错的构想,但也只能是一个构想;因为没有这个statement对象。
再想一下。对了,java不是有一个thread类吗?thread.currentthread()方法获取当前线程,我们能不能通过这个当前线程获取当前运行的method和class呢?很遗憾,如果你还在用jdk1.4或以下版本,那么找不到这样的方法。(jdk1.5的情况后面会讲)
再想一下。对了,我们都有很深刻的印象,当系统抛出exception的时候,总是打印出一串的信息,告诉我们exception发生的位置,和一层一层的调用关系。我们也可以自己调用exception的printstacktrace()方法来打印这些信息。这不就是当前线程运行栈的信息吗?找到了,就是它。
exception的printstacktrace()方法继承自throwable,那么我们来看一下,jdk的throwable的printstacktrace()方法是如何实现的。
我们先来看jdk1.3的源代码,会发现throwable.printstacktrace()方法调用了一个native printstacktrace0()方法。我们找不到任何线索,可以用在我们自己的java代码中。
那怎么办?throwable.printstacktrace()的输出结果字符串里面不是包含了当前线程运行栈的所有信息吗?我们可以从这个字符串中抽取自己需要的信息。jdk1.3的时代,也只能这么做了。
二、log4j 1.2的相关实现
log4j 1.2是jdk1.3时代的作品。我们来看相关源代码。
[code]
/**
instantiate location information based on a throwable. we
expect the throwable t, to be in the format
java.lang.throwable
...
at org.apache.log4j.patternlayout.format(patternlayout.java:413)
at org.apache.log4j.fileappender.doappend(fileappender.java:183)
at org.apache.log4j.category.callappenders(category.java:131)
at org.apache.log4j.category.log(category.java:512)
at callers.fully.qualified.classname.methodname(filename.java:74)
...
*/
public locationinfo(throwable t, string fqnofcallingclass) {
string s;
…
t.printstacktrace(pw);
s = sw.tostring();
sw.getbuffer().setlength(0);
…. // 这里的代码省略
}
[/code]
这里我们可以看到整体的实现思路。
首先,t.printstacktrace(pw); 获得stack trace字符串。这个t是 new throwable()的结果。用户程序调用log4j方法之后,log4j自己又进行了4次调用,然后才获得了 t = new throwable() :
at org.apache.log4j.patternlayout.format(patternlayout.java:413)
at org.apache.log4j.fileappender.doappend(fileappender.java:183)
at org.apache.log4j.category.callappenders(category.java:131)
at org.apache.log4j.category.log(category.java:512)
那么,往下走4行,就可以回到用户程序本身的调用信息:
at callers.fully.qualified.classname.methodname(filename.java:74)
这一行里面,类名、方法名、文件名、行号等信息全有了。解析这一行,就可以获得需要的所有信息。
三、jdk1.4 log的相关实现
log4j大获成功,sun决定在jdk1.4中引入这个log功能。
为了免去解析stacktrace字符串的麻烦,jdk1.4引入了一个新的类,stacktraceelement。
public final class stacktraceelement implements java.io.serializable {
// normally initialized by vm (public constructor added in 1.5)
private string declaringclass;
private string methodname;
private string filename;
private int linenumber;
可以看到,恰好包括类名、方法名、文件名、行号等信息。
我们来看jdk1.4 log的相关实现。
locationinfo.java 的infocaller方法(推算调用者)
// private method to infer the callers class and method names
private void infercaller() {
…
// get the stack trace.
stacktraceelement stack[] = (new throwable()).getstacktrace();
// first, search back to a method in the logger class.
…. // 这里的代码省略
// now search for the first frame before the "logger" class.
while (ix < stack.length) {
stacktraceelement frame = stack[ix];
string cname = frame.getclassname();
if (!cname.equals("java.util.logging.logger"))
// weve found the relevant frame.
… // 这里的代码省略
}
// we haven found a suitable frame, so just punt. this is
// ok as we are only committed to making a "best effort" here.
}
从注释中就可以看出实现思路。过程和log4j异曲同工。只是免去了解析字符串的麻烦。
四、log4j 1.3 alpha的相关实现
既然jdk1.4中引入了stacktraceelement类,log4j也要与时俱进。locationinfo类也有了相应的变化。
/**
instantiate location information based on a throwable. we
expect the throwable t, to be in the format
java.lang.throwable
...
at org.apache.log4j.patternlayout.format(patternlayout.java:413)
at org.apache.log4j.fileappender.doappend(fileappender.java:183)
at org.apache.log4j.category.callappenders(category.java:131)
at org.apache.log4j.category.log(category.java:512)
at callers.fully.qualified.classname.methodname(filename.java:74)
...
however, we can also deal with jit compilers that "lose" the
location information, especially between the parentheses.
*/
public locationinfo(throwable t, string fqnofinvokingclass) {
if(platforminfo.hasstacktraceelement()) {
stacktraceelementextractor.extract(this, t, fqnofinvokingclass);
} else {
legacyextractor.extract(this, t, fqnofinvokingclass);
}}
可以看到,log4j首先判断java平台是否支持stacktraceelement,如果是,那么用stacktraceelementextractor,否则使用原来的legacyextractor。
下面来看stacktraceelementextractor.java
/**
* a faster extractor based on stacktraceelements introduced in jdk 1.4.
*
* the present code uses reflection. thus, it should compile on all platforms.
*
* @author martin schulz
* @author ceki gülcü
*
*/
public class stacktraceelementextractor {
protected static boolean havestacktraceelement = false;
private static method getstacktrace = null;
private static method getclassname = null;
private static method getfilename = null;
private static method getmethodname = null;
private static method getlinenumber = null;
…. // 以下代码省略
可以看到,log4j 1.3仍然兼容jdk1.3,而且为jdk1.4也做了相应的优化。
五、jdk1.5的thread stack trace
jdk1.5在thread类里面引入了getstacktrace()和getallstacktraces()两个方法。这下子,我们不用 (new throwable()).getstacktrace ();可以调用
thread.getcurrentthread().getstacktrace()来获得当前线程的运行栈信息。不仅如此,只要权限允许,还可以获得其它线程的运行栈信息。
/**
* returns an array of stack trace elements representing the stack dump
* of this thread. this method will return a zero-length array if
* this thread has not started or has terminated.
* if the returned array is of non-zero length then the first element of
* the array represents the top of the stack, which is the most recent
* method invocation in the sequence. the last element of the array
* represents the bottom of the stack, which is the least recent method
* invocation in the sequence.
*
*
闽公网安备 35060202000074号