获取方法调用栈的信息

需求

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

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

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

方法调用栈

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

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

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

1
2
3
4
5
6
7
8
public final class StackTraceElement implements java.io.Serializable {

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

...

写一个例子来演示一下吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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());
}
}
}

打印出来的结果如下

1
2
3
4
5
6
7
8
9
10
11
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() 压入栈中。这时候方法栈的情况如下

1
2
3
4
5
6
|      |
| |
|method|
|------|
| main |
--------

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

1
|method|main|

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

把上面的例子改造一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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() {
}
}

这时候的打印结果如下

1
2
3
4
5
index=0----------------------------------
className=com.okada.go.StackTraceDemo
fileName=StackTraceDemo.java
methodName=main
lineNumber=7

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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());
}
}

打印结果

1
2
3
4
className=com.okada.go.StackTraceDemo
fileName=StackTraceDemo.java
methodName=method
lineNumber=10

因为这段代码

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

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

实战

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 作为数组索引

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
}
`

打印结果

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

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

  • 方法调用栈
  • 代码层级

即可实现。

参考