获取方法调用栈的信息

获取方法调用栈的信息

需求

我在做 Android 的日志工具库时,有个需求:希望能够按照如下格式打印日志

类名.方法名(文件名/代码行号): 日志内容

日志内容很好处理,但是类名、方法名、文件名、代码行号这样的信息要如何处理呢?

方法调用栈

这些信息是和方法调用栈相关的,在 Java 中可以通过两种方法获取到方法调用栈的信息

  • (new Throwable()).getStackTrace()
  • Thread.currentThread().getStackTrace()

这两种方式都能返回一个 StackTraceElement 数组,StackTraceElement 对象中包含了类名、方法名、文件名、代码行号这样的信息

public final class StackTraceElement implements java.io.Serializable {

    private String declaringClass;
    private String methodName;
    private String fileName;
    private int    lineNumber;

    ...

写一个例子来演示一下吧

package com.okada.go;

public class StackTraceDemo {

    public static void main(String[] args) {
        method();
    }
    
    private static void method() {
        StackTraceElement[] stackTraceElements = (new Throwable()).getStackTrace();
        for (int i = 0; i < stackTraceElements.length; i++) {
            StackTraceElement stackTraceElement = stackTraceElements[i];
            System.out.println("index=" + i + "----------------------------------");
            System.out.println("className=" + stackTraceElement.getClassName());
            System.out.println("fileName=" + stackTraceElement.getFileName());
            System.out.println("methodName=" + stackTraceElement.getMethodName());
            System.out.println("lineNumber=" + stackTraceElement.getLineNumber());
        }
    }
}

打印出来的结果如下

index=0----------------------------------
className=com.okada.go.StackTraceDemo
fileName=StackTraceDemo.java
methodName=method
lineNumber=10

index=1----------------------------------
className=com.okada.go.StackTraceDemo
fileName=StackTraceDemo.java
methodName=main
lineNumber=6

从打印结果可以发现:StackTraceElement 数组里面有两个元素。这是为什么?

这个是方法调用栈的知识。在 Java 中有一个方法栈,每执行到一个方法,就将方法压入栈,执行完毕再将方法弹出栈。在上面的例子中,main() 方法是第一个方法,因此首先将 main() 方法压入栈,在 main() 方法中遇到了 method() 方法,因此再将 method() 压入栈中。这时候方法栈的情况如下

|      |
|      |
|method|
|------|
| main |
--------

因此数组中元素的位置如下

|method|main|

这样就能明白为什么打印结果是这样的了。

把上面的例子改造一下

package com.okada.go;

public class StackTraceDemo {

    public static void main(String[] args) {
        method();
        StackTraceElement[] stackTraceElements = (new Throwable()).getStackTrace();
        for (int i = 0; i < stackTraceElements.length; i++) {
            StackTraceElement stackTraceElement = stackTraceElements[i];
            System.out.println("index=" + i + "----------------------------------");
            System.out.println("className=" + stackTraceElement.getClassName());
            System.out.println("fileName=" + stackTraceElement.getFileName());
            System.out.println("methodName=" + stackTraceElement.getMethodName());
            System.out.println("lineNumber=" + stackTraceElement.getLineNumber());
        }
    }
    
    private static void method() {
    }
}

这时候的打印结果如下

index=0----------------------------------
className=com.okada.go.StackTraceDemo
fileName=StackTraceDemo.java
methodName=main
lineNumber=7

因为 method() 方法已经执行完毕,被弹出了方法栈,此时方法栈中只有一个 main() 方法,所以这时候的打印结果就是这样。

获取某一个方法调用栈信息

上面的例子中有两个方法 main() 和 method(),如果我只想知道 method() 的信息的话要怎么做?

要实现这个需求,需要在 StackTraceElement 数组的索引上做文章。可以这么实现

package com.okada.go;

public class StackTraceDemo {

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

    private static void method() {
        StackTraceElement[] stackTraceElements = (new Throwable()).getStackTrace();
        StackTraceElement stackTraceElement = stackTraceElements[0];
        System.out.println("className=" + stackTraceElement.getClassName());
        System.out.println("fileName=" + stackTraceElement.getFileName());
        System.out.println("methodName=" + stackTraceElement.getMethodName());
        System.out.println("lineNumber=" + stackTraceElement.getLineNumber());
    }
}

打印结果

className=com.okada.go.StackTraceDemo
fileName=StackTraceDemo.java
methodName=method
lineNumber=10

因为这段代码

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

是在 method() 方法中执行的,此时的方法调用栈栈顶是 method() 方法,所以使用 0 作为数组的索引就可以获取到 method() 的类名、文件名、方法名和行号信息了。

实战

我希望在实际代码中,调用日志库 Logger 的方法打印出相关信息。例子如下

package com.okada.go;

public class StackTraceDemo {

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

    private static void method() {
        test();
    }
    
    private static void test() {
        Logger.debug("我是日志内容");
    }
}

根据方法调用栈的定义和我的代码层级,使用 1 作为数组索引

public class Logger {

    public static void debug(String message) {
        StackTraceElement[] stackTraceElements = (new Throwable()).getStackTrace();
        StackTraceElement stackTraceElement = stackTraceElements[1];
        String className = stackTraceElement.getClassName();
        String fileName = stackTraceElement.getFileName();
        String methodName = stackTraceElement.getMethodName();
        int lineNumber = stackTraceElement.getLineNumber();
        System.out.println(className + "." + methodName + "(" + fileName + "/" + lineNumber + ")" + message);
    }
}

打印结果

com.okada.go.StackTraceDemo.test(StackTraceDemo.java/14)我是日志内容

以上就是我在开发 Android 日志库时的核心方法。只需要明白

  • 方法调用栈
  • 代码层级

即可实现。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,824评论 0 9
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,785评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 135,067评论 19 139
  • 2017.11.26 风 第28篇 今天我歇班,领孩子们上姥姥家。早晨从被窝爬起来已经八点了,匆匆...
    宇涵妈阅读 125评论 0 0
  • ——《宋词背后的秘密》书评 清代文人潘德舆说:“词滥觞于唐,畅于五代,而艺格之闳深曲挚,则莫盛于宋代。词之有北宋,...
    山千黛阅读 1,421评论 1 3