Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

In this quick article, we will have a look at Java 9’s StackWalking API.

The new functionality provides access to a Stream of StackFrames, allowing us to easily browse stack in both directly and making good use of the powerful Stream API in Java 8.

2. Advantages of a StackWalker

In Java 8, the Throwable::getStackTrace and Thread::getStackTrace returns an array of StackTraceElements. Without a lot of manual code, there was no way to discard the unwanted frames and keep only the ones we are interested in.

In addition to this, the Thread::getStackTrace may return a partial stack trace. This is because the specification allows the VM implementation to omit some stack frames for the sake of performance.

In Java 9, using the walk() method of the StackWalker, we can traverse a few frames that we are interested in or the complete stack trace.

Of course, the new functionality is thread-safe; this allows multiple threads to share a single StackWalker instance for accessing their respective stacks.

As described in the JEP-259, the JVM will be enhanced to allow efficient lazy access to additional stack frames when required.

3. StackWalker in Action

Let’s start by creating a class containing a chain of method calls:

public class StackWalkerDemo {

    public void methodOne() {
        this.methodTwo();
    }

    public void methodTwo() {
        this.methodThree();
    }

    public void methodThree() {
        // stack walking code
    }
}

3.1. Capture the Entire Stack Trace

Let’s move ahead and add some stack walking code:

public void methodThree() {
    List<StackFrame> stackTrace = StackWalker.getInstance()
      .walk(this::walkExample);
}

The StackWalker::walk method accepts a functional reference, creates a Stream of StackFrames for the current thread, applies the function to the Stream, and closes the Stream.

Now let’s define the StackWalkerDemo::walkExample method:

public List<StackFrame> walkExample(Stream<StackFrame> stackFrameStream) {
    return stackFrameStream.collect(Collectors.toList());
}

This method simply collects the StackFrames and returns it as a List<StackFrame>. To test this example, please run a JUnit test:

@Test
public void giveStalkWalker_whenWalkingTheStack_thenShowStackFrames() {
    new StackWalkerDemo().methodOne();
}

The only reason to run it as a JUnit test is to have more frames in our stack:

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

In the entire stack trace, we are only interested in top four frames. The remaining frames from org.junit and org.eclipse are nothing but noise frames.

3.2. Filtering the StackFrames

Let’s enhance our stack walking code and remove the noise:

public List<StackFrame> walkExample2(Stream<StackFrame> stackFrameStream) {
    return stackFrameStream
      .filter(f -> f.getClassName().contains("com.baeldung"))
      .collect(Collectors.toList());
}

Using the power of the Stream API, we are keeping only the frames that we are interested in. This will clear out the noise, leaving the top four lines in the stack log:

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

Let’s now identify the JUnit test that initiated the call:

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");
}

Please note that here, we are only interested in a single StackFrame, which is mapped to a String. The output will only be the line containing StackWalkerDemoTest class.

3.3. Capturing the Reflection Frames

In order to capture the reflection frames, which are hidden by default, the StackWalker needs to be configured with an additional option SHOW_REFLECT_FRAMES:

List<StackFrame> stackTrace = StackWalker
  .getInstance(StackWalker.Option.SHOW_REFLECT_FRAMES)
  .walk(this::walkExample);

Using this option, all the reflections frames including Method.invoke() and Constructor.newInstance() will be captured:

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

As we can see, the jdk.internal frames are the new ones captured by SHOW_REFLECT_FRAMES option.

3.4. Capturing Hidden Frames

In addition to the reflection frames, a JVM implementation may choose to hide implementation specific frames.

However, those frames are not hidden from the StackWalker:

Runnable r = () -> {
    List<StackFrame> stackTrace2 = StackWalker
      .getInstance(StackWalker.Option.SHOW_HIDDEN_FRAMES)
      .walk(this::walkExample);
    printStackTrace(stackTrace2);
};
r.run();

Note that we are assigning a lambda reference to a Runnable in this example. The only reason is that JVM will create some hidden frames for the lambda expression.

This is clearly visible in the stack trace:

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

The top two frames are the lambda proxy frames, which JVM created internally. It is worthwhile to note that the reflection frames that we captured in the previous example are still retained with SHOW_HIDDEN_FRAMES option. This is because SHOW_HIDDEN_FRAMES is a superset of SHOW_REFLECT_FRAMES.

3.5. Identifying the Calling Class

The option RETAIN_CLASS_REFERENCE retails the object of Class in all the StackFrames walked by the StackWalker. This allows us to call the methods StackWalker::getCallerClass and StackFrame::getDeclaringClass.

Let’s identify calling class using the StackWalker::getCallerClass method:

public void findCaller() {
    Class<?> caller = StackWalker
      .getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
      .getCallerClass();
    System.out.println(caller.getCanonicalName());
}

This time, we’ll call this method directly from a separate JUnit test:

@Test
public void giveStalkWalker_whenInvokingFindCaller_thenFindCallingClass() {
    new StackWalkerDemo().findCaller();
}

The output of caller.getCanonicalName(), will be:

com.baeldung.java9.stackwalker.StackWalkerDemoTest

Please note that the StackWalker::getCallerClass should not be called from the method at the bottom of the stack. as it will result in IllegalCallerException being thrown.

4. Conclusion

With this article, we’ve seen how easy it is to deal with StackFrames using the power of the StackWalker combined with the Stream API.

Of course, there are various other functionalities we can explore – such as skipping, dropping, and limiting the StackFrames. The official documentation contains a few solid examples for additional use cases.

And, as always, you can get the complete source code for this article over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are closed on this article!