日志门面技术(4):slf4j

日志框架出现的历史顺序:Log4j → JUL → JCL → slf4j → logback → log4j2

前言:JCL日志门面淘汰史

常见的日志门面:JCL、slf4j。为啥日志门面已经有JCL了,还要推出slf4j??

JCL设计缺陷

当时只考虑了主流的日志框架JUL、Log4j,随着技术的发展,后面会出现很多优秀的日志,这些技术我们要使用的话,它默认是不支持的,就表示我们需要在开发时修改源代码进行扩展,一般在企业开发时我们是不会这么做的,因此JCL就被淘汰了

JCL被淘汰了,那么还有谁能站出来帮我们统一管理和维护所有的API呢?答:slf4j,它可以支持现在所有的主流日志框架,以及未来出现新的日志技术进行扩展的接口
 

SL4J 的使用

简单日志门面(Simple Logging Facade For Java)

SL4J 主要是为了给Java日志访问提供一个标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j 和 logback等,当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到,对于一般的Java项目而言,日志框架会选择slf4j-api作为门面,配上具体的实现框架(log4j、logback等),中间试验桥接器完成桥接

描述

SL4J 是目前市面上最流行的日志门面,现在的项目中,基本都是使用SL4J作为我们的日志系统。SL4J日志门面主要提供两大功能:

  1. 日志框架的绑定
  2. 日志框架的桥接

入门案例

创建普通的maven项目即可

1. 导入相关slf4j依赖

    <dependencies>
        <!--junit测试类 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
        </dependency>

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

        <!--slf4j内置的日志实现(功能较为简单,一般使用桥接第三方的日志实现) -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.30</version>
        </dependency>
    </dependencies>

2. 测试用例

public class Demo {
    // 全局logger对象
    private static final Logger logger = LoggerFactory.getLogger(Demo.class);

    @Test
    public void testQuick(){
        // 1.普通输出(级别依次由高到低)
        logger.error("error msg");
        logger.warn("warn msg");
        logger.info("info msg"); // 默认级别
        logger.debug("debug msg");
        logger.trace("trace msg");

        // 2.占位符输出,用{}括号表示占位符
        String name = "梅花十三";
        int age = 18;
        logger.info("用户:{},{}",name,age);

        // 3.将系统的异常输出
        try {
            int i = 1/0;
        }catch (Exception e){
            //e.printStackTrace();
            logger.error("异常信息:",e);
        }
    }
}

3. 测试结果

说明:slf4j不仅提供普通的日志信息打印,还包含了很多重载方法,比如对系统异常的输出

slf4j 日志绑定(Binding)

在入门案例中,我们使用sf4j 绑定了内置的日志简单实现框架

它是如何绑定其他日志主流框架呢?

如果只引入日志门面依赖,而未引入相关日志实现框架,则功能默认处于关闭状态。

1. 我们可以打开sf4j官网:SLF4J,点击用户手册,查看具体的流程,如下:

2. 滑至最下方,可以看到官网提供的绑定日志实现的具体流程图 

绑定说明

情况一:图中最左侧第一个显示的 /dev/null表示,如果只引入slf4j-api 日志门面依赖,而不引入具体的日志实现框架,日志功能默认处于关闭状态,无法进行任何的日志输出。

情况二:图中三个深蓝色模块分别是:logback、slf4j 内置的简单日志实现框架、nop。这三个日志框架设计是比slf4j晚的,因此默认就支持slf4j的API规范,只需要导入其实现即可

情况三:中间两个湖绿色的模块是 log4j、jdk14(JUL: java.util.logging)。这两个日志实现框架设计比较早,默认是没有遵循slf4j的API规范,无法进行直接绑定,中间需要加一层适配层 Adaptation layer,通过这个适配器来适配具体的实现,间接的遵循了slf4j的API规范

一、绑定logback

从功能和性能上而言都比log4j更强大

1. 导入两个实现包依赖(在上述入门案例中的pom.xml 基础上,增加logback的依赖即可)

    <dependencies>
        <!--junit测试类 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
        </dependency>

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

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

        <!-- logback 日志实现框架-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.9</version>
        </dependency>
    </dependencies>

小知识:导入logback-classic.jar 一个依赖即可,因为maven有传递思想,会自动传递core包

2. 测试用例输出结果如下

3. 把slf4j的内置实现框架给注释掉

4. 继续运行测试用例:控制台的输出字体颜色变成了黑色,simple的日志实现框架字体是红色的

注释掉simple的日志框架依赖,如果还出现错误,记得把maven刷新一下

!! 注意:默认日志级别是debug,图中注释说明有误

二、绑定slf4j-simple

slf4j 内置的日志实现框架,功能简单,一般不大使用

在上述入门案例代码中,用的就是simple的日志实现依赖,这里就不再二次贴出演示了

三、绑定slf4j-nop

日志开关,导入slf4j-nop.jar依赖后,表示slf4j将不会使用任何实现框架了

与不绑定具体的日志实现框架的效果是一样的,日志功能将被关闭。

1. 引入slf4j-nop依赖

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

<!--slf4j-nop 日志开关,一旦引入依赖,所有日志实现框架将失效-->
<dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-nop</artifactId>
      <version>1.7.36</version>
</dependency>

2. 测试用例执行效果

!! 注意:如果引入sf4j-api 不引入其他具体实现日志,虽然日志也会失效,但是控制台会报警告, 引入sf4j-nop需要注释掉其他具体实现日志,否则会报警告,并且还会正常打印日志

四、绑定log4j

Apache下的一款开源的日志框架,可以控制日志信息输出到控制台、文件、数据库。

前面也说了,log4j 和 jdk14(JUL: java.util.logging)这两个日志实现框架设计比较早,默认是没有遵循slf4j的API规范,无法进行直接绑定,中间需要加一层适配层 Adaptation layer

要点:通过这个适配器来适配具体的实现,间接遵循slf4j的API规范

1. 增加Adaptation layer适配器依赖 + log4j依赖

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


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

<!-- log4j 日志实现 -->
<dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.13</version>
</dependency>

2. 运行测试用例如下

可以看到log4j日志实现绑定成功了,上述警告提示找不到该logger对象的appenders处理器,它希望我们设置并初始化一个系统配置信息 ,这个问题我在《日志实现框架(1):Log4j》章节讲过,这里不再解释,我们把之前搭建的配置文件,复制到当前项目的资源目录下

3. 搭建log4j.properties配置文件 

4. 再次运行测试用例:可以看到日志信息正常输出,表示log4j绑定成功了,当前使用的是log4j

五、绑定JUL

全称 Java Util Logging,Java原生的日志框架,JDK自带的,使用无需另外引用第三方类库

要点:通过这个适配器来适配具体的实现,间接遵循slf4j的API规范

1.  JUL是JDK自带的,无需引入依赖就能使用,但是采用的是slf4j技术门面,默认不支持JUL,因此需要引入Adaptation layer适配器依赖

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

<!-- 绑定jul 日志实现,需要导入适配器 -->
<dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-jdk14</artifactId>
      <version>1.7.36</version>
</dependency>

2. 测试用例运行结果:成功绑定并使用的日志实现框架是JUL

绑定日志的实现(Binding)

如上所述,SLF4J 支持各种日志框架,SLF4J发行版附带了几个称为“SLF4J绑定”的 jar 文件,每个绑定对应一个受支持的框架。

使用 slf4j 的日志绑定流程

  ① .添加 slf4j-api 的依赖
  ②.使用slf4j的API在项目中进行统一的日志记录
  ③.绑定具体的日志实现框架
       a.绑定已经实现了slf4j的日志框架,直接添加对应依赖
       b.绑定没有实现slf4j的日志框架, 先添加日志的适配器,再添加实现类的依赖
  ④.slf4j有且仅有一个日志实现框架的绑定(如果出现多个默认使用第一个依赖日志实现)


 

1. 查看LoggerFactory 日志工厂获取logger日志记录器的源码实现

// 全局logger对象
private static final Logger logger = LoggerFactory.getLogger(Demo.class);

提示:logger对象是从日志工厂中获取的,因此来查看下日志工厂是如何初始化进行logger装载的

2. 在getILoggerFactory获取工厂方法中,包含了 performInitialization() 初始化工厂方法,如下

 这里涉及到一个名为INITIALIZATION_STATE 的静态变量,用来记录当前初始化的状态。默认是UNINITIALIZED,第一次调用getILoggerFactory()方法时,检查到INITIALIZATION_STATE == UNINITIALIZED,就会调用performInitialization()方法来进行初始化。

performInitialization()方法在初始化完成时,会设置INITIALIZATION_STATE为SUCCESSFUL_INITIALIZATION。这样后面的switch语句就会调用StaticLoggerBinder.getSingleton().getLoggerFactory()来返回需要的ILoggerFactory。

3. 日志工厂是通过bind()方法,来绑定具体的日志实现类

4. findPossibleStaticLoggerBinderPathSet () 作用是查找当前classpath下的StaticLoggerBinder的实现,如果有多个的话,则reportMultipleBindingAmbiguity()和reportActualBinding()方法会在绑定前后打印相应的信息。

findPossibleStaticLoggerBinderPathSet()方法其实就是通过classloader的getResources()方法找到所有的名为”org/slf4j/impl/StaticLoggerBinder.class”的resource。

5. 我们再回来看bind()方法,其实真正绑定的代码只有一行 

这里调用了StaticLoggerBinder.getSingleton()方法。StaticLoggerBinder类的权限定名,恰好和findPossibleStaticLoggerBinderPathSet()方法中查找的一致。

思考:StaticLoggerBinder类从哪里来? 具体的log api实现要如何做才能和slf4j绑定和装载?

在slf4j-api的源代码中,的确有对应的package和类存在

但是打开打包好的slf4j-api.jar,却发现根本没有这个impl 的包。

在slf4j-api项目的pom.xml文件中,我们可以找到下面的内容:

这里通过调用ant在打包为jar文件前,将package org.slf4j.impl和其下的class都删除掉了。

实际impl包内的代码,只是用来占位以保证可以编译通过。需要在运行时再进行绑定。

6. 我们再来看,具体的log api实现要如何做才能和slf4j绑定和装载

如下,在引入了slf4j 内置的日志实现框架simple 以及 JUL日志实现框架中,可以找到其jar包的该类路径:”org/slf4j/impl/StaticLoggerBinder.class”,

!! 注意:同样在slf4j支持的其他日志实现框架依赖的jar包中,例如slf4j-log4j12、slf4j-jdk14、logback-classic 都能找到该类路径的同名类

7. 继续看回StaticLoggerBinder的代码

8. 以slf4j-simple为例:这里的getLoggerFactory()方法会返回slf4j-simple实现的SimpleLoggerFactory。

9. SimpleLoggerFactory实现slf4j定义的 ILoggerFactory接口,getLogger()方法中负责创建SimpleLogger对象并返回 (为了提高性能做了cache)。

结论:因此从这里就能得出,slf4j 绑定的具体实现类接口,是在bind() 方法完整绑定的。

其他日志实现框架流程也是如此,类似的slf4j-log4j12中会返回Log4jLoggerFactory,而Log4jLoggerFactory中通过调用log4j的LogManager来创建log4j的Logger对象并通过Log4jLoggerAdapter类来包装为slf4j的Logger(adapter模式)。

log4jLogger = LogManager.getLogger(name);
Logger newInstance = new Log4jLoggerAdapter(log4jLogger);

SLF4J 绑定日志框架原理解析

slf4j在运行时绑定具体的日志实现框架。关键之处就在于 org.slf4j.impl.StaticLoggerBinder 

  1. SLF4J 通过LoggerFactory 加载日志具体的实现对象
  2. LoggerFactory在初始化的过程中,会通过performinitialization()方法绑定具体的日志实现
  3. 在绑定具体实现的时候,通过类加载器,加载org/slf4g/impl/StaticLoggerBinder.class
  4. 所以,只要是一个日志实现框架,在org/slf4g/impl 包中提供一个自己的StaticLoggerBinder类,在其中提供具体日志实现的LoggerFactory就可以被SLF4J 所加载

Logger 和 LoggerFactory是slf4j定义好的。LoggerFactory通过装载StaticLoggerBinder类来绑定具体的日志实现框架,得到其日志实现框架中ILoggerFactory接口的实现类。该LoggerFactory的实现类会创建其对应日志实现框架的logger对象。并调用logger对象来写日志。

结论:业务代码此时不知道底层是哪个日志实现框架,从而摆脱对具体日志实现框架的依赖

桥接旧的日志框架(Bridging)

解决项目中日志的遗留问题,当系统中存在之前的日志API,可通过桥接转换到slf4j的实现

slf4j桥接说明官网:Log4j Bridge

通常,您所依赖的某些组件依赖于 SLF4J 以外的日志记录 API。您还可以假设这些组件在不久的将来不会切换到 SLF4J。为了处理这种情况,SLF4J 附带了几个桥接模块,这些模块将对 log4j、JCL 和 java.util.logging API 的调用重定向到好像它们是对 SLF4J API 进行的。

操作流程

  1. 先去除之前老的日志框架的依赖
  2. 添加SLF4J提供的桥接组件
  3. 为项目添加SLF4J的具体实现

举例说明

场景:项目初期未考虑到架构层次问题,日志框架是log4j,随着项目功能迭代,现有日志框架不满足业务需求,需升级为日志门面技术slf4j + logback具体的日志实现框架。

诉求:我们希望是不想修改原有log4j原有代码!

操作步骤:

  1. 去除原有log4j依赖
  2. 引入log4j-over-slf4j.jar 桥接依赖(作用:将应用程序迁移到 SLF4J,而无需更改一行代码
  3. 引入slf4j-api.jar 日志门面依赖
  4. 引入目标使用日志框架 logback-classic.jar 依赖

1. 假设现在的项目采用的是log4j ,依赖如下

<dependencies>
     <!--junit测试类 -->
     <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
     </dependency>


     <!-- log4j 日志实现 -->
     <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.13</version>
     </dependency>
</dependencies>

测试用例执行结果

2. 更换日志实现框架为logback

<dependencies>
     <!--junit测试类 -->
     <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
     </dependency>


     <!-- log4j 日志实现 -->
     <!-- 1. 去除log4j依赖
     <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.13</version>
     </dependency>-->

    <!-- 2.引入新的日志框架 -->
     <!--slf4j 日志门面 -->
     <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
     </dependency>

     <!-- logback 日志实现框架-->
     <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.9</version>
     </dependency>
</dependencies>

问题:如果仅仅只是把原log4j 依赖去除,加上新的logback日志框架依赖,那么原log4j的代码会出现 报红异常,导致代码不可运行


解决:只需要再引入slf4j提供的log4j的桥接依赖即可,无需调整代码

<!-- log4j 桥接器 -->
<dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>log4j-over-slf4j</artifactId>
        <version>1.7.36</version>
</dependency>

3. 测试代码用例 

结论:增加了log4j的桥接依赖后,无需修改原log4j的代码,虽然是log4j的代码,但控制台日志输出框架为logback

原理:加入桥接依赖后,原log4j代码不会直接调用log4j底层,而是会委托给SLF4J 所加载,再由SLF4J去调用新引入的logback日志框架,从而不需要修改原log4j的代码,也能替换新的日志实现框架

桥接旧的日志框架注意事项

桥接器和适配器不能同时出现,否则会出现死循环,造成栈溢出!

例如,假设我引入了slf4j技术门面,并绑定了log4j日志实现框架,但是我又引入了log4j桥接器

<dependencies>
     <!--junit测试类 -->
     <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
     </dependency>

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

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

     <!-- log4j 日志实现 -->
     <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.13</version>
     </dependency>

     <!-- log4j 桥接器 -->
     <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
            <version>1.7.36</version>
      </dependency>

</dependencies>

运行测试用例,报栈溢出


注意问题总结

  1. jcl-over-slf4j.jar 桥接依赖 和 slf4j-jcl.jar 适配器不能同时部署,前一个jar文件将导致JCL将日志系统的选择委托给SLF4J,后一个jar文件将导致SLF4J将日志系统的选择委托给JCL,从而导致无限循环
  2. Log4j-over-slf4j.jar 桥接依赖 和 slf4j-log4j12.jar (SLF4J 绑定的log4j日志实现) 不能同时出现
  3. Jul-to-slf4j.jar桥接依赖 和 slf4j-jdk14.jar (SLF4J 绑定的JUL日志实现) 不能同时出现
  4. 所有的桥接都只对Logger日志记录器对象有效,如果程序中调用了内部的配置类或者是Appender,Filter等对象,将无法产生效果

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值