StackWalker 遍历栈帧

背景

在看 springboot 3.x 源码时发现 deduceMainApplicationClass 方法的实现发生了变化,用到了 StackWalker 来遍历栈帧,之前没用过。

image-20241203094528296

StackWalker

StackWalker 是线程安全的。多个线程可以共享单个 StackWalker 对象来遍历其自己的堆栈。

StackFrame 来代表栈帧,可以获取各种信息。

StackWalker. Option 来控制 StackFrame 可以获取哪些信息。

StackFrame

StackWalker 使用 StackFrame 类来表示栈帧的详细信息,但哪些方法可用受 java.lang.StackWalker.Option 的影响。

public interface StackFrame {
    /**
     * 返回声明 StackFrame 所代表方法的类全限定名。比如子类继承了父类但没重写父类的方法, 会返回父类的类名
     */
    public String getClassName();

    /**
     * 返回 StackFrame 所代表方法的方法名
     *
     * 若 Option 配置了 DROP_METHOD_INFO, 则抛异常 UnsupportedOperationException
     */
    public String getMethodName();

    /**
     * 返回声明 StackFrame 所代表方法的 Class 对象。比如子类继承了父类但没重写父类的方法, 会返回父类的类对象
     *
     * 若 Option 配置了 DROP_METHOD_INFO, 或没有配置 RETAIN_CLASS_REFERENCE, 则抛异常 UnsupportedOperationException
     */
    public Class<?> getDeclaringClass();

    /**
     * 返回 StackFrame 所代表方法的 MethodType, 包含参数类型和返回值类型, 比如 String test(int a, String b, Object[] c) 的 MethodType 为 (int,String,Object[])String
     * 若 Option 配置了 DROP_METHOD_INFO, 或没有配置 RETAIN_CLASS_REFERENCE, 则抛异常 UnsupportedOperationException
     */
    public default MethodType getMethodType() {
        throw new UnsupportedOperationException();
    }

    /**
     * 返回此 StackFrame 所表示的方法的描述符(如 Java 虚拟机规范所定义)。比如 String test(int a, String b, Object[] c) 的描述符为 (ILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
     *
     * 若 Option 配置了 DROP_METHOD_INFO, 或没有配置 RETAIN_CLASS_REFERENCE, 则抛异常 UnsupportedOperationException
     */
    public default String getDescriptor() {
        throw new UnsupportedOperationException();
    }

    /**
     * 返回包含此栈帧表示的执行点的 code 属性的代码数组的索引。代码数组给出了实现该方法的 Java 虚拟机代码的实际字节。
     * 
     * 若 Option 配置了 DROP_METHOD_INFO, 则抛异常 UnsupportedOperationException
     */
    public int getByteCodeIndex();

    /**
     * 返回包含此栈帧表示的执行点的源文件的名称。通常,这对应于 Java 虚拟机规范定义的相关类文件的 SourceFile 属性。在某些系统中,该名称可能引用文件以外的某些源代码单元,例如源存储库中的条目。
     *
     * 若 Option 配置了 DROP_METHOD_INFO, 则抛异常 UnsupportedOperationException
     */
    public String getFileName();

    /**
     * 返回包含此栈帧表示的执行点的源代码的行号。通常,这是从 Java 虚拟机规范定义的相关类文件的 LineNumberTable 属性派生的。
     *
     * 若 Option 配置了 DROP_METHOD_INFO, 则抛异常 UnsupportedOperationException
     */
    public int getLineNumber();

    /**
     * 此栈帧表示的执行点的方法是否是 Native 方法
     * 
     * 若 Option 配置了 DROP_METHOD_INFO, 则抛异常 UnsupportedOperationException
     */
    public boolean isNativeMethod();

    /**
     * 为栈帧返回一个 StackTraceElement 对象
     *
     * 若 Option 配置了 DROP_METHOD_INFO, 则抛异常 UnsupportedOperationException
     */
    public StackTraceElement toStackTraceElement();
}

Option

public enum Option {
    /**
     * 在 StackFrames 中保留 Class 对象。
     * 使用此选项配置的 StackWalker 将支持 getCallerClass() 和 StackFrame.getDeclaringClass()。
     */
    RETAIN_CLASS_REFERENCE,
    /**
     * 从 StackFrames 中删除方法信息。
     * 将无法调用 StackFrame 的 getMethodName()、getMethodType()、getLineNumber、getByteCodeIndex()、getFileName()、isNativeMethod() 方法
     *
     * @since 22
     */
    DROP_METHOD_INFO,
    /**
     * 显示所有反射帧。
     *
     * 默认情况下,反射帧处于隐藏状态。配置了此 SHOW_REFLECT_FRAMES 选项的 StackWalker 将显示包含 java.lang.reflect.Method#invoke 和 java.lang.reflect.Constructor#newInstance(Object...) 及其反射实现类。SHOW_HIDDEN_FRAMES 选项也可用于显示所有反射帧,它还将显示特定于实现的其他隐藏帧。
     */
    SHOW_REFLECT_FRAMES,
    /**
     * 显示所有的隐藏帧
     *
     * 除了反射帧之外,Java 虚拟机实现还可以隐藏特定于实现的帧。具有此 SHOW_HIDDEN_FRAMES 选项的 StackWalker 将显示所有隐藏的帧(包括反射帧)。
     */
    SHOW_HIDDEN_FRAMES;
}

方法

创建 StackWalker

StackWalker 可通过 4 个 getInstance 静态方法方法获取实例。estimateDepth 是要遍历的栈帧的估计数量,可以将其用作缓冲区大小的提示。Option 是什么介绍过的。

image-20241203100942997

StackWalker 有 3 个 public 方法:

walk

将给定的函数应用于当前线程的 StackFrames 流(串行流),从堆栈的栈顶开始遍历,栈顶即调用 walk 方法的方法(不是调用 StackWalker#getInstance 的方法)。

参数以一个 Function,其参数为 Stream<StackFrame>,且必须有返回值,可自由的遍历、操作 Stream

public <T> T walk(Function<? super Stream<StackFrame>, ? extends T> function)

例:打印所有信息

package com.example.jdk23;

import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class Test {

    public static void main(String[] args) {
        m1();
    }

    public static void m1() {
        m2();
    }

    public static void m2() {
        m3();
    }

    public static void m3() {
        m4(StackWalker.getInstance(EnumSet.of(StackWalker.Option.RETAIN_CLASS_REFERENCE,
                StackWalker.Option.SHOW_REFLECT_FRAMES, StackWalker.Option.SHOW_HIDDEN_FRAMES)));
    }

    public static void m4(StackWalker stackWalker) {
        List<Map<String, Object>> stacks = stackWalker
                .walk(s -> s
                        .map(Test::getStackInfo)
                        .toList()
                );
        print(stacks);
    }

    public static Map<String, Object> getStackInfo(StackWalker.StackFrame frame) {
        LinkedHashMap<String, Object> info = new LinkedHashMap<>();
        info.put("className", frame.getClassName());
        info.put("methodName", frame.getMethodName());
        info.put("declaringClass", frame.getDeclaringClass());
        info.put("methodType", frame.getMethodType());
        info.put("descriptor", frame.getDescriptor());
        info.put("byteCodeIndex", frame.getByteCodeIndex());
        info.put("fileName", frame.getFileName());
        info.put("lineNumber", frame.getLineNumber());
        info.put("isNativeMethod", frame.isNativeMethod());
        info.put("stackTraceElement", frame.toStackTraceElement());
        return info;
    }

    private static void print(List<Map<String, Object>> stacks) {
        for (Map<String, Object> stack : stacks) {
            stack.forEach((k, v) -> System.out.println(k + ": " + v));
            System.out.println();
        }
    }
}

m3、m4 方法的 javap 结果

  public static void m3();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=0, args_size=0
         0: getstatic     #18                 // Field java/lang/StackWalker$Option.RETAIN_CLASS_REFERENCE:Ljava/lang/StackWalker$Option;
         3: getstatic     #24                 // Field java/lang/StackWalker$Option.SHOW_REFLECT_FRAMES:Ljava/lang/StackWalker$Option;
         6: getstatic     #27                 // Field java/lang/StackWalker$Option.SHOW_HIDDEN_FRAMES:Ljava/lang/StackWalker$Option;
         9: invokestatic  #30                 // Method java/util/EnumSet.of:(Ljava/lang/Enum;Ljava/lang/Enum;Ljava/lang/Enum;)Ljava/util/EnumSet;
        12: invokestatic  #36                 // Method java/lang/StackWalker.getInstance:(Ljava/util/Set;)Ljava/lang/StackWalker;
        15: invokestatic  #42                 // Method m4:(Ljava/lang/StackWalker;)V
        18: return
      LineNumberTable:
        line 23: 0
        line 25: 18

  public static void m4(java.lang.StackWalker);
    descriptor: (Ljava/lang/StackWalker;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: aload_0
         1: invokedynamic #46,  0             // InvokeDynamic #0:apply:()Ljava/util/function/Function;
         6: invokevirtual #50                 // Method java/lang/StackWalker.walk:(Ljava/util/function/Function;)Ljava/lang/Object;
         9: checkcast     #54                 // class java/util/List
        12: astore_1
        13: aload_1
        14: invokestatic  #56                 // Method print:(Ljava/util/List;)V
        17: return
      LineNumberTable:
        line 28: 0
        line 29: 6
        line 33: 13
        line 34: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0 stackWalker   Ljava/lang/StackWalker;
           13       5     1 stacks   Ljava/util/List;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
           13       5     1 stacks   Ljava/util/List<Ljava/util/Map<Ljava/lang/String;Ljava/lang/Object;>;>;
    MethodParameters:
      Name                           Flags
      stackWalker

输出:

第 5 行 descriptor 为 (Ljava/lang/StackWalker;)V,与 javap 结果的 18 行对应。

第 6 行 byteCodeIndex 为 6,从 javap 结果的 24 行可看出是调用 StackWalker.walk

第 8 行 lineNumber 为 29,从 javap 结果的 32 行可看出是与 code 6 对应,即调用 StackWalker.walk

className: com.example.jdk23.Test
methodName: m4
declaringClass: class com.example.jdk23.Test
methodType: (StackWalker)void
descriptor: (Ljava/lang/StackWalker;)V
byteCodeIndex: 6
fileName: Test.java
lineNumber: 29
isNativeMethod: false
stackTraceElement: com.example.jdk23.Test.m4(Test.java:29)

className: com.example.jdk23.Test
methodName: m3
declaringClass: class com.example.jdk23.Test
methodType: ()void
descriptor: ()V
byteCodeIndex: 15
fileName: Test.java
lineNumber: 23
isNativeMethod: false
stackTraceElement: com.example.jdk23.Test.m3(Test.java:23)

className: com.example.jdk23.Test
methodName: m2
declaringClass: class com.example.jdk23.Test
methodType: ()void
descriptor: ()V
byteCodeIndex: 0
fileName: Test.java
lineNumber: 19
isNativeMethod: false
stackTraceElement: com.example.jdk23.Test.m2(Test.java:19)

className: com.example.jdk23.Test
methodName: m1
declaringClass: class com.example.jdk23.Test
methodType: ()void
descriptor: ()V
byteCodeIndex: 0
fileName: Test.java
lineNumber: 15
isNativeMethod: false
stackTraceElement: com.example.jdk23.Test.m1(Test.java:15)

className: com.example.jdk23.Test
methodName: main
declaringClass: class com.example.jdk23.Test
methodType: (String[])void
descriptor: ([Ljava/lang/String;)V
byteCodeIndex: 0
fileName: Test.java
lineNumber: 11
isNativeMethod: false
stackTraceElement: com.example.jdk23.Test.main(Test.java:11)

从输出中可以看出栈顶是 m4,而非 m3

例:打印反射帧、隐藏帧

package com.example.jdk23;

import java.util.EnumSet;
import java.util.List;
import java.util.stream.Stream;

public class Test2 {

    public static void main(String[] args) {
        showReflectFrames();
        System.out.println();
        showHiddenFrames();
    }


    static void showReflectFrames() {
        try {
            Test2.class.getDeclaredMethod("showReflectFrames0").invoke(null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    static void showReflectFrames0() {
        List<StackWalker.StackFrame> frames = StackWalker.getInstance(EnumSet.of(StackWalker.Option.SHOW_REFLECT_FRAMES))
                .walk(Stream::toList);
        for (StackWalker.StackFrame frame : frames) {
            System.out.println(frame);
        }
    }

    static void showHiddenFrames() {
        try {
            Test2.class.getDeclaredMethod("showHiddenFrames0").invoke(null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    static void showHiddenFrames0() {
        List<StackWalker.StackFrame> frames = StackWalker.getInstance(EnumSet.of(StackWalker.Option.SHOW_HIDDEN_FRAMES))
                .walk(Stream::toList);
        for (StackWalker.StackFrame frame : frames) {
            System.out.println(frame);
        }
    }
}

showReflectFrames0 的输出包含 java.lang.invokejdk.internal.reflect 帧。

showHiddenFrames0 的输出比 showReflectFrames0 多了一些帧,这些是隐藏帧。

com.example.jdk23.Test2.showReflectFrames0(Test2.java:26)
java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
java.base/java.lang.reflect.Method.invoke(Method.java:580)
com.example.jdk23.Test2.showReflectFrames(Test2.java:18)
com.example.jdk23.Test2.main(Test2.java:10)

com.example.jdk23.Test2.showHiddenFrames0(Test2.java:42)
java.base/java.lang.invoke.DirectMethodHandle$Holder.invokeStatic(DirectMethodHandle$Holder)
java.base/java.lang.invoke.LambdaForm$MH/0x00000296bd003400.invoke(LambdaForm$MH)
java.base/java.lang.invoke.Invokers$Holder.invokeExact_MT(Invokers$Holder)
java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invokeImpl(DirectMethodHandleAccessor.java:153)
java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
java.base/java.lang.reflect.Method.invoke(Method.java:580)
com.example.jdk23.Test2.showHiddenFrames(Test2.java:34)
com.example.jdk23.Test2.main(Test2.java:12)

forEach

对当前线程的 StackFrame 流的每个元素执行给定的操作,从栈顶开始遍历,栈顶即调用 forEach 方法的方法(不是调用 StackWalker#getInstance 的方法)。该方法等效于调用 walk(s -> { s. forEach(action); return null; });

参数是一个 Consumer,其参数是 StackFrame,只能顺序遍历栈帧,不像 walk 方法可以自由的操作 Stream<StackFrame>

public void forEach(Consumer<? super StackFrame> action)

例子

public class Test2 {

    public static void main(String[] args) {
        m1();
    }

    public static void m1() {
        m2();
    }

    public static void m2() {
        m3();
    }

    public static void m3() {
        m4(StackWalker.getInstance(EnumSet.of(StackWalker.Option.RETAIN_CLASS_REFERENCE,
                StackWalker.Option.SHOW_REFLECT_FRAMES, StackWalker.Option.SHOW_HIDDEN_FRAMES)));
    }

    public static void m4(StackWalker stackWalker) {
        stackWalker.forEach(System.out::println);
    }
}

输出

com.example.jdk23.Test2.m4(Test2.java:34)
com.example.jdk23.Test2.m3(Test2.java:29)
com.example.jdk23.Test2.m2(Test2.java:25)
com.example.jdk23.Test2.m1(Test2.java:21)
com.example.jdk23.Test2.main(Test2.java:17)

getCallerClass

获取调用者的 Class 对象,假设调用 getCallerClass 方法的方法为 a ,调用方法 a 的方法为 b,那方法 b 就是调用者。

此方法会过滤反射帧、java. lang. invoke. MethodHandle 和隐藏帧,而不管 StackWalker 是否配置了 SHOW_REFLECT_FRAMESSHOW_HIDDEN_FRAMES 选项。

只有在调用者帧存在时才能调用此方法。如果从栈上最底部的帧调用此方法,则会引发 IllegalCallerException,比如在 main 方法中调用。

若 Option 配置了 DROP_METHOD_INFO, 或没有配置 RETAIN_CLASS_REFERENCE, 则抛异常 UnsupportedOperationException。

public Class<?> getCallerClass()

例:直接调用、反射调用

package com.example.jdk23;

import java.util.EnumSet;

public class Test4 {

    static class C1 {
        static void m1() {
            C2.m2();
        }
    }

    static class C2 {
        static void m2() {
            C.m();

            try {
                C.class.getDeclaredMethod("m").invoke(null);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    static class C {
        static void m() {
            System.out.println(StackWalker.getInstance(EnumSet.of(StackWalker.Option.RETAIN_CLASS_REFERENCE,
                    StackWalker.Option.SHOW_REFLECT_FRAMES, StackWalker.Option.SHOW_HIDDEN_FRAMES)).getCallerClass());
        }
    }

    public static void main(String[] args) {
        C1.m1();
        C2.m2();
        C.m();
    }
}

直接调用、反射调用的结果是相同的,确实隐藏了反射帧、隐藏帧。

class com.example.jdk23.Test4$C2
class com.example.jdk23.Test4$C2
class com.example.jdk23.Test4$C2
class com.example.jdk23.Test4$C2
class com.example.jdk23.Test4

例:栈底调用会抛异常

package com.example.jdk23;

public class Test3 {
    public static void main(String[] args) {
        StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass();
    }
}

Exception in thread "main" java.lang.IllegalCallerException: no caller frame: [com.example.jdk23.Test3 , null]
	at java.base/java.lang.StackStreamFactory$CallerClassFinder.consumeFrames(StackStreamFactory.java:752)
	at java.base/java.lang.StackStreamFactory$CallerClassFinder.consumeFrames(StackStreamFactory.java:713)
	at java.base/java.lang.StackStreamFactory$AbstractStackWalker.doStackWalk(StackStreamFactory.java:330)
	at java.base/java.lang.StackStreamFactory$AbstractStackWalker.callStackWalk(Native Method)
	at java.base/java.lang.StackStreamFactory$AbstractStackWalker.beginStackWalk(StackStreamFactory.java:424)
	at java.base/java.lang.StackStreamFactory$AbstractStackWalker.walkHelper(StackStreamFactory.java:267)
	at java.base/java.lang.StackStreamFactory$AbstractStackWalker.walk(StackStreamFactory.java:259)
	at java.base/java.lang.StackStreamFactory$CallerClassFinder.findCaller(StackStreamFactory.java:725)
	at java.base/java.lang.StackWalker.getCallerClass(StackWalker.java:649)
	at com.example.jdk23.Test3.main(Test3.java:5)

参考

Java 9 揭秘(16. 虚拟机栈遍历) - 林本托 - 博客园

Java 9 StackWalking API入门介绍 | Baeldung中文网

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值