文章目录
- 【深入日志打印】log.error(“你好{}“, “世界“, e);只有一个占位符是否会打印后面多出的参数呢?(详细跟进源码讲解调试分析)
- 测试代码
- 执行结果
- 调试分析
- 其他样例探讨
【深入日志打印】log.error(“你好{}“, “世界“, e);只有一个占位符是否会打印后面多出的参数呢?(详细跟进源码讲解调试分析)
测试代码
直接在try里弄一个除以0的报错,然后打印日志,日志字符串里只设置了一个占位符,传2个参数过去,是否会打印两个参数呢?
@Slf4j
public class Demo {
public static void main(String[] args) {
try {
int a = 1 / 0;
} catch (Exception e) {
log.error("你好{}", "世界", e);
}
}
}
执行结果
先直接公布结果,打印了异常堆栈
调试分析
我们点开源代码里看看,我们可以看到这个log.error(“你好{}”, “世界”, e);匹配到的方法是下线这个方法,第一个参数为格式字符串,第二个参数为任意参数对象1,第三个参数为任意参数对象2。看着像这后面2个参数是为了提供给格式字符串格式化替换占位符用的,如果格式字符串里只有一个占位符的话,那后面那个参数就不会被格式化进去,也就不会打印,这样的思考是有道理的,那为啥又打印了呢?我们继续往下看。
我们点开方法实现,发现有四个实现,直接每个实现里代码第一行都加个断点,看看到底进了哪个
(断点是走debug模式启动的,点左侧代码行那添加断点,不懂就百度一下,你就知道。)
断点进去看到进了
ch.qos.logback.classic.Logger#error(java.lang.String, java.lang.Object, java.lang.Object)
继续往下走看这个filterAndLog_2方法,你会发现这个Throwable参数是null,那这个时候你会不会就在想,既然异常对象的参数为空,为啥最后又打印了异常堆栈呢?对,我也很好奇,我们继续往下调试,看看堆栈啥时候打印出来的,哪个代码打印的,代码的堆栈数据从哪里来的
继续往下走我们看到新建了一个LoggingEvent,把这个空的Throwable也传进去了,而我们那个异常对象的e是在params里传进去了
继续往下走看这个callAppenders(le)
遍历Appender对象,执行doAppend方法,传入的参数是LoggingEvent
这个doAppend有两个实现,都加断点看去了哪个,最后去了ch.qos.logback.core.UnsynchronizedAppenderBase
这个UnsynchronizedAppenderBase是个抽象类,append方法又有三个实现,异步Appender、数据库Appender、输出流Appender,最后进了OutputStreamAppender
又有个subAppend,又有两个实现,输出流的Appender、滚动文件Appender,继续往下追
可以看到,由encoder.encode把event编码为字节数组了,然后执行写入字节数组。
encode又是两个实现,继续往下追断点
到了layout.doLayout,又是五个实现,
最后进了TTLLayout里
我们看下这个格式字符串最终格式化出来的结果,只有你好世界,没有那个e,证明格式字符串做格式化的时候确实是按照我们写的占位符一个,只把第一个参数格式化了,第二个是没有格式化进去的,那为啥还是打印了堆栈呢,我们一步步调试发现是打印这个event.getThrowableProxy()时打印的堆栈信息
IThrowableProxy tp = event.getThrowableProxy();
if (tp != null) {
String stackTrace = tpc.convert(event);
sb.append(stackTrace);
}
有个private ThrowableProxy throwableProxy;可是我就没有赋值过这玩意呀,那我们看下这个玩意哪里赋值的
我们看下这个LoggingEvent构造方法里,这不就出来了,如果throwable为空,则执行某个导出throwable的方法,我们当时的throwable是null的,重新debug,让代码再次断点到这个构造方法来,进去看看
断点确实进到了throwable==null的分支里
这里用了个EventArgUtil.extractThrowable(argArray);从参数里去导出了一个Throwable
继续看源码,你可以看到,如果参数为空,那就返回null,然后去获取了参数数组里最后一个参数,判定是否为Throwable的实例,如果是就返回这个,否则返回null,到这你就明白了,为啥格式化出来的字符串没有异常堆栈信息,最后却打印了,这是因为在构造LoggingEvent的时候会去看参数数组最后一个参数是不是个异常,是的话就会作为异常存下来给后面打印异常堆栈使用了。
其他样例探讨
@Slf4j
public class Demo {
public static void main(String[] args) {
try {
int a = 1 / 0;
} catch (Exception e) {
log.error("你好{}", e, "世界");
}
}
}
打印结果,多出的那个参数是没打印的
@Slf4j
public class Demo {
public static void main(String[] args) {
try {
int a = 1 / 0;
} catch (Exception e) {
log.error("你好{}", "啊", e, "世界");
}
}
}
打印结果,异常对象e放到中间也是不会打印的。刚看过源码你是明白的,不是占位符要的参数的话,只判断了最后一个参数。
@Slf4j
public class Demo {
public static void main(String[] args) {
try {
int a = 1 / 0;
} catch (Exception e) {
log.error("你好{}", e);
}
}
}
你觉得会不会打印两次e?结果是不会。
你可以看到源码里写的第二个参数是Throwable,作为异常对象来传过去的,然后调其他方法的时候params参数传的null,这就是为啥格式字符串里占位符没转化为e了。
最后我补充一点看完了你要学的是分析跟踪的思维,不要死记会不会打印,因为你项目里用的包、版本、实现类、代码可能不同,最后的结果按最后执行的代码来定。