1. 概述

在这篇简短的文章中,我们将探讨Java 9的栈步行API

新功能提供了对栈帧流的访问权限,使得我们可以轻松浏览直接堆栈,并充分利用Java 8的强大流式API

2. 栈步行的优势

在Java 8中,Throwable::getStackTraceThread::getStackTrace返回一个StackTraceElement数组。没有太多手动代码,我们无法丢弃不想要的帧并只保留感兴趣的帧。

此外,Thread::getStackTrace可能返回一个部分堆栈跟踪。这是因为规范允许虚拟机实现为了性能而省略一些堆栈帧。

在Java 9中,使用StackWalkerwalk方法,我们可以遍历我们感兴趣的几个帧或完整的堆栈跟踪。

当然,新功能是线程安全的,这允许多个线程共享单个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_FRAMESSHOW_REFLECT_FRAMES的超集。

3.5. 识别调用类

RETAIN_CLASS_REFERENCE选项保留了StackWalker遍历的所有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.baeldung.java9.stackwalker.StackWalkerDemoTest

请注意,不应从堆栈底部的方法调用StackWalker::getCallerClass,因为这会导致IllegalCallerException抛出。

4. 总结

通过这篇文章,我们看到了如何使用StackWalker的强大功能以及流式API轻松处理StackFrame

当然,我们还可以探索其他功能,如跳过、删除和限制StackFrame。官方文档包含了一些针对更多用例的坚实示例。

一如既往,您可以在GitHub上获取本文的完整源代码。