日志框架
slf4j
slf4j是一个面向java Logging框架的开源包,其将各个不同的日志框架抽象成一套统一的API进行操作。slf4j不参与具体的日志代码实现,仅仅是根据程序的配置来绑定具体的日志系统。因此,基于slf4j可以让日志使用独立于具体的日志框架。
如上图,slf4j配合具体日志框架的桥接可以在多个日志框架之间切换。
扩展:Java日志框架的行号是怎么获取到的?
用过很多框架,如log4j,logback等。每种框架都支持在日志中输出行号,那框架是如何得到行号的呢?
以logback来说。
在发起logger.info("xxx");调用时,会build一个LoggingEvent对象。在此对象中有获取当前当前调用栈的方法:
1 public StackTraceElement[] getCallerData() { 2 if (callerDataArray == null) { 3 callerDataArray = CallerData 4 .extract(new Throwable(), fqnOfLoggerClass, loggerContext.getMaxCallerDataDepth(), loggerContext.getFrameworkPackages()); 5 } 6 return callerDataArray; 7 }
在这里,通过new Threwable(),然后遍历StackTraceElement来获取当前调用有效栈。因为堆栈是从当前点往外逐层叠加的,那遍历栈时判断栈归属类是否为ch.qos.logback.classic.Logger.class.getName() || org.apache.log4j.Category || org.slf4j.Logger || 是否在框架自身的package内。当跳出此循环后,即定位到日志输出行。在StackTraceElement中有lineNumber记录,直接读取即可得到此行号。此过程如下:
1 /** 2 * Extract caller data information as an array based on a Throwable passed as 3 * parameter 4 */ 5 public static StackTraceElement[] extract(Throwable t, String fqnOfInvokingClass, final int maxDepth, List<String> frameworkPackageList) { 6 if (t == null) { 7 return null; 8 } 9 10 StackTraceElement[] steArray = t.getStackTrace(); 11 StackTraceElement[] callerDataArray; 12 13 int found = LINE_NA; 14 for (int i = 0; i < steArray.length; i++) { 15 if (isInFrameworkSpace(steArray[i].getClassName(), fqnOfInvokingClass, frameworkPackageList)) { 16 // the caller is assumed to be the next stack frame, hence the +1. 17 found = i + 1; 18 } else { 19 if (found != LINE_NA) { 20 break; 21 } 22 } 23 } 24 25 // we failed to extract caller data 26 if (found == LINE_NA) { 27 return EMPTY_CALLER_DATA_ARRAY; 28 } 29 30 int availableDepth = steArray.length - found; 31 int desiredDepth = maxDepth < (availableDepth) ? maxDepth : availableDepth; 32 33 callerDataArray = new StackTraceElement[desiredDepth]; 34 for (int i = 0; i < desiredDepth; i++) { 35 callerDataArray[i] = steArray[found + i]; 36 } 37 return callerDataArray; 38 }
扩展日志框架在日志内容中插入自定义的内容时,需要注意框架包声明。
因为是在扩展中调用具体日志框架实例,那日志输出的行号会定位到你的扩展实现中,此时丢失业务日志输出行号。从上述逻辑可以看出,在定位有效堆栈的时候,框架package是一个过滤条件,那将扩展所在的package加入框架package中即可解决此问题。
相比声明框架包外,使用标准的MDC是更推荐的一种实现方式。支持MDC的日志框架只有logback与log4j。其余的日志框架,在slf4j下会走到空处理,需要通过代码从MDC中读出来。
MDC本质是一个维护一个ThreadLocal对象,存储每个线程的上下文,然后在日志文件中用%X{param}读取变量并输出到日志文件中。
这样看来,日志要获取输出点行号是比较复杂的一个过程,日志输出如果过多,节点资源损耗预计比较可观。