前言
在系统抛出异常后,往往都会伴随代码调用栈,以便我们调试程序,找出问题所在原因。
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)