1. 概述
在这篇简短的文章中,我们将探讨Java 9的栈步行API。
新功能提供了对栈帧流的访问权限,使得我们可以轻松浏览直接堆栈,并充分利用Java 8的强大流式API。
2. 栈步行的优势
在Java 8中,Throwable::getStackTrace
和Thread::getStackTrace
返回一个StackTraceElement
数组。没有太多手动代码,我们无法丢弃不想要的帧并只保留感兴趣的帧。
此外,Thread::getStackTrace
可能返回一个部分堆栈跟踪。这是因为规范允许虚拟机实现为了性能而省略一些堆栈帧。
在Java 9中,使用StackWalker
的walk
方法,我们可以遍历我们感兴趣的几个帧或完整的堆栈跟踪。
当然,新功能是线程安全的,这允许多个线程共享单个StackWalker
实例以访问各自的堆栈。
正如JEP-259中所述,JVM将增强其功能,以便在需要时高效地懒加载额外的堆栈帧。
3. StackWalker
的使用
让我们从创建包含链式方法调用的类开始:
public class StackWalkerDemo {
public void methodOne() {
this.methodTwo();
}
public void methodTwo() {
this.methodThree();
}
public void methodThree() {
// stack walking code
}
}
3.1. 获取完整堆栈跟踪
接下来,添加一些栈步行代码:
public void methodThree() {
List<StackFrame> stackTrace = StackWalker.getInstance()
.walk(this::walkExample);
}
StackWalker::walk
方法接受一个函数引用,为当前线程创建一个StackFrame
流,应用函数到流中,然后关闭流。
现在定义StackWalkerDemo::walkExample
方法:
public List<StackFrame> walkExample(Stream<StackFrame> stackFrameStream) {
return stackFrameStream.collect(Collectors.toList());
}
这个方法只是收集StackFrame
并将其作为List<StackFrame>
返回。要测试这个示例,请运行JUnit测试:
@Test
public void giveStalkWalker_whenWalkingTheStack_thenShowStackFrames() {
new StackWalkerDemo().methodOne();
}
之所以将其作为JUnit测试运行,是因为我们需要更多的堆栈帧:
class com.baeldung.java9.stackwalker.StackWalkerDemo#methodThree, Line 20
class com.baeldung.java9.stackwalker.StackWalkerDemo#methodTwo, Line 15
class com.baeldung.java9.stackwalker.StackWalkerDemo#methodOne, Line 11
class com.baeldung.java9.stackwalker
.StackWalkerDemoTest#giveStalkWalker_whenWalkingTheStack_thenShowStackFrames, Line 9
class org.junit.runners.model.FrameworkMethod$1#runReflectiveCall, Line 50
class org.junit.internal.runners.model.ReflectiveCallable#run, Line 12
...more org.junit frames...
class org.junit.runners.ParentRunner#run, Line 363
class org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference#run, Line 86
...more org.eclipse frames...
class org.eclipse.jdt.internal.junit.runner.RemoteTestRunner#main, Line 192
在整个堆栈跟踪中,我们只对顶部四个帧感兴趣。其余的来自org.junit
和*org.eclipse
的帧不过是噪音帧。
3.2. 过滤StackFrame
让我们改进栈步行代码并移除噪音:
public List<StackFrame> walkExample2(Stream<StackFrame> stackFrameStream) {
return stackFrameStream
.filter(f -> f.getClassName().contains("com.baeldung"))
.collect(Collectors.toList());
}
利用流式API的力量,我们只保留感兴趣的帧,这将清除噪音,只保留堆栈日志中的前四个行:
class com.baeldung.java9.stackwalker.StackWalkerDemo#methodThree, Line 27
class com.baeldung.java9.stackwalker.StackWalkerDemo#methodTwo, Line 15
class com.baeldung.java9.stackwalker.StackWalkerDemo#methodOne, Line 11
class com.baeldung.java9.stackwalker
.StackWalkerDemoTest#giveStalkWalker_whenWalkingTheStack_thenShowStackFrames, Line 9
现在让我们找出启动调用的JUnit测试:
public String walkExample3(Stream<StackFrame> stackFrameStream) {
return stackFrameStream
.filter(frame -> frame.getClassName()
.contains("com.baeldung") && frame.getClassName().endsWith("Test"))
.findFirst()
.map(f -> f.getClassName() + "#" + f.getMethodName()
+ ", Line " + f.getLineNumber())
.orElse("Unknown caller");
}
请注意,这里我们只对单个StackFrame
感兴趣,它被映射到一个String
。输出将是包含StackWalkerDemoTest
类的行。
3.3. 识别反射帧
为了捕获默认隐藏的反射帧,StackWalker
需要配置额外选项SHOW_REFLECT_FRAMES
:
List<StackFrame> stackTrace = StackWalker
.getInstance(StackWalker.Option.SHOW_REFLECT_FRAMES)
.walk(this::walkExample);
使用此选项,所有包括Method.invoke()
和Constructor.newInstance()
在内的反射帧都将被捕获:
com.baeldung.java9.stackwalker.StackWalkerDemo#methodThree, Line 40
com.baeldung.java9.stackwalker.StackWalkerDemo#methodTwo, Line 16
com.baeldung.java9.stackwalker.StackWalkerDemo#methodOne, Line 12
com.baeldung.java9.stackwalker
.StackWalkerDemoTest#giveStalkWalker_whenWalkingTheStack_thenShowStackFrames, Line 9
jdk.internal.reflect.NativeMethodAccessorImpl#invoke0, Line -2
jdk.internal.reflect.NativeMethodAccessorImpl#invoke, Line 62
jdk.internal.reflect.DelegatingMethodAccessorImpl#invoke, Line 43
java.lang.reflect.Method#invoke, Line 547
org.junit.runners.model.FrameworkMethod$1#runReflectiveCall, Line 50
...eclipse and junit frames...
org.eclipse.jdt.internal.junit.runner.RemoteTestRunner#main, Line 192
如图所示,jdk.internal
帧是由SHOW_REFLECT_FRAMES
选项捕获的新帧。
3.4. 捕获隐藏帧
除了反射帧外,JVM实现可能会选择隐藏特定实现的帧。
然而,这些帧并未对StackWalker
隐藏:
Runnable r = () -> {
List<StackFrame> stackTrace2 = StackWalker
.getInstance(StackWalker.Option.SHOW_HIDDEN_FRAMES)
.walk(this::walkExample);
printStackTrace(stackTrace2);
};
r.run();
在这个例子中,我们将lambda引用赋值给一个Runnable
。唯一的原因是JVM会为lambda表达式创建一些隐藏帧。
这在堆栈跟踪中非常明显:
com.baeldung.java9.stackwalker.StackWalkerDemo#lambda$0, Line 47
com.baeldung.java9.stackwalker.StackWalkerDemo$$Lambda$39/924477420#run, Line -1
com.baeldung.java9.stackwalker.StackWalkerDemo#methodThree, Line 50
com.baeldung.java9.stackwalker.StackWalkerDemo#methodTwo, Line 16
com.baeldung.java9.stackwalker.StackWalkerDemo#methodOne, Line 12
com.baeldung.java9.stackwalker
.StackWalkerDemoTest#giveStalkWalker_whenWalkingTheStack_thenShowStackFrames, Line 9
jdk.internal.reflect.NativeMethodAccessorImpl#invoke0, Line -2
jdk.internal.reflect.NativeMethodAccessorImpl#invoke, Line 62
jdk.internal.reflect.DelegatingMethodAccessorImpl#invoke, Line 43
java.lang.reflect.Method#invoke, Line 547
org.junit.runners.model.FrameworkMethod$1#runReflectiveCall, Line 50
...junit and eclipse frames...
org.eclipse.jdt.internal.junit.runner.RemoteTestRunner#main, Line 192
堆栈跟踪的顶部两个帧是JVM内部创建的lambda代理帧。值得注意的是,我们在上一个示例中捕获的反射帧仍然保留在SHOW_HIDDEN_FRAMES
选项下。这是因为SHOW_HIDDEN_FRAMES
是SHOW_REFLECT_FRAMES
的超集。
3.5. 识别调用类
RETAIN_CLASS_REFERENCE
选项保留了StackWalker
遍历的所有StackFrame
中的Class
对象。这使我们能够调用StackWalker::getCallerClass
和StackFrame::getDeclaringClass
方法。
让我们使用StackWalker::getCallerClass
方法来识别调用类:
public void findCaller() {
Class<?> caller = StackWalker
.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.getCallerClass();
System.out.println(caller.getCanonicalName());
}
这次,我们将在单独的JUnit测试中直接调用这个方法:
@Test
public void giveStalkWalker_whenInvokingFindCaller_thenFindCallingClass() {
new StackWalkerDemo().findCaller();
}
caller.getCanonicalName()
的输出将是:
com.baeldung.java9.stackwalker.StackWalkerDemoTest
请注意,不应从堆栈底部的方法调用StackWalker::getCallerClass
,因为这会导致IllegalCallerException
抛出。
4. 总结
通过这篇文章,我们看到了如何使用StackWalker
的强大功能以及流式API轻松处理StackFrame
。
当然,我们还可以探索其他功能,如跳过、删除和限制StackFrame
。官方文档中包含了一些针对更多用例的坚实示例。
一如既往,您可以在GitHub上获取本文的完整源代码。