谁调用了我?

152 阅读2分钟

前言

在系统抛出异常后,往往都会伴随代码调用栈,以便我们调试程序,找出问题所在原因。

Thread类提供了一个dumpStack()方法,他会简单打印当前调用栈信息,但有时候我们更希望使用Thread.currentThread().getStackTrace()这种方式来手动获取信息。

for (StackTraceElement stackTraceElement : Thread.currentThread().getStackTrace()) {
    System.out.println(stackTraceElement.getMethodName());
}

但在JDK 9中,新增加了StackWalker来帮助我们获取这些信息,和原本方式有个最大不同就是,Thread.currentThread().getStackTrace()最顶层的一帧是系统的getStackTrace()方法信息,而StackWalker就是当前方法,这更符合逻辑。

List<StackWalker.StackFrame> stack = StackWalker.getInstance().walk(s ->
        s.collect(Collectors.toList()));
for (StackWalker.StackFrame stackFrame : stack) {
    System.out.println(stackFrame.getMethodName());
}

使用

从上面也已经看出,需要通过StackWalker的getInstance()方法来获取实例,但也可以全局使用一个,并通过其walk()方法来获取调用栈。

他有几个重载方法,是为了控制获取信息的深度,其中Option有三个值。

选项描述
RETAIN_CLASS_REFERENCE保留Class对象。
SHOW_HIDDEN_FRAMES显示所有隐藏的帧。
SHOW_REFLECT_FRAMES显示所有反射帧。

比如你想获取调用栈时所在的类,那么需要加入RETAIN_CLASS_REFERENCE选项。

比如下面是获取调用栈中所在类名.方法名:行号的一个例子

public class Main {
    private static StackWalker stackWalker= StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
    public static void main(String[] args) throws InterruptedException {
        print("");
    }
    private static void print(String msg){
        List<StackWalker.StackFrame> stack = stackWalker.walk(s ->
                s.collect(Collectors.toList()));
        for (StackWalker.StackFrame stackFrame : stack) {
            System.out.println(stackFrame.getDeclaringClass().getSimpleName() +"."+stackFrame.getMethodName()+":"+stackFrame.getLineNumber());
        }
    }
}

输出如下

Main.print:14
Main.main:10

你会发现walk方法借助着Stream给了我们极大的方便,他采用一个函数,传入Stream<StackFrame>并根据我们的需要映射到任何想要的内容,比如只获取谁调用了我的信息。

public class Main {
    private static StackWalker stackWalker= StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
    public static void main(String[] args) throws InterruptedException {
        print("a");
    }
    private static void print(String msg){
        StackWalker.StackFrame stack = stackWalker.walk(s -> s.skip(1).findFirst().orElse(null));
        System.out.println(stack);
    }
}

在或者取前十个信息。

public class Main {
    private static StackWalker stackWalker= StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
    public static void main(String[] args) throws InterruptedException {
        print("a");
    }
    private static void print(String msg){
        List<StackWalker.StackFrame> stack = stackWalker.walk(s ->
                s.limit(10).collect(Collectors.toList()));
        for (StackWalker.StackFrame stackFrame : stack) {
            System.out.println(stackFrame.getDeclaringClass().getSimpleName() +"."+stackFrame.getMethodName()+":"+stackFrame.getLineNumber());
        }
    }
}

但如果你想简单打印信息,只需要一行就足以。

StackWalker.getInstance().forEach(System.out::println);

或者只获取调用当前方法的类信息。

System.out.println(StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass().getSimpleName());

隐藏帧

JVM 会为 lambda 表达式创建一些隐藏帧,如果不附加SHOW_HIDDEN_FRAMES选项,这是看不到的。

private static void print(String msg){
    Runnable r = () -> {
        StackWalker.getInstance(StackWalker.Option.SHOW_HIDDEN_FRAMES).forEach(System.out::println);
    };
    r.run();
}

反射帧

默认情况下,反射帧是隐藏的,需要加入SHOW_REFLECT_FRAMES才能获取,但从这个反射帧上可以迁出另一个问题,如何判断当前方法是不是通过反射调用的。

public class Main {
    public static void main(String[] args) throws InterruptedException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Test test = Test.class.newInstance();
        Test.class.getDeclaredMethod("print").invoke(test);
    }
    static class Test{
        public void print(){
            StackWalker.getInstance(StackWalker.Option.SHOW_REFLECT_FRAMES).forEach(System.out::println);
        }
    }
}

输出如下。

Main$Test.print(Main.java:21)
java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
java.base/java.lang.reflect.Method.invoke(Method.java:578)
Main.main(Main.java:14)