SLF4J源码分析

介绍

官网:http://www.slf4j.org/

github:https://github.com/qos-ch/slf4j

SLF4J(Simple Logging Facade for Java),它为Java的日志系统提供了一套统一的接口(门面),即:作为各种日志框架(java.util.logging,logback,log4j)的抽象。

通过引入SLF4J,可以使项目与logging具体的实现分离,在提供了一致的接口的同时,提供了灵活选择logging实现的能力。(引入SLF4J的库/应用意味着仅添加一个强制性依赖项slf4j-api.jar)

1、为什么要设计出一个日志接口的抽象层?

我们都知道,日志对于一个系统来说非常重要。同样,我们在开发出了一个库时,也需要打印一些调试或者运行日志,而我们系统往往会引入大量的第三方库。这是,就会遇到一个问题:假设我们系统使用的是Log4j日志框架,引入了RMQ库使用的是Logback框架,这时系统就出现了两个日志框架,维护起来非常麻烦。

解决这个问题的方法是引入一个适配层。例如:

如果我们都是通过SLF4J这种统一的接口,那么RMQ库在发布时就无需带着具体日志框架的实现,这样我们系统引入RMQ后,仍然使用的是我们系统中引入的日志实现了,这样就方便了维护。

slf4j只做两件事情:

  • 提供日志接口
  • 提供获取具体日志对象的方法

说明:这种抽象的思想,在软件开发中很常见。

2、SLF4J和JCL区别:

在SLF4J之前,Apache Common Logging(即Jakarta Commons Logging,简称JCL)也提供了类似的功能(即:统一的日志接口)。它与SLF4J的区别在于:

  • JCL即提供了统一的接口,也提供了一套默认的实现;SLF4J则只提供了接口层
  • JCL采用运行时绑定,通过Classloader体系加载相应的logging实现;SLF4J采用了静态绑定
  • SLF4J在接口易用性上更有优势,大大减少了不必要的日志拼接:
    • JCL为了避免无效的字符串拼接,一般需要通过if判断:
    • SLF4J则提供了占位符"{}",只在必要的情况下才会进行日志字符串处理和拼接:
//JCL
if (log.isInfoEnabled()){
  log.info("testid:"+id+",cont:"+JSON.toJSONString(jsonstr));
}

//slf4j
log.info("testid:{},cont:{}",id,JSON.toJSONString(jsonstr));

推荐使用slf4j中占位符原因主要有两点:

  • 当设置的日志级别高于某条代码中的日志级别时,使用占位符可以免掉字符串拼接操作;
  • 占位符底层使用的是StringBuilder进行的拼接,性能比“+”要好;

注:在SLF4J和JCL中,推荐使用前者。

3、SLF4J使用:

SLF4J的使用非常简单:

  • 引入SLF4J依赖 (slf4j-api.jar)
  • 引入一种SLF4J的实现,比如:logback、log4j...

然后:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public MyClass {
  Logger logger = LoggerFactory.getLogger(MyClass.class);

  puhblic void method() {
    logger.info("hello world...");
  }
}

注:从1.6.0开始,如果在类路径上未找到绑定,则SLF4J将默认为无操作实现;

下图从SLF4J官网中找到的一个图,表示了各种实现类和SLF4J的关系:

 

总之,SLF4J接口及其各种适配器非常简单,不依赖任何类加载器,所以SLF4J不会遇到类加载器问题或Commons Logging(JCL)所观察​​到的内存泄漏。实际上,每个SLF4J绑定在编译时都进行了硬连线,以使用一个且仅一个特定的日志记录框架。

静态绑定原理

和Apache Common Logging不同,SLF4j采用了静态绑定来确定具体日志库。静态绑定就是:

  • 每一个具体的日志库定义一个包名和类名都相同的类: org.slf4j.impl.StaticLoggerBinder,这个类的功能就是调用具体的日志库,该类存放在Adaptation layer(适配层)或者native implementation of slf4j-api(实现包)的jar包中;(该类在slf4j-api打成jar包时被mvn移除)
  • SLF4j的使用者只要把具体日志库对应的Adaptation layer或者native implementation of slf4j-api的jar包放入classpath中,SLF4j便会装载(load)对应版本的org.slf4j.impl.StaticLoggerBinder,从而调用具体的日志库;
  • slf4j-api.jar中通过classLoader.getResources("org/slf4j/impl/StaticLoggerBinder.class")来加载classpath中具体的日志库中的StaticLoggerBinder类;

SLF4J相比JCL的一大优势是采用了静态绑定,避免了在OSGI等场景中通过classloader动态绑定造成的困扰。

参考:https://blog.csdn.net/weixin_34248023/article/details/91891106

1、1.7.25版本的slf4j-api.jar静态绑定过程分析:

1.1)源码分析:

在demo中可知,使用SLF4J的LoggerFactory.getLogger(Class<?>)方法获取一个Logger对象,这个过程完成了和具体日志实现类的绑定。通过slf4j-api.jar源码,SLF4J是调用bind()方法实现的绑定。

1)bind()方法:

  1. 调用findPossibleStaticLoggerBinderPathSet()方法获取classpath上所有的org/slf4j/impl/StaticLoggerBinder.class,用来报告(没有找到也不会报错);
  2. 执行StaticLoggerBinder.getSingleton()实现静态绑定,如果没有日志实现框架,则抛出异常;
  3. 执行reportActualBinding()方法

 

2)findPossibleStaticLoggerBinderPathSet()方法:

通过jdk提供的ClassLoader.getStstemResources()方法获取指定资源的URI。

 

可以发现,在slf4j-api.jar包中根本没有org.slf4j.impl.StaticLoggerBinder 这个类,所以,如果没有具体的日志实现库,那么在执行到StaticLoggerBinder.getSingleton()方法时就会抛出NoClassDeffoundException

 

3)日志实现库:

slf4j-log4j12库中的org.slf4j.impl.StaticLoggerBinder

1.2)疑问:

通过slf4j源码,LoggerFactory.java文件有一行import org.slf4j.impl.StaticLoggerBinder; 但是上面我们发现在slf4j-api.jar中居然没有该org.slf4j.impl.StaticLoggerBinder类,也就是说slf4j-api这个工程是无法编译通过的,又是如何打成slf4j-api.jar的呢?

写一个工程A,类似sfl4j-api,然后把StaticLoggerBinder类删掉,工程虽然报错,但是可以通过mvn install打包成功;

写一个工程B,引入A.jar,然后调用其中方法,会发现报错:Unresolved compilation problem: 从A.jar包中查看相应的LoggerFatory类,居然是这样的:

可以发现:虽然上面可以用mvn打包成功,但是由于A工程是一个编译有问题的工程,反编译字节码文件可以看到方法全都抛出异常,这说明在打包时,LoggerFactory类生成的字节码文件是不完整的,带有错误的。

通过slf4j-api源码可以发现,其实在slf4j-api工程中是有org.slf4j.impl.StaticLoggerBinder.java类的,只是在mvn打包的时候通过ant插件,将org.slf4j.impl.StaticLoggerBinder.class移除掉了。骗过了jdk,使得LoggerFactory.class是一个完整的,可以校验通过的字节码文件。

1.3)总结:

先来明确一下 Java 的绑定(Binding)的概念,Java 本身只支持静态(static)绑定与运行时(runtime)绑定,直到与 JDK 1.6 版本一起发布的 JSR269 才能进行编译时绑定,编译时绑定最有代表的是lomok 在编译过程中修改字节码。

1.7.25版本的SFL4j 的 logger 实例是 new 出来的(通过StaticLoggerBinder单例),绑定 LogContext 的 StaticLoggerBinder(中介类) 是写死的,编译时并没有处理任何逻辑,也谈不上什么编译时绑定,而且翻遍了 SLF4j 文档也没有找到任何有关编译时绑定的材料,官方只提到了 “static binding”, 所以,SLF4j使用的是 Convention over Configuration(CoC)– 惯例优于配置原则,不管是什么日志框架,只加载org.slf4j.impl.StaticLoggerBinder。这完美契合了软件设计的 KISS(Keep It Simple, Stupid)原则。

而 Commons-logging 魔法(magic)一样的动态加载虽然设计很高大上,在应用领域却直接被打脸,低效率、与 OSGi 共同使用所导致的 ClassLoader 问题更是火上浇油,所以员外与大家共勉,写代码切勿炫技。

参考:

https://juejin.im/post/6844903574116237326

https://www.jianshu.com/p/b562b7ff499f

2、1.8版本的slf4j-api.jar静态绑定过程分析:

SLF4J 1.8中最大的改进就是摒弃了之前的hard code的代码绑定(要求具体实现日志框架中必须要有一个org.slf4j.impl.StaticLoggerBinder.java),而是使用了更加优雅、耦合更松的SPI方式进行服务发现。我们看看1.8版本slf4j-api中对日志绑定的改进:

  • 提供了org.slf4j.spi.SLF4JServiceProvider服务接口用于SPI绑定
  • 改进了org.slf4j.LoggerFactory.bind()的实现,采用SPI方式进行SLF4JServiceProvider服务发现和绑定
  • 不再支持1.8版本以前的按照约定的类型StaticXxxBinder约定类名进行绑定的方式

由此可见,1.8版本和之前的版本是不兼容的(http://www.slf4j.org/codes.html#version_mismatch)。而且1.8往上的版本都是beta,没有一个是stable/release的。

说明:(官网)

从客户端的角度来看,slf4j-api的所有版本都是兼容的。只需要确保绑定的版本与slf4j-api.jar的版本匹配即可。在初始化时,如果SLF4J怀疑可能存在sfl4j-api与绑定版本不匹配的问题,它将发出有关可疑不匹配的警告。

1.1)源码分析:

1)bind方法:

2)findServiceProviders()方法:

3)日志实现库:

slf4j-api:1.8.0-beta-2版本,对应的logback-classic版本为logback-classic:1.3.0-alpha4。为了兼容1.8的SLF4J,logback-classic提供了SPI服务配置文件,如下图。这样,在启动阶段,SLF4J就可以通过ServiceLoader找到logback-classic并进行注册了。

同时,最新版的logback也去掉了org.slf.impl包,彻底摒弃了老版本SLF4J的支持。

同样,在slf4j-log4j12-1.8版本中,也是去掉了org.slf.impl包,提供了SPI服务配置文件:

总结:

slf4j-api1.8版本整个流程和1.7的基本一致,除了采用了更优雅的服务发现机制,在其他方面,SLF4J 1.8与之前版本差别很小。

参考:

https://www.jianshu.com/p/6cf21fb18639

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赶路人儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值