slf4j的log.warn()的参数问题研究

简介: slf4j的log.warn()的参数问题研究

一、背景

今天我们探讨一下@Slf4j的log.warn()的参数问题。


二、上源码

我先给出下面的几个测试用例,大家想想分别会输出什么?

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.junit.Test;
@Slf4j
public class WarnLogTest {
    @Test
    public void test1() {
        try {
            mockException();
        } catch (Exception e) {
            log.warn("code={},msg={}",
                    500, "呜呜,某某业务出错了!", "肿么办?");
        }
    }
    @Test
    public void test2() {
        try {
            mockException();
        } catch (Exception e) {
            log.warn("code={},msg={},e={}",
                    500, "呜呜,某某业务出错了!", JSON.toJSONString(e));
        }
    }
    @Test
    public void test3() {
        try {
            mockException();
        } catch (Exception e) {
            log.warn("code={},msg={},e={}",
                    500, "呜呜,某某业务出错了!", ExceptionUtils.getStackTrace(e));
        }
    }
    @Test
    public void test4() {
        try {
            mockException();
        } catch (Exception e) {
            log.warn("code={},msg={}",
                    500, "呜呜,某某业务出错了!", e);
        }
    }
    private void mockException() {
        throw new RuntimeException("某个运行时异常");
    }

}

想好之后看看和我的分析是一样还是有出入。


三、分步解析

我们先看下warn的源码


/**

    * Log a message at the WARN level according to the specified format

    * and arguments.

    *

    *

This form avoids superfluous string concatenation when the logger

    * is disabled for the WARN level. However, this variant incurs the hidden

    * (and relatively small) cost of creating an Object[] before invoking the method,

    * even if this logger is disabled for WARN. The variants taking

    * {@link #warn(String, Object) one} and {@link #warn(String, Object, Object) two}

    * arguments exist solely in order to avoid this hidden cost.

    *

    * @param format    the format string

    * @param arguments a list of 3 or more arguments

    */

   public void warn(String format, Object... arguments);

发现前面是format格式化字符串,后面是对应的参数。格式化的占位符(即“{}”)和后面的参数一一对应。


 @Test

   public void test0() {

       log.warn("code={},msg={}", 200, "成功!");

   }

参数指  200(第一个参数,对应第一个占位符),第二个参数“成功!”对应第二个占位符。


输出日志时拼接在一起:code=200,msg=成功


输出结果为:


00:05:46.731 [main] WARN com.chujianyun.common.log.WarnLogTest - code=200,msg=成功!


和设想的一样(前面是自动追加的可以定制)。


这个占位符和参数和String.format()函数非常相似


public static String format(String format, Object... args) {

       return new Formatter().format(format, args).toString();

   }

前面是格式化字符串,后面是对应占位符的参数。


等价于以下代码(我们可以对比学习):


String.format("code=%d,msg=%s", 200, "成功!");

因此看第一个,根据上面的参数我们可以推测到,前面的{}是占位符,有两个,后面参数有三个,应该最后一个不显示


   @Test

   public void test1() {

       try {

           mockException();

       } catch (Exception e) {

           log.warn("code={},msg={}",

                   500, "呜呜,某某业务出错了!", "肿么办?");

       }

   }

运行结果:


23:37:18.525 [main] WARN com.chujianyun.common.log.WarnLogTest - code=500,msg=呜呜,某某业务出错了!


果然和我们想的一样!!


我们看第二个:


   @Test

   public void test2() {

       try {

           mockException();

       } catch (Exception e) {

           log.warn("code={},msg={},e={}",

                   500, "呜呜,某某业务出错了!", JSON.toJSONString(e));

       }

   }

根据上面的理论,三个占位符,三个参数应该没问题。




虽然和我们想的一样,但是打印的太不优雅了,好长而且都在一行里!!!


那我们换一个写法,采用工具类美化一下:


   @Test

   public void test3() {

       try {

           mockException();

       } catch (Exception e) {

           log.warn("code={},msg={},e={}",

                   500, "呜呜,某某业务出错了!", ExceptionUtils.getStackTrace(e));

       }

   }

效果还不错:




我们再看另外一个写法:


   @Test

   public void test4() {

       try {

           mockException();

       } catch (Exception e) {

           log.warn("code={},msg={}",

                   500, "呜呜,某某业务出错了!", e);

       }

   }

根据前面的经验,我们认为这个应该不会打印e,因为格式化占位符只有两个,这个是三个参数。


我们发现结果和我们想的不一样!!!


四、探究

它是一个接口,我们看看他的实现类


我们以Log4JLoggerAdapter为例吧,看名字就知道是适配器模式。


适配器模式的目的:将一个类的接口转换成客户希望的另外一个接口。


适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。


如果想仔细研究可以看文末的参考资料。


我们继续,发现实现的代码在这里(这里是适配的函数):


org.slf4j.impl.Log4jLoggerAdapter#warn(java.lang.String, java.lang.Object...)
   public void warn(String format, Object... argArray) {
        if (this.logger.isEnabledFor(Level.WARN)) {
            FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray);
            this.logger.log(FQCN, Level.WARN, ft.getMessage(), ft.getThrowable());
        }
    }
上半部分调用了这里:
    final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray) {
        Throwable throwableCandidate = getThrowableCandidate(argArray);
        Object[] args = argArray;
        if (throwableCandidate != null) {
            args = trimmedCopy(argArray);
        }
        return arrayFormat(messagePattern, args, throwableCandidate);
    }
然后调用到了这里
static final Throwable getThrowableCandidate(Object[] argArray) {
        if (argArray == null || argArray.length == 0) {
            return null;
        }
        final Object lastEntry = argArray[argArray.length - 1];
        if (lastEntry instanceof Throwable) {
            return (Throwable) lastEntry;
        }
        return null;
    }
和这里
    private static Object[] trimmedCopy(Object[] argArray) {
        if (argArray == null || argArray.length == 0) {
            throw new IllegalStateException("non-sensical empty or null argument array");
        }
        final int trimemdLen = argArray.length - 1;
        Object[] trimmed = new Object[trimemdLen];
        System.arraycopy(argArray, 0, trimmed, 0, trimemdLen);
        return trimmed;
    }

真相就浮现了!


getThrowableCandidate函数会判断数组最后一个是不是Throwable的子类型,如果是转成Throwable返回给前面,否则返回null.

而trimmedCopy(Object[] argArray)函数只复制了参数长度-1个长度,忽略最后一个元素。


最终调用org.slf4j.helpers.MessageFormatter#arrayFormat(java.lang.String, java.lang.Object[], java.lang.Throwable)去构造打印对象FormattingTuple。


然后调用log4j的


org.apache.log4j.Category#log(java.lang.String, org.apache.log4j.Priority, java.lang.Object, java.lang.Throwable)来实现打印。


public void log(String FQCN, Priority p, Object msg, Throwable t) {

       int levelInt = this.priorityToLevelInt(p);

       this.differentiatedLog((Marker)null, FQCN, levelInt, msg, t);

   }

另外我们还可以打断点验证一下(具体自己可单步跟下去)。


另外特别提示,善用左下角的调用栈,可以看到整个调用链路,双击可以切到上层源码。


因此结论是:


使用org.slf4j.Logger#warn(java.lang.String, java.lang.Object...)时,最后一个参数如果是异常可自动追加到日志中。


这得益于适配器模式,底层实现提供了这种兼容。


另外为什么会调用到适配器这里,可以看文末的我的另外一篇文章《Slf4j的优势与原理》。


五、总结

一、遇到和自己预期不符的代码,一定要借机研究一下,能够学到更多。可能发现自己没注意的点,没掌握好的点,也可能会发现一个隐患或者BUG。


二、遇到问题尽量跟到源码里,从源码角度去分析为什么,是快速成长的一个途径。


三、验证代码是否运行,可以断点,这是一个经验。


附录:

适配器模式  https://www.runoob.com/design-pattern/adapter-pattern.html


Slf4j的优势与原理:https://blog.csdn.net/w605283073/article/details/89683847

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
5月前
|
Java API
LOG4J2-MDC-全链路跟踪等功能研究
LOG4J2-MDC-全链路跟踪等功能研究
85 0
|
监控 数据库 Windows
Log“.NET研究”4Net 全方位跟踪程序运行
  前端日子自己写了一个简单的日志跟踪程序,现在目前正在做的一个项目中使用以便于跟踪程序异常和运行状况,但是被否认了!可能是没有权威性,于是自己总结了一下Log4net日志跟踪系统,这里分享大家学习一下。
1020 0
|
Unix
深入研究Clang(九) Clang代码阅读之打log读流程2
继续上一篇,同样的hello.c,同样的执行过程,只不过继续添加了一些log信息,而且对代码进行了更近一步的挖掘。先看输入和输出的log信息(前半部分): shining@shining-VirtualBox:~/llvm-3.
1417 0
深入研究Clang(八) Clang代码阅读之打log读流程1
这个过程简单的分为几步。 第一步:写一个简单的小程序,hello.c。内容如下: #includeint main() {  printf("Hello world!\n");} 第二步:找出如何在LLVM里面输出信息,最后选择采用llvm::errs(),等于采用了LLVM的错误机制。
1187 0
|
监控 网络协议
iptables学习与研究(使用LOG记录失败日志)
原文地址: http://blog.csdn.net/fafa211/article/details/2307581   通常情况下,iptables的默认政策为DROP,不匹配的数据包将被直接丢弃。
813 0
|
24天前
|
Java
使用Java代码打印log日志
使用Java代码打印log日志
77 1
|
25天前
|
Linux Shell
Linux手动清理Linux脚本日志定时清理日志和log文件执行表达式
Linux手动清理Linux脚本日志定时清理日志和log文件执行表达式
78 1
|
29天前
|
SQL 关系型数据库 MySQL
MySQL数据库,可以使用二进制日志(binary log)进行时间点恢复
对于MySQL数据库,可以使用二进制日志(binary log)进行时间点恢复。二进制日志是MySQL中记录所有数据库更改操作的日志文件。要进行时间点恢复,您需要执行以下步骤: 1. 确保MySQL配置文件中启用了二进制日志功能。在配置文件(通常是my.cnf或my.ini)中找到以下行,并确保没有被注释掉: Copy code log_bin = /path/to/binary/log/file 2. 在需要进行恢复的时间点之前创建一个数据库备份。这将作为恢复的基准。 3. 找到您要恢复到的时间点的二进制日志文件和位置。可以通过执行以下命令来查看当前的二进制日志文件和位