• Java如何获取当前正在执行方法体的方法名称
  • 发布于 2个月前
  • 249 热度
    0 评论
给大家分享一篇关于在 Java 中获取当前正在执行方法体的方法名称的文章,文章中介绍了四种不同的方法。你看看你能知道几个。

在 Java 中,有四种方法可以获取当前正在执行方法体的方法名称,分别是:
方法一:使用 Thread.currentThread().getStackTrace() 方法
方法二:使用异常对象的 getStackTrace() 方法
方法三:使用匿名内部类的 getClass().getEnclosingMethod() 方法
方法四:Java 9 的 Stack-Walking API

本文将根据以上四种方法来给大家进行具体讲解,不过不知道大家有没有想过,获取当前执行方法体的方法名称有什么用嘞?它可以用于日志记录、异常处理、测试框架等方面。例如我们可以在方法的开始和结束时打印出当前方法名和参数,以便追踪程序的执行流程和性能。

方法一
使用 Thread.currentThread().getStackTrace() 方法
这种方法是通过获取当前线程的堆栈跟踪信息,然后从中提取出当前方法名的。

具体的代码如下:
// 获取当前方法名
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
// 打印当前方法名
System.out.println("当前方法名:" + methodName)
这种方法的优点是简单易用,不需要创建额外的对象。缺点是性能较低,因为 Thread.currentThread().getStackTrace() 方法获取堆栈跟踪信息需要遍历整个调用栈,而且需要保证线程安全性。

方法二
使用异常对象的 getStackTrace()方法
这种方法是通过创建一个新的异常对象,然后从其堆栈跟踪信息中提取出当前方法名和参数的。

具体的代码如下:
// 获取当前方法名
String methodName = new Exception().getStackTrace()[0].getMethodName();
// 打印当前方法名
System.out.println("当前方法名:" + methodName);
这种方法仍然需要获取堆栈跟踪信息,而且需要创建异常对象。缺点是需要创建额外的对象,而且代码较为复杂,不太直观。

方法三
匿名内部类的 getClass().getEnclosingMethod()方法
这种方法是通过创建一个匿名内部类的对象,然后从其类对象中获取当前方法的方法对象,再从方法对象中获取当前方法名和参数的。

具体的代码如下:
// 获取当前方法名
String methodName = new Object(){}.getClass().getEnclosingMethod().getName();
// 打印当前方法名
System.out.println("当前方法名:" + methodName);
这种方法的优点是不需要获取堆栈跟踪信息,而且不会创建异常对象,因此性能和可读性都较好。缺点是需要创建额外的对象,而且代码较为复杂,不太直观。

方法四
Java 9 的 Stack-Walking API
Java 9 引入了 Stack-Walking API,以惰性且高效的方式遍历 JVM 堆栈帧。可以使用这个 API 找到当前正在执行的方法,具体的代码如下:
StackWalker walker = StackWalker.getInstance();
Optional<String> optional = walker.walk(frames -> frames
        .findFirst()
        .map(StackWalker.StackFrame::getMethodName));
System.out.println("当前方法名:" + optional.get());
首先,我们使用 StackWalker.getInstance() 工厂方法获取 StackWalker 实例。

然后我们使用 walk() 方法从上到下遍历栈帧:
walk() 方法可以将堆栈帧转化为 Stream流
findFirst() 方法从 Stream 流中的获取第一个元素,也就是堆栈的顶部帧,顶部帧就代表当前正在执行的方法

map() 方法用于获取顶部帧 StackFrame 的当前方法名称


与以上方法相比,Stack-Walking API 有很多优点:
1.线程安全
2.无需创建匿名内部类实例 - new Object().getClass(){}
3.无需创建异常 - new Throwable()

4.无需急切地捕获整个堆栈跟踪,这可能成本很高 - Thread.currentThread()


StackWalker 是以一种懒惰的方式逐一遍历堆栈。在需要获取当前方法名称时,我们可以只获取顶部帧,而不需要捕获整个堆栈跟踪。


面试题:Logback
Logback 是一个流行的 Java 日志框架,它是 Log4j 的继承者,由 Log4j 的创始人设计。

Logback 有以下特点:
1.高性能:Logback 比其他日志框架更快,更节省空间,有时甚至大得多。
2.灵活配置:Logback 支持 XML 和 Groovy 两种配置方式,可以实现动态修改配置,无需重启应用。
3.丰富功能:Logback 提供了多种输出目标,如控制台、文件、数据库、邮件等,还支持滚动策略、过滤器、异步日志等高级功能。
4.与 SLF4J 集成:Logback 是 SLF4J 的原生实现,可以与其他基于 SLF4J 的日志框架无缝切换。
不知道大家有没有想过,我们在使用 Logback 日志框架中打印日志时,是如何获取当前执行方法体的方法名称的嘞?

在 Spring 项目中,我们一般是通过 Logback 的 xml 文件 parttern 属性来配置日志格式的。

xml 配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <springProperty scope="context" name="appName" source="spring.application.name" defaultValue="dev"/>
    <property name="logPath" value="/home/logs/${appName}"/>
    <property name="pattern"
              value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{request_id}] [%thread] [%-5level] %logger{36}:%L %M - %msg%n"/>

    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- encoder 默认配置为PatternLayoutEncoder -->
        <encoder>
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>
    <!-- 记录日志到文件 -->
    <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${logPath}/info.log</file>
        <encoder>
            <pattern>${pattern}</pattern>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logPath}/run.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
    </appender>
    ...
</configuration>
可以看到我们配置的日志输出格式是
%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{request_id}] [%thread] [%-5level] %logger{36}:%L %M - %msg%n
Logback 在打印日志时,会解析这个日志输出格式,最后将 %M 占位符替换为当前方法名称。解析日志格式的源码就在 FormattingConverter 类的 write() 方法中,write() 方法中会执行 convert() 方法,这个方法就是执行占位符替换的。

源码截图如下:

如上图根据类名我们可以看到红线框起来的 MethodOfCallerConverter 类就是用来执行 %M 占位符替换逻辑的,源码如下,
public class MethodOfCallerConverter extends ClassicConverter {
    public String convert(ILoggingEvent le) {
        StackTraceElement[] cda = le.getCallerData();
        if (cda != null && cda.length > 0) {
            // 返回当前方法名称
            return cda[0].getMethodName();
        } else {
            return CoreConstants.NA;
        }
    }
}
方法逻辑如下,
StackTraceElement[] cda = le.getCallerData() 获取当前堆栈顶部帧
cda[0].getMethodName() 根据顶部帧获取当前方法名称。
如上我们只需要看下 le.getCallerData() 方法的堆栈是从哪里获取来的,就能知道答案了。进入 LoggingEvent 源码类中,我们可以发现堆栈获取逻辑,源码如下:
public class LoggingEvent implements ILoggingEvent {
    public StackTraceElement[] getCallerData() {
        if (callerDataArray == null) {
            // 堆栈初始化
            callerDataArray = CallerData.extract(new Throwable(), fqnOfLoggerClass,
                    loggerContext.getMaxCallerDataDepth(), loggerContext.getFrameworkPackages());
        }
        return callerDataArray;
    }
    ...
}
通过分析源码,我们可以知道,如果当前堆栈为空,进行堆栈信息初始化。这里就可以看到堆栈信息初始化来自 extract 方法:
CallerData.extract(new Throwable(), fqnOfLoggerClass,loggerContext.getMaxCallerDataDepth(), loggerContext.getFrameworkPackages())
如果堆栈信息不为空,直接返回当前堆栈。这里是为了避免浪费,针对在一个方法中重复获取堆栈信息的情况。Ok,到这里离胜利就只差一步了。

进一步查看 CallerData.extract(...) 方法,源码如下:
public class CallerData {
    public static StackTraceElement[] extract(Throwable t, String fqnOfInvokingClass, final int maxDepth,
            List<String> frameworkPackageList) {
        if (t == null) {
            return null;
        }

        StackTraceElement[] steArray = t.getStackTrace();
        StackTraceElement[] callerDataArray;
        ...
        callerDataArray = new StackTraceElement[desiredDepth];
        for (int i = 0; i < desiredDepth; i++) {
            callerDataArray[i] = steArray[found + i];
        }
        return callerDataArray;
    }
    ...
}
为了突出源码逻辑的重点,这里我删去了一部分代码,是为了让大家更好的看清楚 Logback 中堆栈信息的初始化,其实用的就是异常对象的 getStackTrace() 方法。也就是上面源码中 StackTraceElement[] steArray = t.getStackTrace() 方法所体现的。

那么到这里我就可以下一个结论了:Logback 日志框架中打印日志时,就是使用异常对象的 getStackTrace() 方法来获取当前执行方法的方法名称的。

JMH,跑一把
到这里我们介绍了四种方法获取当前执行方法名称,一般情况下大家使用异常对象的 getStackTrace() 方法以及匿名内部类的 getClass().getEnclosingMethod() 方法都是可以的,它们的性能都 OK,代码书写复杂程度都大差不差。

在 Java 9 以后推荐使用 Stack-Walking API,它的功能更为强大,与程序里的堆栈语意也跟为契合,性能 OK,并且还是线程安全的。那么它们的性能表现到底如何呢?我们可以通过使用专业基准测试工具 JMH 来对如上四种方案进行测试。

测试环境配置如下:
cpu:i7-8759H,6 核 12 线程

内存 16 GB(2667 MHZ)


先简单的科普一下基准测试。

基准测试,也称之为性能测试,是一种用于衡量计算机系统,软件应用或硬件组件性能的测试方法。基准测试旨在通过运行一系列标准化的任务场景来测量系统的性能表现,从而帮助评估系统的各种指标,如响应时间、吞吐量、延迟、资源利用率等。JMH,即 Java Microbenchmark Harness,是专门用于代码微基准测试的工具套件。何谓 Micro Benchmark 呢?简单的来说就是基于方法层面的基准测试,精度可以达到微秒级。其由 Oracle/openjdk 内部开发 JIT 编译器的大佬们所开发,作为 Java 的方法级性能测试工具可以说是非常合适的。

JMH  可以通过注解和配置参数来控制测试的环境和结果,例如预热次数,迭代次数,线程数,时间单位等。它还可以生成详细的测试报告,包括最小值,平均值,最大值,标准差,置信区间等。

BeachMark:基准测试,主要用来测试一些方法的性能,可以根据不同的参数以不同的单位进行计算(可以使用平均时间作为单位,也可以使用吞吐量作为单位,可以在 BenchmarkMode 值进行调整)。
MIcro Benchmark:简单地说就是在 method 层面上的 benchmark,精度可以精确到微秒级。
OPS:Operation Per Second:每秒操作量,是衡量性能的重要指标,数值的性能越好。类似的有:TPS、QPS。
Throughput:吞吐量。
Warmup:预热,因为 JVM 的 JIT 机制的存储,如果某个函数被调用多次之后,JVM 会尝试将其编译称为机器码从而提高执行速度。为了让结果更加接近真实情况就需要进行预热。

JMH,方案一
使用 Thread.currentThread().getStackTrace() 方法
基准测试参数配置:
Warmup: 3 iterations, 10 s each
Measurement: 5 iterations, 3 s each
Timeout: 10 min per iteration
Threads: 8 threads, will synchronize iterations
Benchmark mode: Throughput
测试代码:
需要说明的是下面的四种测试方法的 JMH 注解配置以及 main 方法都是相同的。所以为了节约篇幅,突出重点,后面三种方案将省去 JMH 注解以及 main 方法配置。
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 3)
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS)
@Threads(8)
@Fork(2)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class MethodNameTest {
    @Benchmark
    @BenchmarkMode({Mode.Throughput})
    public void m1() {
        // 获取当前方法名
        String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(MethodNameTest.class.getSimpleName())
                .build();

        new Runner(opt).run();
    }
}
...
# Run progress: 0.00% complete, ETA 00:06:00
# Fork: 1 of 2
# Warmup Iteration   1: 16.946 ops/ms
# Warmup Iteration   2: 17.086 ops/ms
# Warmup Iteration   3: 17.116 ops/ms
Iteration   1: 17.159 ops/ms
Iteration   2: 17.118 ops/ms
Iteration   3: 17.279 ops/ms
Iteration   4: 17.329 ops/ms
Iteration   5: 17.241 ops/ms

# Run progress: 12.50% complete, ETA 00:05:23
# Fork: 2 of 2
# Warmup Iteration   1: 16.546 ops/ms
# Warmup Iteration   2: 17.340 ops/ms
# Warmup Iteration   3: 17.431 ops/ms
Iteration   1: 17.331 ops/ms
Iteration   2: 17.099 ops/ms
Iteration   3: 17.280 ops/ms
Iteration   4: 17.511 ops/ms
Iteration   5: 17.323 ops/ms


Result "ltd.newbee.mall.MethodNameTest.m1":
  17.267 ±(99.9%) 0.184 ops/ms [Average]
  (min, avg, max) = (17.099, 17.267, 17.511), stdev = 0.122
  CI (99.9%): [17.083, 17.451] (assumes normal distribution)
上面一大堆输出信息,大家直接看重点:在最后 Result "ltd.newbee.mall.MethodNameTest.m1": 这里,平均 ops 是每毫秒 17 次,比较低。

JMH,方案二
使用异常对象的 getStackTrace() 方法
测试代码:
@Benchmark
@BenchmarkMode({Mode.Throughput})
public void m2() {
    // 获取当前方法名
    String methodName = new Throwable().getStackTrace()[0].getMethodName();
}
测试结果:
...
# 堆代码 duidaima.com
# Run progress: 25.00% complete, ETA 00:04:37
# Fork: 1 of 2
# Warmup Iteration   1: 12.891 ops/ms
# Warmup Iteration   2: 12.873 ops/ms
# Warmup Iteration   3: 13.023 ops/ms
Iteration   1: 25.617 ops/ms
Iteration   2: 25.840 ops/ms
Iteration   3: 25.301 ops/ms
Iteration   4: 24.839 ops/ms
Iteration   5: 25.930 ops/ms

# Run progress: 37.49% complete, ETA 00:03:51
# Fork: 2 of 2
# Warmup Iteration   1: 12.511 ops/ms
# Warmup Iteration   2: 12.329 ops/ms
# Warmup Iteration   3: 13.011 ops/ms
Iteration   1: 23.842 ops/ms
Iteration   2: 24.292 ops/ms
Iteration   3: 25.600 ops/ms
Iteration   4: 25.745 ops/ms
Iteration   5: 25.789 ops/ms


Result "ltd.newbee.mall.MethodNameTest.m2":
  25.280 ±(99.9%) 1.088 ops/ms [Average]
  (min, avg, max) = (23.842, 25.280, 25.930), stdev = 0.720
  CI (99.9%): [24.191, 26.368] (assumes normal distribution)
直接看最后 Result "ltd.newbee.mall.MethodNameTest.m2": 这里,平均 ops 是每毫秒 25 次,也比较低。

JMH,方案三
使用匿名内部类的 getClass().getEnclosingMethod() 方法
测试代码:
@Benchmark
@BenchmarkMode({Mode.Throughput})
public void m1() {
    // 获取当前方法名
    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
}
测试结果:
...
# Run progress: 49.99% complete, ETA 00:03:04
# Fork: 1 of 2
# Warmup Iteration   1: 10489.110 ops/ms
# Warmup Iteration   2: 9233.590 ops/ms
# Warmup Iteration   3: 10504.615 ops/ms
Iteration   1: 10695.898 ops/ms
Iteration   2: 10570.155 ops/ms
Iteration   3: 11089.810 ops/ms
Iteration   4: 10805.448 ops/ms
Iteration   5: 10027.222 ops/ms

# Run progress: 62.49% complete, ETA 00:02:18
# Fork: 2 of 2
# Warmup Iteration   1: 11322.008 ops/ms
# Warmup Iteration   2: 10025.593 ops/ms
# Warmup Iteration   3: 10808.095 ops/ms
Iteration   1: 10684.594 ops/ms
Iteration   2: 11241.540 ops/ms
Iteration   3: 10742.348 ops/ms
Iteration   4: 9940.437 ops/ms
Iteration   5: 11226.023 ops/ms


Result "ltd.newbee.mall.MethodNameTest.m3":
  10702.347 ±(99.9%) 672.631 ops/ms [Average]
  (min, avg, max) = (9940.437, 10702.347, 11241.540), stdev = 444.904
  CI (99.9%): [10029.716, 11374.979] (assumes normal distribution)
直接看最后 Result "ltd.newbee.mall.MethodNameTest.m3": 这里,平均 ops 是每毫秒 10702 次。非常夸张,可以看到 ops 对比上面两种方法一下子从几十级别提升到上万级别。

JMH,方案四
Java 9 的 Stack-Walking API
测试代码:
@Benchmark
@BenchmarkMode({Mode.Throughput})
public void m1() {
    // 获取当前方法名
    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
}
测试结果:
...
# Run progress: 74.99% complete, ETA 00:01:32
# Fork: 1 of 2
# Warmup Iteration   1: 2191.034 ops/ms
# Warmup Iteration   2: 2141.886 ops/ms
# Warmup Iteration   3: 2192.843 ops/ms
Iteration   1: 2262.279 ops/ms
Iteration   2: 2263.193 ops/ms
Iteration   3: 2201.354 ops/ms
Iteration   4: 2282.906 ops/ms
Iteration   5: 2130.322 ops/ms

# Run progress: 87.48% complete, ETA 00:00:46
# Fork: 2 of 2
# Warmup Iteration   1: 2207.800 ops/ms
# Warmup Iteration   2: 2269.887 ops/ms
# Warmup Iteration   3: 2239.005 ops/ms
Iteration   1: 2001.840 ops/ms
Iteration   2: 2047.698 ops/ms
Iteration   3: 2349.138 ops/ms
Iteration   4: 2362.165 ops/ms
Iteration   5: 2305.982 ops/ms


Result "ltd.newbee.mall.MethodNameTest.m4":
  2220.688 ±(99.9%) 186.910 ops/ms [Average]
  (min, avg, max) = (2001.840, 2220.688, 2362.165), stdev = 123.629
  CI (99.9%): [2033.778, 2407.598] (assumes normal distribution)
直接看最后 Result "ltd.newbee.mall.MethodNameTest.m4": 这里,平均 ops 是每毫秒 2220 次。对比 第一种和第二种方案的 几十 ops 来说性能提升也很客观,但是对比第三种方法的上万级别 ops 还是不足。

四种方案的最终得分对比
Benchmark           Mode  Cnt      Score     Error   Units
MethodNameTest.m1  thrpt   10     17.267 ±   0.184  ops/ms
MethodNameTest.m2  thrpt   10     25.280 ±   1.088  ops/ms
MethodNameTest.m3  thrpt   10  10702.347 ± 672.631  ops/ms
MethodNameTest.m4  thrpt   10   2220.688 ± 186.910  ops/ms
MethodNameTest.m1     ss   10      0.686 ±   0.289   ms/op
MethodNameTest.m2     ss   10      0.339 ±   0.287   ms/op
MethodNameTest.m3     ss   10      0.031 ±   0.011   ms/op
MethodNameTest.m4     ss   10      0.074 ±   0.027   ms/op
根据最后得分可以看出,四种方案中性能最好的方案是基于匿名内部类的 getClass().getEnclosingMethod() 方案,性能第二是的是基于 Java 9 出现的 Stack-Walking API 方案,其他两种性能过于低下了。

好了,本文的技术部分就到这里啦。
用户评论