JDK9 的 Stack-Walking API


為了在堆疊追蹤時更為方便,JDK9 新增了 Stack-Walking API,具體來說,可以透過 java.lang.StackWalkergetInstance() 方法,取得 StackWalker 實例後,運用 forEach() 方法來循序走訪 StackWalker.StackFrame,每個 StackFrame 代表著 JVM Stack 中的一個 Stack Frame。例如,可改寫〈取得 StackTraceElement〉的 StackTraceDemo 如下:

package cc.openhome;

import static java.lang.System.out;

public class StackWalkerDemo {   
    public static void main(String[] args) {
        c();
    }

    static void c() {
        b();
    }

    static void b() {
        a();
    }

    static void a() {
        out.printf("Stack trace of thread %s:%n",
                        Thread.currentThread().getName());
        StackWalker stackWalker = StackWalker.getInstance();
        stackWalker.forEach(out::println);
    }
}

堆疊追蹤顯示結果會是:

Stack trace of thread main:
cc.openhome/cc.openhome.StackWalkerDemo.a(StackWalkerDemo.java:21)
cc.openhome/cc.openhome.StackWalkerDemo.b(StackWalkerDemo.java:15)
cc.openhome/cc.openhome.StackWalkerDemo.c(StackWalkerDemo.java:11)
cc.openhome/cc.openhome.StackWalkerDemo.main(StackWalkerDemo.java:7)

StackWalker 本身有個 getCallerClass() 方法,而 StackFrame 有個 getDeclaringClass(),然而預設是不能從這兩個方法取得 Class 實例,必須在呼叫 StackWalkergetInstance() 時指定 StackWalker.OptionRETAIN_CLASS_REFERENCE,否則會引發 java.lang.UnsupportedOperationException

例如,若只想顯示類別與方法名稱,可以如下:

package cc.openhome;

import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
import static java.lang.System.out;

public class StackWalkerDemo2 {   
    public static void main(String[] args) {
        c();
    }

    static void c() {
        b();
    }

    static void b() {
        a();
    }

    static void a() {
        out.printf("Stack trace of thread %s:%n", 
                Thread.currentThread().getName());

        StackWalker stackWalker =
                StackWalker.getInstance(RETAIN_CLASS_REFERENCE);
        out.printf("Caller class %s%n",
                stackWalker.getCallerClass().getName());

        stackWalker.forEach(stackFrame -> {
            out.printf("%s.%s%n", 
                    stackFrame.getDeclaringClass(), 
                    stackFrame.getMethodName());
        });
    }
}

堆疊追蹤顯示結果會是:

Stack trace of thread main:
Caller class cc.openhome.StackWalkerDemo2
class cc.openhome.StackWalkerDemo2.a
class cc.openhome.StackWalkerDemo2.b
class cc.openhome.StackWalkerDemo2.c
class cc.openhome.StackWalkerDemo2.main

如果你只對某幾個 StackFrame 感興趣,或者想對 StackFrame 做轉換或過濾,可以使用 StackWalker 實例的 walk() 方法,例如,只想找到第一個 StackFrame

Optional<StackFrame> frame =
     stackWalker.walk(frameStream -> frameStream.findFirst());

walk() 的 Lambda 運算式中,會傳入 Stream<? super StackFrame>,而 Lambda 運算式的傳回值就是 walk() 的傳回值,因此,你可以惰性操作傳入的 Stream<? super StackFrame>,基本上,Stream 定義的操作,像是 filter()map()collect()count()等都可以使用。

有些 Stack Frame 與反射或者特定的 JVM 特定實作相關,在不指定參數的情況下,StackWalker.getInstance() 預設是不可取得 Class 實例且不顯示這些 Stack Frame,如果想要顯示反射相關的 Stack Frame,可以使用 SHOW_REFLECT_FRAMES,若想顯示隱藏的 Stack Frame(包含反射相關Stack Frame),可以使用 SHOW_HIDDEN_FRAMES

底下是個簡單的示範,使用了反射機制來呼叫 c() 方法,看看在不同選項下,StackWalker 走訪的 Stack Frame 有何不同:

package cc.openhome;

import static java.lang.StackWalker.Option.*;
import static java.lang.System.out;
import java.util.List;

public class StackWalkerDemo3 {   
    public static void main(String[] args) throws Exception {
        StackWalkerDemo3.class.getDeclaredMethod("c").invoke(null);
    }

    static void c() {
        b();
    }

    static void b() {
        a();
    }

    static void a() {
        List<StackWalker> stackWalkers =
                List.of(
                    StackWalker.getInstance(),
                    StackWalker.getInstance(SHOW_REFLECT_FRAMES),
                    StackWalker.getInstance(SHOW_HIDDEN_FRAMES)
                );

         stackWalkers.forEach(
             stackWalker -> {
                 out.println();
                 stackWalker.forEach(out::println);
             }
         );
    }
}

執行結果如下:

cc.openhome/cc.openhome.StackWalkerDemo3.lambda$a$0(StackWalkerDemo3.java:33)
java.base/java.lang.Iterable.forEach(Iterable.java:75)
cc.openhome/cc.openhome.StackWalkerDemo3.a(StackWalkerDemo3.java:30)
cc.openhome/cc.openhome.StackWalkerDemo3.b(StackWalkerDemo3.java:18)
cc.openhome/cc.openhome.StackWalkerDemo3.c(StackWalkerDemo3.java:14)
cc.openhome/cc.openhome.StackWalkerDemo3.main(StackWalkerDemo3.java:10)

cc.openhome/cc.openhome.StackWalkerDemo3.lambda$a$0(StackWalkerDemo3.java:33)
java.base/java.lang.Iterable.forEach(Iterable.java:75)
cc.openhome/cc.openhome.StackWalkerDemo3.a(StackWalkerDemo3.java:30)
cc.openhome/cc.openhome.StackWalkerDemo3.b(StackWalkerDemo3.java:18)
cc.openhome/cc.openhome.StackWalkerDemo3.c(StackWalkerDemo3.java:14)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:564)
cc.openhome/cc.openhome.StackWalkerDemo3.main(StackWalkerDemo3.java:10)

cc.openhome/cc.openhome.StackWalkerDemo3.lambda$a$0(StackWalkerDemo3.java:33)
cc.openhome/cc.openhome.StackWalkerDemo3$$Lambda$23/25126016.accept(Unknown Source)
java.base/java.lang.Iterable.forEach(Iterable.java:75)
cc.openhome/cc.openhome.StackWalkerDemo3.a(StackWalkerDemo3.java:30)
cc.openhome/cc.openhome.StackWalkerDemo3.b(StackWalkerDemo3.java:18)
cc.openhome/cc.openhome.StackWalkerDemo3.c(StackWalkerDemo3.java:14)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:564)
cc.openhome/cc.openhome.StackWalkerDemo3.main(StackWalkerDemo3.java:10)

可以看到預設的堆疊追蹤記錄最為簡潔,而 SHOW_REFLECT_FRAMES 顯示了一些反射相關的 Stack Frame,至於 SHOW_HIDDEN_FRAMES 除了顯示反射相關的 Stack Frame 之外,還多了個 JVM 特定實作的 Stack Frame。

如果需要在 StackWalker.getInstance() 時指定多個選項,可以使用 Set 來指定,這時結合 Set.of() 會很方便,例如:

StackWalker stackWalker = StackWalker.getInstance(
    Set.of(RETAIN_CLASS_REFERENCE, SHOW_REFLECT_FRAMES)
);

如果你想限制可取得的 Stack Frame 深度,可以使用 StackWalker.getInstance() 的另一個版本:

StackWalker stackWalker = StackWalker.getInstance(
    Set.of(RETAIN_CLASS_REFERENCE, SHOW_REFLECT_FRAMES),
    10  // 深度最多為10
);