Java 9 StackWalker API简介
1. 简介
在这篇快速文章中,我们将了解 Java 9 的StackWalking API 。 新功能提供了对StackFrame的Stream的访问,使我们能够轻松地直接浏览堆栈并充分利用Java 8中强大的Stream API 。
2. StackWalker的优点
在 Java 8 中,Throwable::getStackTrace和Thread::getStackTrace返回一个StackTraceElement数组。如果没有大量手动代码,就无法丢弃不需要的帧而只保留我们感兴趣的帧。
除此之外,Thread::getStackTrace可能会返回部分堆栈跟踪。这是因为规范允许 VM 实现为了性能而省略一些堆栈帧。
在 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的Stream,将该函数应用于Stream,然后关闭Stream。
现在让我们定义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.blogdemo.java9.stackwalker.StackWalkerDemo#methodThree, Line 20
class com.blogdemo.java9.stackwalker.StackWalkerDemo#methodTwo, Line 15
class com.blogdemo.java9.stackwalker.StackWalkerDemo#methodOne, Line 11
class com.blogdemo.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.blogdemo"))
.collect(Collectors.toList());
}
使用Stream API 的强大功能,我们只保留我们感兴趣的帧。这将清除噪音,在堆栈日志中保留前四行:
class com.blogdemo.java9.stackwalker.StackWalkerDemo#methodThree, Line 27
class com.blogdemo.java9.stackwalker.StackWalkerDemo#methodTwo, Line 15
class com.blogdemo.java9.stackwalker.StackWalkerDemo#methodOne, Line 11
class com.blogdemo.java9.stackwalker
.StackWalkerDemoTest#giveStalkWalker_whenWalkingTheStack_thenShowStackFrames, Line 9
现在让我们确定发起调用的 JUnit 测试:
public String walkExample3(Stream<StackFrame> stackFrameStream) {
return stackFrameStream
.filter(frame -> frame.getClassName()
.contains("com.blogdemo") && frame.getClassName().endsWith("Test"))
.findFirst()
.map(f -> f.getClassName() + "#" + f.getMethodName()
+ ", Line " + f.getLineNumber())
.orElse("Unknown caller");
}
请注意,在这里,我们只对映射到String的单个StackFrame感兴趣。输出只会是包含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.blogdemo.java9.stackwalker.StackWalkerDemo#methodThree, Line 40
com.blogdemo.java9.stackwalker.StackWalkerDemo#methodTwo, Line 16
com.blogdemo.java9.stackwalker.StackWalkerDemo#methodOne, Line 12
com.blogdemo.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.blogdemo.java9.stackwalker.StackWalkerDemo#lambda$0, Line 47
com.blogdemo.java9.stackwalker.StackWalkerDemo$$Lambda$39/924477420#run, Line -1
com.blogdemo.java9.stackwalker.StackWalkerDemo#methodThree, Line 50
com.blogdemo.java9.stackwalker.StackWalkerDemo#methodTwo, Line 16
com.blogdemo.java9.stackwalker.StackWalkerDemo#methodOne, Line 12
com.blogdemo.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.blogdemo.java9.stackwalker.StackWalkerDemoTest
请注意,不应从堆栈底部的方法调用StackWalker::getCallerClass。因为它会导致IllegalCallerException被抛出。