取得 StackTraceElement


在〈認識堆疊追蹤〉中曾經看過例外發生時,利用例外物件自動收集的 Stack Frame 來顯示堆疊追蹤,自動收集的 Stack Frame 是來自於主執行緒的 JVM Stack,實際上,每個執行緒都會有自己專屬的 JVM Stack,顧名思義,JVM Stack 是個先進後出結構,每呼叫一個方法,JVM 就會建立一個 Stack Frame 來儲存區域變數、方法、類別等資訊並置放至 JVM Stack,方法呼叫結束後 Stack Frame 就從 JVM Stack 中跳出銷毀。

也許是為了瞭解應用程式的行為,或者是進行除錯,自行取得堆疊追蹤有其需求,為此,JDK 從 1.4 開始就在 ThrowableThread 類別上提供了 getStackTrace() 方法,可用來取得當時活動執行緒的 JVM Stack,並以 StackTraceElement 陣列傳回 JVM Stack 中全部的 Stack Frame。

如果你想主動取得 Stack Frame,方式之一是建立 Throwable 實例並呼叫其 getStackTrace() 方法:

StackTraceElement[] stackTrace  = new Throwable().getStackTrace();

另一個方式是取得 Thread 實例,像是透過 Thread.currentThread(),並呼叫其 getStackTrace() 方法:

StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();

每個 StackTraceElement 實例代表著 JVM Stack 中的一個 Stack Frame,可以使用 StackTraceElementgetClassName()getFileName()getLineNumber()getMethodName(),或者是 JDK9 新增的 getModuleName()getModuleVersion()getClassLoaderName() 等方法取得對應的資訊。例如,可改寫〈認識堆疊追蹤〉中的範例,主動顯示堆疊追蹤:

package cc.openhome;

import static java.lang.System.out;
import java.util.List;

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

    static void c() {
        b();
    }

    static void b() {
        a();
    }

    static void a() {
        Thread currentThread = Thread.currentThread();
        StackTraceElement[] stackTrace = currentThread.getStackTrace();

        out.printf("Stack trace of thread %s:%n", currentThread.getName());
        List.of(stackTrace).forEach(out::println);
    }
}

這個範例程式中,c() 方法呼叫 b() 方法,b() 方法呼叫 a() 方法,因而堆疊追蹤顯示結果會是:

Stack trace of thread main:
java.base/java.lang.Thread.getStackTrace(Thread.java:1654)
cc.openhome/cc.openhome.StackTraceDemo.a(StackTraceDemo.java:21)
cc.openhome/cc.openhome.StackTraceDemo.b(StackTraceDemo.java:16)
cc.openhome/cc.openhome.StackTraceDemo.c(StackTraceDemo.java:12)
cc.openhome/cc.openhome.StackTraceDemo.main(StackTraceDemo.java:8)

每個執行緒會有自己的 JVM Stack,因此底下的程式會顯示執行緒個別的追蹤:

package cc.openhome;

import static java.lang.System.out;
import java.util.List;

public class StackTraceDemo2 {   
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> c());
        t.start();
        t.join(); // 這是為了能循序顯示個別的堆疊追蹤

        c();
    }

    static void c() {
        b();
    }

    static void b() {
        a();
    }

    static void a() {
        Thread currentThread = Thread.currentThread();
        StackTraceElement[] stackTrace = currentThread.getStackTrace();

        out.printf("Stack trace of thread %s:%n", currentThread.getName());
        List.of(stackTrace).forEach(out::println);
    }
}

執行結果如下:

Stack trace of thread Thread-0:
java.base/java.lang.Thread.getStackTrace(Thread.java:1654)
cc.openhome/cc.openhome.StackTraceDemo2.a(StackTraceDemo2.java:24)
cc.openhome/cc.openhome.StackTraceDemo2.b(StackTraceDemo2.java:19)
cc.openhome/cc.openhome.StackTraceDemo2.c(StackTraceDemo2.java:15)
cc.openhome/cc.openhome.StackTraceDemo2.lambda$main$0(StackTraceDemo2.java:8)
java.base/java.lang.Thread.run(Thread.java:844)
Stack trace of thread main:
java.base/java.lang.Thread.getStackTrace(Thread.java:1654)
cc.openhome/cc.openhome.StackTraceDemo2.a(StackTraceDemo2.java:24)
cc.openhome/cc.openhome.StackTraceDemo2.b(StackTraceDemo2.java:19)
cc.openhome/cc.openhome.StackTraceDemo2.c(StackTraceDemo2.java:15)
cc.openhome/cc.openhome.StackTraceDemo2.main(StackTraceDemo2.java:11)

如果只是想取得簡單的資訊,基本上可以使用以上的方式,然而,getStackTrace() 會取得全部的 Stack Frame,如果你只是想察看前幾個,就會顯得沒有效率;若是想取得方法所在的類別資訊呢?StackTraceElement只提供 getClassName() 傳回字串,你得自己想辦法,像是透過反射(Reflection)等機制來達到目的,如果想要進一步取得呼叫方法那方的類別資訊呢?JDK8 或以前的版本,沒有標準 API 可以從事這個任務(真的硬是要取得呼叫方法那方的類別資訊的話,在 JDK 內部 API 中,sun.reflect.Reflection 是有個 getCallerClass() 方法)。