获取方法调用栈的信息
需求
我在做 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 日志库时的核心方法。只需要明白
- 方法调用栈
- 代码层级
即可实现。