1 SLF4J介绍

SLF4J即Simple Logging Facade for Java,它提供了Java中所有日志框架的简单外观或抽象。因此,它使用户能够使用单个依赖项处理任何日志框架,例如:Log4j,Logback和JUL(java.util.logging)。通过在类路径中插入适当的 jar 文件(绑定),可以在部署时插入所需的日志框架。如果要更换日志框架,仅仅替换依赖的slf4j bindings。比如,从java.util.logging替换为log4j,仅仅需要用slf4j-log4j12-1.7.28.jar替换slf4j-jdk14-1.7.28.jar。

2 SLF4J源码分析

我们通过代码入手,层层加码,直观感受SLF4J打印日志,并跟踪代码追本溯源。主要了解,SLF4J是如何作为门面和其他日志框架进行解耦。

2.1 pom只引用依赖slf4j-api,版本是1.7.30

        <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>

2.1.1 执行一个Demo

public class HelloSlf4j {

    public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloSlf4j.class);
logger.info("Hello World info");
}
}

2.1.2 日志提示信息

绑定org.slf4j.impl.StaticLoggerBinder失败。如果在类路径上没有找到绑定,那么 SL​​F4J 将默认为无操作实现

2.1.3 跟踪源码

点开方法getLogger(),可以直观看到LoggerFactory使用静态工厂创建Logger。通过以下方法,逐步点击,报错也很容易找到,可以在bind()方法看到打印的异常日志信息。

org.slf4j.LoggerFactory#getLogger(java.lang.Class<?>)

org.slf4j.LoggerFactory#getLogger(java.lang.String)

org.slf4j.LoggerFactory#getILoggerFactory

org.slf4j.LoggerFactory#performInitialization

org.slf4j.LoggerFactory#bind

private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
// skip check under android, see also
// http://jira.qos.ch/browse/SLF4J-328
if (!isAndroid()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
} catch (NoClassDefFoundError ncde) {
String msg = ncde.getMessage();
if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
Util.report("Failed to load class "org.slf4j.impl.StaticLoggerBinder".");
Util.report("Defaulting to no-operation (NOP) logger implementation");
Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
} else {
failedBinding(ncde);
throw ncde;
}
} catch (java.lang.NoSuchMethodError nsme) {
String msg = nsme.getMessage();
if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
INITIALIZATION_STATE = FAILED_INITIALIZATION;
Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
Util.report("Your binding is version 1.5.5 or earlier.");
Util.report("Upgrade your binding to version 1.6.x.");
}
throw nsme;
} catch (Exception e) {
failedBinding(e);
throw new IllegalStateException("Unexpected initialization failure", e);
} finally {
postBindCleanUp();
}
}

进一步分析绑定方法findPossibleStaticLoggerBinderPathSet(),可以发现在当前ClassPath下查询了所有该路径的资源“org/slf4j/impl/StaticLoggerBinder.class”,这里可能没有加载到任何文件,也可能绑定多个,对没有绑定和绑定多个的场景进行了友好提示。这里通过路径加载资源的目的主要用来对加载的各种异常场景提示。

再往下代码StaticLoggerBinder.getSingleton()才是实际的绑定,并且获取StaticLoggerBinder的实例。这里如果反编译,你会发现根本没有这个类StaticLoggerBinder。

如果没有加载到文件,正如上边demo执行的结果一样,命中NoSuchMethodError异常,并打印没有绑定场景的提示信息。

方法findPossibleStaticLoggerBinderPathSet()的源码如下,可以发现类加载器通过路径获取URL资源。

            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}

2.2 pom引用依赖logback-classic

        <dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>

2.2.1 执行demo

可以看到正常的打印日志信息,并且没有任何异常

2.2.2 跟踪源码

这个时候如果再点击进入方法StaticLoggerBinder.getSingleton(),发现类StaticLoggerBinder是由包logback-classic提供的,并且实现了SLF4J中的接口LoggerFactoryBinder。StaticLoggerBinder的创建用到了单例模式,该类主要目的返回一个创建Logger的工厂。这里实际返回了ch.qos.logback.classic.LoggerContext的实例,再由该实例创建ch.qos.logback.classic.Logger。

UML类图如下:

2.3 pom再引入log4j-slf4j-impl

        <dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.9.1</version>
</dependency>

2.3.1 执行demo

打印日志如下,提示绑定了两个StaticLoggerBinder.class,但最终实际绑定的是ch.qos.logback.classic.util.ContextSelectorStaticBinder。这里边也验证了一旦一个类被加载之后,全局限定名相同的类就无法被加载了。这里Jar包被加载的顺序直接决定了类加载的顺序。

SLF4J: Found binding in [jar:file:/D:/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/D:/.m2/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.9.1/log4j-slf4j-impl-2.9.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
18:19:43.521 [main] INFO com.cj.HelloSlf4j - Hello World info

2.4 log4j-slf4j-impl和logback-classic的引入位置变换

如果Pom文件先引入log4j-slf4j-impl,再引入logback-classic

2.4.1 执行demo

根据日志打印结果,可以看到实际绑定的是org.apache.logging.slf4j.Log4jLoggerFactory;但是没有正常打印出日志,需要进行log4j2的日志配置。说明实际绑定的是og4j-slf4j-impl包中的org/slf4j/impl/StaticLoggerBinder.class文件;这里也验证了如果有引入了多个桥接包,实际绑定的是先加载到的文件;

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/D:/.m2/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.9.1/log4j-slf4j-impl-2.9.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/D:/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]
ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console. Set system property 'log4j2.debug' to show Log4j2 internal initialization logging.

2.5 类加载方式的变化

2.5.1 slf4j-api-1.7.30版本的打包技巧

反编译看slf4j-api-1.7.30-sources.jar,发现压根没有这个类org.slf4j.impl.StaticLoggerBinder,他怎么会编译成功呢?猜想是不是打包的时候把这个类排除掉了呢?通过git下载源码发现slf4j源码其实是有这个文件的,org/slf4j/impl/StaticLoggerBinder.class;这里使用了一个小技巧,打包的时候把实现类排除掉了,虽然不太优雅,但是思路很巧妙。

2.5.2 slf4j-api-2.0.0版本引入SPI(Service Provider Interface)

该版本通过使用SPI方式进行实现类的加载,感觉比之前的实现方式优雅了很多。桥接包只需要在这个位置:META-INF/services/,定义一个文件org.slf4j.spi.SLF4JServiceProvider(命名为SLFJ4提供的接口名),并且文件中指定实现类。只要引入这个桥接包,就可以适配到对应实现的日志框架。

以下是SPI方式加载的源码

private static List<SLF4JServiceProvider> findServiceProviders() {
ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
List<SLF4JServiceProvider> providerList = new ArrayList();
Iterator var2 = serviceLoader.iterator(); while(var2.hasNext()) {
SLF4JServiceProvider provider = (SLF4JServiceProvider)var2.next();
providerList.add(provider);
} return providerList;
}

2.5.3 类加载方式对比

2.6 SLF4J官方已经实现绑定的日志框架

slf4j已经提供了常用日志框架的桥接包,以及详细的文档描述,使用起来非常简单。

下图是SLF4J官网中提供的,表示了各种日志实现框架和SLF4J的关系:

2.7 总结

  • SLF4J API旨在一次绑定一个且仅一个底层日志框架。而且引入SLF4J后,不管是否可以加载到StaticLoggerBinder,或者加载到多个StaticLoggerBinder,都进行友好提示,用户体验上考虑都很周到。如果类路径上存在多个绑定,SLF4J 将发出警告,列出这些绑定的位置。当类路径上有多个绑定可用时,应该选择一个希望使用的绑定,然后删除其他绑定。
  • 单纯看SLF4J源码,其实整体设计实现上都很简单明确,定位非常清楚,就是做好门面。
  • 鉴于 SLF4J 接口及其部署模型的简单性,新日志框架的开发人员应该会发现编写 SLF4J 绑定非常容易。
  • 对于目前比较主流的日志框架都通过实现适配进行兼容支持。只要用户选择了SLF4J,就可以确保以后变更日志框架的自由。

3 SLF4J设计模式的使用

在slf4j中用到了一些经典的设计模式,比如门面模式、单例模式、静态工厂模式等,我们来分析以下几种设计模式。

3.1 门面模式(Facade Pattern)

1)解释

门面模式,也叫外观模式,要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。使用了门面模式,使客户端调用变得更加简单。

Slf4j制定了log日志的使用标准,提供了高层次的接口, 我们编码过程只需要依赖接口Logger和工厂类 LoggerFactory就可以实现日志的打印,完全不用关心日志内部的实现细节是logback实现的方式,还是log4j的实现方式。

2)图解

        Logger logger = LoggerFactory.getLogger(HelloSlf4j.class);
logger.info("Hello World info");

3)优点

解耦,减少系统的相互依赖。所有的依赖都是对门面对象的依赖,与子系统无关,业务层的开发不需要关心底层日志框架的实现及细节,在编码的时候也不需要考虑日后更换框架所带来的成本。

接口和实现分离,屏蔽了底层的实现细节,面向接口编程。

3.2 单例模式(Singleton Pattern)

1)解释

单例模式,确保一个类仅有一个实例,并提供一个访问它的全局访问点。

在SLF4J的适配包中都需要实现类StaticLoggerBinder,而类StaticLoggerBinder的实现就用了单例模式,而且是最简单的实现方法,在静态初始化器中直接new StaticLoggerBinder(),提供全局访问方法获取该实例。

2)UML图

3)优点

在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就防止其它对象对自己的实例化,确保所有的对象都访问一个实例

单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。

提供了对唯一实例的受控访问。

在内存里只有一个实例,减少了内存的开销,提高系统的性能。

4 启示

  • 尽管SLF4J整体代码短小但很精炼,可见门面模式运用好的威力。门面模式也为我们提供了对于多版本的实现如何统一定义接口以及兼容提供了参考。
  • SLF4J定义和实现方案对用户都很友好,同时又提供了各种桥接包,进行完善的文档指导使用。总之各项用户体验都很棒,这也许也是SLF4J目前最受欢迎的原因之一吧。
  • 我们要多思考面向接口编程的思想,降低代码耦合度,提高代码扩展性。
  • 使用SPI的方式,优雅的加载扩展实现。
  • 好产品是设计出来的,更是优化迭代出来的。

5 参考资料

作者:京东物流 曹俊

来源:京东云开发者社区

SLF4J门面日志框架源码探索的更多相关文章

  1. laravel框架源码分析(一)自动加载

    一.前言 使用php已有好几年,laravel的使用也是有好长时间,但是一直对于框架源码的理解不深,原因很多,归根到底还是php基础不扎实,所以源码看起来也比较吃力.最近有时间,所以开启第5.6遍的框 ...

  2. CI框架源码阅读笔记3 全局函数Common.php

    从本篇开始,将深入CI框架的内部,一步步去探索这个框架的实现.结构和设计. Common.php文件定义了一系列的全局函数(一般来说,全局函数具有最高的加载优先权,因此大多数的框架中BootStrap ...

  3. Eureka源码探索(一)-客户端服务端的启动和负载均衡

    1. Eureka源码探索(一)-客户端服务端的启动和负载均衡 1.1. 服务端 1.1.1. 找起始点 目前唯一知道的,就是启动Eureka服务需要添加注解@EnableEurekaServer,但 ...

  4. iOS开发之Masonry框架源码深度解析

    Masonry是iOS在控件布局中经常使用的一个轻量级框架,Masonry让NSLayoutConstraint使用起来更为简洁.Masonry简化了NSLayoutConstraint的使用方式,让 ...

  5. YII框架源码分析(百度PHP大牛创作-原版-无广告无水印)

           YII 框架源码分析    百度联盟事业部——黄银锋 目 录 1. 引言 3 1.1.Yii 简介 3 1.2.本文内容与结构 3 2.组件化与模块化 4 2.1.框架加载和运行流程 4 ...

  6. Masonry框架源码深度解析

    Masonry是iOS在控件布局中经常使用的一个轻量级框架,Masonry让NSLayoutConstraint使用起来更为简洁.Masonry简化了NSLayoutConstraint的使用方式,让 ...

  7. iOS开发之Masonry框架源码解析

    Masonry是iOS在控件布局中经常使用的一个轻量级框架,Masonry让NSLayoutConstraint使用起来更为简洁.Masonry简化了NSLayoutConstraint的使用方式,让 ...

  8. Gin框架源码解析

    Gin框架源码解析 Gin框架是golang的一个常用的web框架,最近一个项目中需要使用到它,所以对这个框架进行了学习.gin包非常短小精悍,不过主要包含的路由,中间件,日志都有了.我们可以追着代码 ...

  9. TCP/UDP简易通信框架源码,支持轻松管理多个TCP服务端(客户端)、UDP客户端

    目录 说明 TCP/UDP通信主要结构 管理多个Socket的解决方案 框架中TCP部分的使用 框架中UDP部分的使用 框架源码结构 补充说明 源码地址 说明 之前有好几篇博客在讲TCP/UDP通信方 ...

  10. 介绍开源的.net通信框架NetworkComms框架 源码分析

    原文网址: http://www.cnblogs.com/csdev Networkcomms 是一款C# 语言编写的TCP/UDP通信框架  作者是英国人  以前是收费的 售价249英镑 我曾经花了 ...

随机推荐

  1. Oracle宕机之PMON (ospid: 248987): terminating the instance due to error 484(另附hugepage配置方法)

    数据库版本:11.2.0.4 RAC环境 操作系统版本:Asianux Server release 7.3 数据库报错分析 接到业务消息,应用无法访问,开发人员查看日志后发现无法连接数据库. 查看数 ...

  2. [MySQL]innodb_flush_log_at_trx_commit与sync_binlog

    1 innodb_flush_log_at_trx_commit 辨析 innodb_flush_log_at_trx_commit = 0 : 每秒将日志缓冲区写入log file,并同时flush ...

  3. .NET Core MongoDB数据仓储和工作单元模式封装

    前言 上一章我们把系统所需要的MongoDB集合设计好了,这一章我们的主要任务是使用.NET Core应用程序连接MongoDB并且封装MongoDB数据仓储和工作单元模式,因为本章内容涵盖的有点多关 ...

  4. AtCoder Beginner Contest 236 E - Average and Median

    给定一个序列,要求相邻两个数至少选一个,求选出数的最大平均数和最大中位数 \(\text{sol}\):二分答案. 二分平均数\(\text{mid}\),将每个元素减去\(\text{mid}\), ...

  5. 活字格性能优化技巧(3):如何巧用CDN提升含页面的访问速度

    本文由葡萄城技术团队于博客园原创并首发转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 上两篇中我们分享了如何利用数据库主键和表格设置默认不加载数据来提升应用 ...

  6. MySQL(四)用户与权限管理

    用户与权限管理 用户管理 MySQL用户分为普通用户和root用户,提供了许多语句来管理包括登录.退出MySQL服务器.创建用户.删除用户.密码管理和权限管理等内容. 登录MySQL服务器 mysql ...

  7. LNMP搭建静态网页服务器

    chattr -i default/.user.ini LNMP搭建使用 1.安装screen,命令或者操作可以一直运行下去 yum install screen 2.获取及安装 LNMP wget ...

  8. 【迭代器设计模式详解】C/Java/JS/Go/Python/TS不同语言实现

    简介 迭代器模式(Iterator Pattern),是一种结构型设计模式.给数据对象构建一套按顺序访问集合对象元素的方式,而不需要知道数据对象的底层表示. 迭代器模式是与集合共存的,我们只要实现一个 ...

  9. 读《mysql是怎样运行的》有感

    最近读了一本书<mysql是怎样运行的>,读完后在大体上对mysql的运行有一定的了解.在以前,我对mysql有以下的为什么: InnoDB中的表空间.段.区和页是什么? redo log ...

  10. vue-cli3构建和发布 实现分环境打包步骤(给不同的环境配置相对应的打包命令)

    https://panjiachen.github.io/vue-element-admin-site/zh/guide/essentials/deploy.html#%E6%9E%84%E5%BB% ...