分析堆栈信息封装一个SLF4J的静态类

1,358 阅读3分钟

前言

当我们在使用日志框架的时候,每个类都要通过工厂方法获取一个日志对象来打印日志,感觉太麻烦了。所以想着去封装一个日志静态类。但是问题是封装的那个静态日志类打印出的类信息都是日志类自己,这肯定不是我们想要的啊,我们需要的是当前调用打印日志的这个类的信息啊。

开始思考

我们发现平时使用的Logback等日志框架能准确捕获源代码的所在的类、方法、行。但java并没有提供相应的方法,这似乎很神奇。其实Logback是通过java错误堆栈来实现的,也就是说通过new一个异常Throwable,然后再捕获,从而得到堆栈信息,在进行分析就可以得到行号等信息了。

我们可以借鉴这种思想,在静态日志类中,获取当前线程的堆栈信息,然后找到实际调用者。JDK的Thread类提供了一个getStackTrace()可以获取线程的堆栈跟踪链。下面来看一下这个方法。

/**
* 返回一个表示该线程堆栈转储的堆栈跟踪元素数组。如果该线程尚未启动或已经终止,则该方法将返回一个零长度数组。
* 如果返回的数组不是零长度的,则其第一个元素代表堆栈顶,它是该序列中最新的方法调用。最后一个元素代表堆栈底,是该序列中最旧的方法调用。
*/
public StackTraceElement[] getStackTrace() {
    // 如果该线程不是当前线程
    if (this != Thread.currentThread()) {
        // 获取安全管理器
        SecurityManager security = System.getSecurityManager();
        // 如果有安全管理器
        if (security != null) {
            // 检查是否有权限获取堆栈跟踪
            security.checkPermission(SecurityConstants.GET_STACK_TRACE_PERMISSION);
        }
        // 如果线程未启动或已终止,则不会获取堆栈跟踪信息,直接返回一个空堆栈
        if (!isAlive()) {
            return EMPTY_STACK_TRACE;
        }
        StackTraceElement[][] stackTraceArray = dumpThreads(new Thread[] {this});
        StackTraceElement[] stackTrace = stackTraceArray[0];
        // 可能线程刚刚终止,没有获取到堆栈跟踪,也返回一个空堆栈
        if (stackTrace == null) {
            stackTrace = EMPTY_STACK_TRACE;
        }
        return stackTrace;
    } else {
        // new一个Exception,然后捕获当前线程的堆栈信息并返回
        return (new Exception()).getStackTrace();
    }
}

那么堆栈信息时啥样的呢,下面打印一个看看。

java.lang.Thread.getStackTrace(Thread.java:1559)
util.Logger.getCaller(Logger.java:20)
util.Logger.log(Logger.java:60)
util.Logger.info(Logger.java:99)
byteBuf.test1.main(test1.java:36)

这是调用静态日志的方法得到的堆栈信息。可以看到日志类的堆栈信息的后面紧跟着的就是调用日志方法的类。要拿到的就是这个调用类,然后通过这个类名构造一个日志对象,真正的日志打印是通过这个对象来执行的。

静态日志类

public class Logger {

	// 当前日志类名
	private final static String logClassName = Logger.class.getName();

	/**
	 * 获取最原始被调用的堆栈信息
	 */
	private static StackTraceElement getCaller() {

		// 获取堆栈信息
		StackTraceElement[] traceElements = Thread.currentThread()
				.getStackTrace();
		if (null == traceElements) {
			return null;
		}

		// 最原始被调用的堆栈信息
		StackTraceElement caller = null;

		// 循环遍历到日志类标识
		boolean isEachLogFlag = false;

		// 遍历堆栈信息,获取出最原始被调用的方法信息
		// 当前日志类的堆栈信息完了就是调用该日志类对象信息
		for (StackTraceElement element : traceElements) {
			// 遍历到日志类
			if (element.getClassName().equals(logClassName)) {
				isEachLogFlag = true;
			}

			// 下一个非日志类的堆栈,就是最原始被调用的方法
			if (isEachLogFlag) {
				if (!element.getClassName().equals(logClassName)) {
					caller = element;
					break;
				}
			}
		}

		return caller;
	}

	/**
	 * 自动匹配请求类名,生成logger对象
	 */
	private static org.slf4j.Logger log() {
		// 最原始被调用的堆栈对象
		StackTraceElement caller = getCaller();
		// 空堆栈处理
		if (caller == null) {
			return LoggerFactory.getLogger(Logger.class);
		}

		// 取出被调用对象的类名,并构造一个Logger对象返回
		return LoggerFactory.getLogger(caller.getClassName());
	}

	// 下列是封装后的方法
    // 省略其它方法的封装
	public static void info(String message) {
		log().info(message);
	}
}