Contents

Java 9 StackWalker API简介

1. 简介

在这篇快速文章中,我们将了解 Java 9 的StackWalking API新功能提供了对StackFrameStream的访问,使我们能够轻松地直接浏览堆栈并充分利用Java 8中强大的Stream API 。

2. StackWalker的优点

在 Java 8 中,Throwable::getStackTraceThread::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方法接受一个函数引用,为当前线程创建一个StackFrameStream,将该函数应用于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_REFERENCEStackWalker遍历的所有StackFrame中零售Class的对象。这允许我们调用StackWalker::getCallerClassStackFrame::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被抛出。