05.java日志之slf4j门面

816 阅读16分钟

本篇文章中涉及到的所有代码都已经上传到gitee中: gitee.com/sss123a/log…

java日志系列全解

# 00.java⽇志之发展史

# 01.java日志之原生的日志框架jul

# 02.java日志之log4j入门及xml配置详解

# 03.java日志之log4j的基础组件和各种Appender

# 04.java日志之jcl门面

# 05.java日志之slf4j门面

# 06.java日志之logback源码和xml解析

# 07.java日志之logback内部状态数据Status

# 08.java日志之logabck各环境配置

# 09.java日志之logback的appender标签及其子标签全解析

slf4j-api.2.0.10

日志 API 和配置分离鉴于 SLF4J 提供了一个狭窄的 API,仅限于编写日志语句,但没有日志配置,SLF4J 强制执行关注点分离。日志记录语句是使用 SLF4j API 编写的,并通过底层日志记录后端进行配置,通常在单个位置。
在部署时选择您的日志框架通过在类路径上插入适当的 jar 文件(提供程序/绑定),可以在部署时插入所需的日志记录框架。
快速失败操作在 SLF4J 初始化期间,将很早就搜索提供者。如果 SLF4J 在类路径上找不到提供程序,它将发出一条警告消息并默认为无操作实现。
流行日志框架的提供者SLF4J 支持流行的日志框架,即 reload4j、log4j 1.x、log4j 2.x、java.util.logging、Simplelogging 和 NOP。logback 、logevents、penna项目 原生支持 SLF4J。
桥接旧版日志记录 APIJCL 在 SLF4J 上的实现(即 jcl-over-slf4j.jar)将允许您的项目逐步迁移到 SLF4J,而不会破坏与使用 JCL 的现有软件的兼容性。同样,log4j-over-slf4j.jar 和 jul-to-slf4j 模块将允许您将 log4j 和 java.util.logging 调用分别重定向到 SLF4J。有关更多详细信息,请参阅桥接旧版 API页面。
迁移您的源代码slf4j-migrator实用程序可以帮助您迁移源以使用 SLF4J。
支持参数化日志消息所有 SLF4J 提供程序/绑定都支持参数化日志消息,并显着提高了性能 结果。

slf4j官网介绍

Simple Logging Facade for Java (SLF4J) 用作各种日志框架(例如 java.util.logging、logback、log4j)的简单外观或抽象,允许最终用户在部署时插入所需的日志框架 。

在您采用SLF4J之前,我们建议您阅读简明的SLF4J用户手册

请注意,启用 SLF4J 库意味着仅添加一个强制依赖项,即slf4j-api.jar。如果在类路径上找不到binding/provider,则 SLF4J 将默认为无操作实现。

如果您希望将 Java 源文件迁移到 SLF4J,请考虑我们的迁移器工具,它可以在短短几分钟内将您的项目迁移为使用 SLF4J API。

如果您依赖的外部维护组件使用 SLF4J 以外的日志记录 API,例如 commons logging、log4j 或 java.util.logging,请查看 SLF4J 对旧版 API的二进制支持。

以上内容翻译自:slf4j官方网站

这里面提到的binding/provider其实是slf4j中实现日志门面的两个重要class,

binding对应org.slf4j.spi.LoggerFactoryBinder(2.x版本中已经过期了)

package org.slf4j.spi;  
import org.slf4j.ILoggerFactory;  
  
public interface LoggerFactoryBinder {  
    ILoggerFactory getLoggerFactory();  

    String getLoggerFactoryClassStr();  
}

provider对应org.slf4j.spi.SLF4JServiceProvider

package org.slf4j.spi;  
import org.slf4j.ILoggerFactory;  
import org.slf4j.IMarkerFactory;  
 
public interface SLF4JServiceProvider {  
    ILoggerFactory getLoggerFactory();  

    IMarkerFactory getMarkerFactory();  

    MDCAdapter getMDCAdapter();  

    String getRequestedApiVersion();  

    void initialize();  
}

SLF4JServiceProvider类在被反射实例化后,调用initialize()方法进行初始化

我们可以看一下slf4j-api.2.0.10包的类结构

image.png

如无特别说明,本文中引入的slf4j依赖都是基于slf4j-api.2.0.10版本的

关于slf4j的maven依赖有很多,比如:

Hello world!

引入maven依赖,如果想使用slf4j,必须引用slf4j-api。这里使用的log4j版本号是2.0.10

<!--slf日志门面-->  
<dependency>  
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-api</artifactId>  
    <version>2.0.10</version>  
</dependency>

DEMO如下:

package com.matio.slf4j.helloworld;  
  
import org.slf4j.LoggerFactory;  
import org.slf4j.Logger;  
  
public class HelloWorld {  
    public static void main(String[] args) {  
        Logger logger = LoggerFactory.getLogger(HelloWorld.class.getName());  
        logger.info("Hello slf4j!");  

        System.out.println("Logger对象:" + logger);  
    }  
}

打印结果如下:

image.png 红色部分是slf4j的警告信息,具体可以看看org.slf4j.helpers.Reporter

这打印结果可能不会跟我们预期的有出入,那是因为我们目前只引入了slf4j-api这一个依赖包,运行时找不到slf4j的providers,导致了我们生成的最终的logger对象是NOPLogger,而它只是一个空实现。

如果我们想打印日志到console上,可以额外再引入一个包slf4j-simple,它只是slf4j的一个很简单的日志实现,没什么强大的功能,所以使用很少:

<!--slf4j内置简单的实现-->
<dependency>
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-simple</artifactId>  
    <version>2.0.10</version>  
</dependency>

再次运行Hello world,发现这次日志可以正确输出了,而且生成的logger对象是SimpleLogger

image.pngSimpleLogger这个类是slf4j-simple包中的类。

slf4j-simple-2.0.10的包结构如下: image.png

据此我们可以猜测:生成的哪种logger实例是由引入的依赖决定的。比如:

slf4j + jul

移除掉slf4j-simple依赖,额外再引入slf4j-jdk14依赖,生成的logger对象类是JDK14LoggerAdapter

slf4j + log4j

移除掉slf4j-simple依赖,额外再引入slf4j-log4j12依赖,生成的logger对象类是Reload4jLoggerAdapter

接下来我们来看看slf4j的底层实现,先一句话总结下:

slf4j 2.x版本 是基于spi机制实现的,

低版本则是基于类加载实现的

slf4j日志绑定原理

Hello world!代码如下:

package com.matio.slf4j.helloworld;  
  
import org.slf4j.LoggerFactory;  
import org.slf4j.Logger;  
  
public class HelloWorld {  
    public static void main(String[] args) {  
        Logger logger = LoggerFactory.getLogger(HelloWorld.class.getName());  
        logger.info("Hello slf4j!");
    }  
}

获取org.slf4j.Logger子实现

public static Logger getLogger(String name) {  
    ILoggerFactory iLoggerFactory = getILoggerFactory();  
    return iLoggerFactory.getLogger(name);  
}

ILoggerFactory是一个专门生产logger实例的工厂,它里面就只有一个getLogger()方法。所以需要我们先找到ILoggerFactory的具体实现类,这个具体实现类不是在slf4j-api中,而是在jul、log4j、logback等各类日志对接slf4j的实现包当中。那么该如何获取ILoggerFactory实现呢,我们继续看下去:

public static ILoggerFactory getILoggerFactory() {  
    return getProvider().getLoggerFactory();  
}  
  
static SLF4JServiceProvider getProvider() {  
    // 移除了一些不必要的代码,更简洁些...
    INITIALIZATION_STATE = 1;  
    performInitialization();  
    
    switch (INITIALIZATION_STATE) {  
        case 1:  
            return SUBST_PROVIDER;
        case 3:  
            return PROVIDER;  
        case 4:  
            return NOP_FALLBACK_SERVICE_PROVIDER;  
    }  
}

private static final void performInitialization() {  
    bind();  
    if (INITIALIZATION_STATE == 3) {  
        versionSanityCheck();  
    }    
}

原来ILoggerFactory子实现是通过SLF4JServiceProvider类获取的,直接看bind()方法

private static final void bind() {  
    try {  
        // 读取系统属性slf4j.provider
        // 利用spi读取所有SLF4JServiceProvider子实现  
        List<SLF4JServiceProvider> providersList = findServiceProviders();  
        reportMultipleBindingAmbiguity(providersList);  
        if (providersList != null && !providersList.isEmpty()) {  
            // 只获取第一个  
            PROVIDER = (SLF4JServiceProvider)providersList.get(0);  
            // 调用其初始化方法  
            PROVIDER.initialize();  
            INITIALIZATION_STATE = 3;  
            reportActualBinding(providersList);  
        } else {  
            INITIALIZATION_STATE = 4;  
            // 读取classpath中所有org/slf4j/impl/StaticLoggerBinder.class类  
            // 因为在低版本中log4j门面实现主要是依靠该类,比如slf4j-simple.1.7.21、slf4j-jdk14.1.7.25  
            // 而且他们的包名都是org/slf4j/impl  
            // 通过StaticLoggerBinder类调用其getLoggerFactory()方法也可以获取到ILoggerFactory子实现  
            // 因为在新版本中该类已经过期,所以只是输出个告警日志,并没有用StaticLoggerBinder去获取ILoggerFactory子实现  
            Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();  
            reportIgnoredStaticLoggerBinders(staticLoggerBinderPathSet);  
        }  
        postBindCleanUp();  
    } catch (Exception var2) {  
        failedBinding(var2);  
        throw new IllegalStateException("Unexpected initialization failure", var2);  
    }  
}

上面代码看着很多,其实绝大多数都在打日志,主要看findServiceProviders()findPossibleStaticLoggerBinderPathSet()这两个方法就够了。

那么如何获取SLF4JServiceProvider子实现呢?具体看findServiceProviders()方法:

  1. 读取系统属性slf4j.provider对应的value(从 2.0.9 开始,可以通过“slf4j.provider”系统属性显式指定提供程序类。这绕过了用于查找提供者的服务加载器机制,并可能缩短 SLF4J 初始化时间。)
  2. 利用SPI机制读取META-INF\services\org.slf4j.spi.SLF4JServiceProvider文件中所有SLF4JServiceProvider子实现。如果有多个则取第一个

通过反射将classname生成最终的SLF4JServiceProvider子实现(系统属性中优先级最高),然后调用其initialize()方法进行初始化。

如果发现SLF4JServiceProvider子实现一个都没有,那调用findPossibleStaticLoggerBinderPathSet()方法读取classpath中所有org/slf4j/impl/StaticLoggerBinder.class类,然后打印告警日志。其实这个方法可以是slf4j低版本中门面模式的主要实现,在高版本中已经过期了。原因如下:

因为在低版本中log4j门面实现主要是依靠StaticLoggerBinder这个类的,比如slf4j-simple.1.7.21slf4j-jdk14.1.7.25等,而且这些jar包中StaticLoggerBinder.class类的包名都是org/slf4j/impl

通过StaticLoggerBinder类调用其getLoggerFactory()方法也可以获取到ILoggerFactory子实现;

因为在新版本中该类已经过期,所以这里只是输出个告警日志,StaticLoggerBinder已经被废弃了。

从 2.0.0 版本开始,SLF4J中的bindings称为provider。尽管如此,总体思路仍然是一样的。SLF4J API 版本 2.0.0 依赖SPI机制来查找其日志记录后端。有关更多详细信息,这是总体思路的图形说明。 image.png

上面这副图就表明了 SLF 是如何绑定其它的主流日志框架。

针对上面的编号进行介绍:
①:SLF4J
    单独导入slf4j-api是没有日志打印的效果,只会打印几句提示信息,提示未绑定日志实现,因为底层没有绑定具体的日志框架
②:SLF4J+logback
    底层绑定logback日志实现框架
③:SLF4J+reload4j(Log4j)
    底层绑定Log4j日志实现框架(reload4j是Log4j的升级版,因为之前Log4j出现了重大漏洞)
④:SLF4J+JUL
    底层绑定Java自带的JUL日志实现框架
⑤:SLF4J+Simple
    底层绑定SLF4J官方推出的基本日志实现框架
⑥:SLF4J+nop
    关闭一切日志输出信息
下面按照顺序依次介绍及使用

如果 Java 应用中需要使用日志记录的话,则首先需要引入 slf4j-api.jar 的依赖,统一日志接口。并且,它需要引入具体的实现,共有三种情况:

  1. 没有引入具体的实现:那么,日志功能将不能起作用
  2. 引入了一类实现(上图的蓝色框:slf4j-logbackslf4j-simpleslf4j-nop),由于它们的设计比 SLF 要晚,所以默认遵循 SLF 的规范,只需要导入它们的依赖就可使用
  3. 引入了另一类实现(log4jjdk14),它们的设计比 SLF 要早,在设计之初,并没有遵循 SLF 的规范,无法直接进行绑定。所以,需要添加一个适配层 Adaptation layer。通过适配器进行适配,从而间接地遵循了 SLF 的规范。

使用 SLF 的日志绑定流程:

1、添加 slf4j-api 的依赖

2、使用 slf4j 的 API 在项目中进行统一的日志记录

3、 绑定具体的日志实现

1、绑定了已经实现sfl4j的日志框架,直接添加对应的依赖 2、绑定了没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖

4、slf4j 有且仅有一个日志实现框架的绑定(如果出现多个,默认使用第一个依赖日志实现)

slf4j和各种日志实现框架组合

slf4j + slf4j

<!--slf日志门面-->  
<dependency>  
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-api</artifactId>  
    <version>2.0.10</version>  
</dependency>  
  
<!--slf4j内置简单的实现-->  
<dependency>  
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-simple</artifactId>  
    <version>2.0.10</version>  
</dependency>

slf4j-simple日志包支持的功能非常简单,仅支持输出到控制台或某个文件中,适合slf4j初学者,使用场景较少.slf4j-simple-2.0.10.jar源码包结构如下:

image.png

执行流程大致如下:

SimpleServiceProvider#initialize();

new SimpleLoggerFactory();

SimpleLogger.lazyInit();

SimpleLoggerConfiguration#init();

最终会调用到SimpleLoggerConfiguration#init();这里来,该方法主要负责加载配置文件,读取用户自定义配置,代码如下:

void init() {
    // 加载resources下simplelogger.properties配置文件
    this.loadProperties();
    String defaultLogLevelString = this.getStringProperty("org.slf4j.simpleLogger.defaultLogLevel", (String)null);
    if (defaultLogLevelString != null) {
        this.defaultLogLevel = stringToLevel(defaultLogLevelString);
    }

    this.showLogName = this.getBooleanProperty("org.slf4j.simpleLogger.showLogName", true);
    this.showShortLogName = this.getBooleanProperty("org.slf4j.simpleLogger.showShortLogName", false);
    this.showDateTime = this.getBooleanProperty("org.slf4j.simpleLogger.showDateTime", false);
    this.showThreadName = this.getBooleanProperty("org.slf4j.simpleLogger.showThreadName", true);
    this.showThreadId = this.getBooleanProperty("org.slf4j.simpleLogger.showThreadId", false);
    dateTimeFormatStr = this.getStringProperty("org.slf4j.simpleLogger.dateTimeFormat", DATE_TIME_FORMAT_STR_DEFAULT);
    this.levelInBrackets = this.getBooleanProperty("org.slf4j.simpleLogger.levelInBrackets", false);
    this.warnLevelString = this.getStringProperty("org.slf4j.simpleLogger.warnLevelString", "WARN");
    this.logFile = this.getStringProperty("org.slf4j.simpleLogger.logFile", this.logFile);
    this.cacheOutputStream = this.getBooleanProperty("org.slf4j.simpleLogger.cacheOutputStream", false);
    this.outputChoice = computeOutputChoice(this.logFile, this.cacheOutputStream);
    if (dateTimeFormatStr != null) {
        this.dateFormatter = new SimpleDateFormat(dateTimeFormatStr);
    }
}

主要分两步:

  1. 加载resources目录下simplelogger.properties配置文件
  2. 优先从系统属性中读取上述配置,其次才会从simplelogger.properties文件中读取

resources目录simplelogger.properties文件内容大致如下,具体参考SimpleLoggerConfiguration#init();

# @see org.slf4j.simple.SimpleLoggerConfiguration.init()  

# 入口:SimpleServiceProvider.initialize();实例化SimpleLoggerFactory对象时会读取到该文件完成初始化配置  

# 以下属性也支持从系统属性中读取,从系统属性中读取优先级高  

# 默认的日志级别,默认info,支持trace、debug、info、warn、error、off  
org.slf4j.simpleLogger.defaultLogLevel=info  
# 默认true  
org.slf4j.simpleLogger.showLogName=true  
# 默认false  
org.slf4j.simpleLogger.showShortLogName=false  
# 是否显示调用线程名称,默认false  
org.slf4j.simpleLogger.showThreadName=true  
# 默认false  
org.slf4j.simpleLogger.showThreadId=false  
# 是否显示日期,跟dateTimeFormat搭配使用,默认false  
org.slf4j.simpleLogger.showDateTime=true  
# 日期格式  
org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss SSS  
# 默认false  
org.slf4j.simpleLogger.levelInBrackets=false  
org.slf4j.simpleLogger.warnLevelString=WARN  
# 默认System.err,支持System.out和自定义保存日志的路径  
org.slf4j.simpleLogger.logFile=System.err  
org.slf4j.simpleLogger.cacheOutputStream=false

这些属性也支持从系统属性中读取,而且从系统属性中读取的优先级更高

slf4j + jul

<!--slf日志门面-->  
<dependency>  
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-api</artifactId>  
    <version>2.0.10</version>  
</dependency>  

<!--绑定jdk14实现-->  
<dependency>  
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-jdk14</artifactId>  
    <version>2.0.10</version>  
</dependency>

slf4j-jdk14底层日志是jul,不需要额外引入依赖。slf4j-jdk14-2.0.10.jar源码包结构如下:

image.png

执行流程大致如下:

org.slf4j.jul.JULServiceProvider#initialize();

java.util.logging.Logger.getLogger("");

后续就是jul中逻辑了,可以参考juejin.cn/post/734586…

这里需要特别说明的是:因为jul出现要比slf4j要早,所以并没有遵循slf4j的规范,无法直接进行绑定。所以,需要添加一个适配层 Adaptation layer。通过适配器进行适配,从而间接地遵循了slf4j的规范。比如:

image.png 所以我们通过org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(App.class);获取到的logger对象其实是JDK14LoggerAdapter,它可以看成是java.util.logging.Logger的装饰器

更多关于java util logging可以参考我之前写的文章:

# 01.java日志之原生的日志框架jul

slf4j + log4j

<!--slf日志门面-->  
<dependency>  
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-api</artifactId>  
    <version>2.0.10</version>  
</dependency>  

<!--绑定log4j实现,需要导入适配器-->  
<dependency>  
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-log4j12</artifactId>  
    <version>2.0.10</version>  
</dependency>

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

因为log4j是最早出现的日志框架,slf4j出现的时间肯定要比log4j晚,所以slf4j为了适配log4j,需要增加额外的适配层,也就是slf4j-log4j12。而且log4j不同于jul,它是第三方框架,所以还需要引入log4j包。

这里我们也要注意下slf4j-log4j12的版本问题,比如这里引入的版本是2.0.10。此版本引入的slf4j-reload4j-2.0.10.jar,而更低版本的可能就不会,具体可以参考slf4j-log4j12pom.xml

slf4j-log4j12-2.0.10.jar源码包结构如下: image.png

更多关于log4j可以参考我之前写的文章:

# 01 log4j入门及xml配置详解

# log4j的基础组件和各种Appender

slf4j + logback

<!--slf日志门面-->  
<dependency>  
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-api</artifactId>  
    <version>2.0.10</version>  
</dependency>  
  
<!-- logback -->  
<dependency>  
    <groupId>ch.qos.logback</groupId>  
    <artifactId>logback-classic</artifactId>  
    <version>1.4.6</version>  
</dependency>

关于logback底层的文章还在撰写中,后续会更新。

slf4j + log4j2

注意版本问题

<!--slf日志门面-->  
<dependency>  
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-api</artifactId>  
    <version>1.7.26</version>  
</dependency>  
<!--使用log4j2的适配器进行绑定-->  
<dependency>  
    <groupId>org.apache.logging.log4j</groupId>  
    <artifactId>log4j-slf4j-impl</artifactId>  
    <version>2.10.0</version>  
</dependency>  
  
<!--log4j2的日志门面-->  
<dependency>  
    <groupId>org.apache.logging.log4j</groupId>  
    <artifactId>log4j-api</artifactId>  
    <version>2.11.1</version>  
</dependency>  
<!--log4j2的日志实现-->  
<dependency>  
    <groupId>org.apache.logging.log4j</groupId>  
    <artifactId>log4j-core</artifactId>  
    <version>2.10.0</version>  
</dependency>

slf4j 禁用日志

<!--slf日志门面-->  
<dependency>  
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-api</artifactId>  
    <version>2.0.10</version>  
</dependency>  
  
<dependency>  
    <groupId>org.slf4j</groupId>  
    <artifactId>slf4j-nop</artifactId>  
    <version>2.0.10</version>  
</dependency>

这个禁用原理就更简单了,就是生成一个logger,里面啥都没干。

slf4j项目实战

案例一

一个项目,一个模块用log4j,另一个模块用slf4j+log4j2,如何统一输出?
其实在某些中小型公司,这种情况很常见。我曾经见过某公司的项目,因为研发不懂底层的日志原理,日志文件里头既有log4j.properties,又有log4j2.xml,各种API混用,惨不忍睹!
还有人用着jul的API,然后拿着log4j.properties,跑来问我,为什么配置不生效!简直是一言难尽!
OK,回到我们的问题,如何统一输出!OK,这里就要用上slf4j的适配器,slf4j提供了各种各样的适配器,用来将某种日志框架委托给slf4j。其最明显的集成工作方式有如下:

图片

进行选择填空,将我们的案例里的条件填入,根据题意应该选log4j-over-slf4j适配器,于是就变成下面这张图

图片

就可以实现日志统一为log4j2来输出!

ps:根据适配器工作原理的不同,被适配的日志框架并不是一定要删除!以上图为例,log4j这个日志框架删不删都可以,你只要能保证log4j的加载顺序在log4j-over-slf4j后即可。因为log4j-over-slf4j这个适配器的工作原理是,内部提供了和log4j一模一样的api接口,因此你在程序中调用log4j的api的时候,你必须想办法让其走适配器的api。如果你删了log4j这个框架,那你程序里肯定是走log4j-over-slf4j这个组件里的api。如果不删log4j,只要保证其在classpth里的顺序比log4j前即可!

案例二

如何让spring以log4j2的形式输出?
spring默认使用的是jcl输出日志,由于你此时并没有引入Log4j的日志框架,jcl会以jul做为日志框架。此时集成图如下

图片

而你的应用中,采用了slf4j+log4j-core,即log4j2进行日志记录,那么此时集成图如下

图片

那我们现在需要让spring以log4j2的形式输出?怎么办?
OK,第一种方案,走jcl-over-slf4j适配器,此时集成图就变成下面这样了

图片

在这种方案下,spring框架中遇到日志输出的语句,就会如上图红线流程一样,最终以log4J2的形式输出!
OK,有第二种方案么?
有,走jul-to-slf4j适配器,此时集成图如下

图片

ps:这种情况下,记得在代码中执行

SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();

这样jul-to-slf4j适配器才能正常工作,详情可以查询该适配器工作原理。

天啦噜!要死循环

假设,我们在应用中调用了sl4j-api,但是呢,你引了四个jar包,slf4j-api-xx.jar,slf4j-log4j12-xx.jar,log4j-xx.jar,log4j-over-slf4j-xx.jar,于是你就会出现如下尴尬的场面

图片

如上图所示,在这种情况下,你调用了slf4j-api,就会陷入死循环中!slf4j-api去调了slf4j-log4j12,slf4j-log4j12又去调用了log4j,log4j去调用了log4j-over-slf4j。最终,log4j-over-slf4j又调了slf4j-api,陷入死循环!

以上部分内容参考: mp.weixin.qq.com/s/8VvBdRH_Y…