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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 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日志并进行多维度分析。
相关文章
|
4月前
|
存储 SQL Java
Spring Boot使用slf4j进行日志记录
本节课主要对 slf4j 做了一个简单的介绍,并且对 Spring Boot 中如何使用 slf4j 输出日志做了详细的说明,着重分析了 logback.xml 文件中对日志相关信息的配置,包括日志的不同级别...
|
10天前
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
36 1
|
21天前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
37 3
|
1月前
|
数据采集 监控 Java
SpringBoot日志全方位超详细手把手教程,零基础可学习 日志如何配置及SLF4J的使用......
本文是关于SpringBoot日志的详细教程,涵盖日志的定义、用途、SLF4J框架的使用、日志级别、持久化、文件分割及格式配置等内容。
76 0
SpringBoot日志全方位超详细手把手教程,零基础可学习 日志如何配置及SLF4J的使用......
|
3月前
|
存储 监控 Java
Java日志通关(三) - Slf4j 介绍
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第三篇。
|
3月前
|
存储 监控 Java
Java日志通关(三) - Slf4j 介绍
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第三篇。
|
3月前
|
JavaScript Java API
Java日志通关(二) - Slf4j+Logback 整合及排包
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第二篇。
|
4月前
|
运维
系统日志使用问题之如何防止在打印参数时遇到NPE(空指针异常)
系统日志使用问题之如何防止在打印参数时遇到NPE(空指针异常)
|
4月前
|
缓存 流计算
explorer链接失败报错和延迟问题之Blink参数优化来限制读取日志流的TPS的问题如何解决
explorer链接失败报错和延迟问题之Blink参数优化来限制读取日志流的TPS的问题如何解决
|
3月前
|
存储 容器
Job类日志采集问题之DaemonSet采集方式的参数以减小采集延时如何调整
Job类日志采集问题之DaemonSet采集方式的参数以减小采集延时如何调整