获取方法的调用者

时间:2021-07-21 07:33:59

问题的引入

在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有一个Thread类,Thread.currentThread()方法获取当前线程,能不能通过这个当前线程获取当前运行的Method和Class呢?很遗憾,如果是JDK1.4或以下版本,那么没有这样的方法。(JDK1.5的情况见后文)
  • 思路二:当系统抛出Exception的时候,总是打印出一串的信息,显示Exception发生的位置,和一层一层的调用关系。也可以自己调用Exception的printStackTrace()方法来打印这些信息。这就是当前线程运行栈的信息了。

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时代的作品,其中相关代码如下:

/**
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);
    …. // 这里的代码省略
}

可以看到整体的实现思路:
首先,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 caller’s 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"))
        // We’ve found the relevant frame.// 这里的代码省略
    }
    // We haven’t 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 的相关实现

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.
*
*
If there is a security manager, and this thread is not
* the current thread, then the security manager’s
* checkPermission method is called with a
* RuntimePermission("getStackTrace") permission
* to see if it’s ok to get the stack trace.
*
*
Some virtual machines may, under some circumstances, omit one
* or more stack frames from the stack trace. In the extreme case,
* a virtual machine that has no stack trace information concerning
* this thread is permitted to return a zero-length array from this
* method.
*
* @return an array of StackTraceElement,
* each represents one stack frame.
*
* @since 1.5
*/
public StackTraceElement[] getStackTrace() {
    if (this != Thread.currentThread()) {
        // check for getStackTrace permission
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkPermission(SecurityConstants.GET_STACK_TRACE_PERMISSION);
        }
    }
    if (!isAlive()) {
        return EMPTY_STACK_TRACE;
    }
    Thread[] threads = new Thread[1];
    threads[0] = this;
    StackTraceElement[][] result = dumpThreads(threads);
    return result[0];
}
 
/**
* Returns a map of stack traces for all live threads.
*
* @since 1.5
*/
public static Map getAllStackTraces() {
    // check for getStackTrace permission
    // Get a snapshot of the list of all threads
}

直接获取调用类名的方法

sun.reflect.Reflection的getCallerClass()方法的说明如下:

/** Returns the class of the method realFramesToSkip
frames up the stack (zero-based), ignoring frames associated
with java.lang.reflect.Method.invoke() and its implementation.
The first frame is that associated with this method, so
getCallerClass(0) returns the Class object for
sun.reflect.Reflection. Frames associated with
java.lang.reflect.Method.invoke() and its implementation are
completely ignored and do not count toward the number of "real"
frames skipped. */
public static native Class getCallerClass(int realFramesToSkip);

这是一个native方法。原理也是根据StackFrame(运行栈)获取相应类的信息。这个方法直接返回一个Class名字。参数realFramesToSkip用来选取所需要的Stack层次,所以可以用这个方法获得任何层次的上的调用类名。