5.第五章Slf4j
5.1日志门面概述
5.1.1门面模式(外观模式)
我们先谈一谈GOF23中设计模式其中之一。
门面模式(Facade Pattern),也称之为外观模式,其核心为:外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用。
外观模式主要是体现了Java中的一种好的封装性,更简单的说,就是对外提供的接口要尽可能的简单。
5.1.2日志门面
前面介绍的几种日志框架,每一种日志框架都有自己单独的API,要使用对应的框架就要使用其对应的API,这就大大的增加了应用程序代码对于日志框架的耦合性。
为了解决这个问题,就是在日志框架和应用程序之间架设一个沟通的桥梁,对于应用程序来说,无论底层的日志框架如何变,都不需要有任何感知,只要门面服务做的足够好,随意换另外一个日志框架,应用程序不需要修改任意一行代码,就可以直接上线。
5.1.3常见的日志框架及日志门面
常见的日志实现:JUL、Log4j、Logback、Log4j2
常见的日志门面:JCL、Slf4j
出现顺序:Log4j-->JUL-->JCL-->Slf4j-->Logback-->Log4j2
5.2Slf4j日志门面
5.2.1Slf4j简介
简单日志门面(Simple Logging Facade Pattern For Java)Slf4j主要是为了给Java日志访问提供一套标准、规范的API框架,其主要的意义在于提供接口,具体实现可以交由其他日志框架,例如Log4j和Logback等。当然Slf4j自己也有功能较为简单的实现,但是一般很少用到。对于一般的Java项目而言,日志框架会选择Slf4j作为门面,配置具体的实现框架(Log4j、Logback等),中间使用桥接器完成桥接。所以我们可以得出Slf4j最重要的两个功能就是对于日志框架的绑定以及日志框架的桥接。
5.2.2Slf4j桥接技术
通常,我们依赖的某些组件依赖于Slf4j以外的日志API。我们可能还假设这些组件在不久的将来不会切换到Slf4j,为了处理这种情况,Slf4j附带了几个桥接模块,这些模块会将对Log4j,JCL(JCL内也有对日志的实现),JUL API的调用重定向为行为,就好像对Slf4j API进行的操作一样。
5.3案例分析
首先我们还是创建一个mavne项目,引入需要的依赖
<dependencies>
<!-- 单元测试依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- slf4j核心依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!-- slf4j自带的简单日志实现 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
本次我们创建的为Slf4j包下的Logger,如下图
Logger
通过观察Logger类我们可以了解到Slf4j具有trace、debug、info、warn、error五个级别
trace:日志追踪信息
debug:日志调试信息
info:日志关键信息(默认打印级别)
warn:日志警告信息
error:日志错误信息
在没有任何日志框架集成的基础之上,Slf4j使用的就是自带的框架,并且slf4j-simple也必须以单独的依赖的形式导入进来,经过测试,Slf4j自带日志实现输出如下
在集成其他日志框架之前,我们先来了解一下Slf4j与其他日志框架之前的关系结构,如下图(官网地址:https://www.slf4j.org/images/concrete-bindings.png)
根据上图,我们了解到Slf4j日志门面共有3种情况对日志实现进行绑定
1.在没有绑定任何日志实现的基础上,日志不能够绑定实现任何功能,值得注意的是,slf4j-simple是Slf4j官网提供的简单日志实现,我们需要引入依赖,其会自动绑定到Slf4j门面上,如果不引入依赖,Slf4j核心依赖是不提供任何实现的。
2.Logback和simple(包括nop),都是Slf4j门面之后出现的日志实现,所以API完全遵循Slf4j进行设计,因此我们只需要引入要使用的日志实现依赖,即可完成与Slf4j的无缝衔接,值得注意的是,nop虽然划分到实现中,但是它是指不实现日志记录(具体情况,下面会做出讲解)
3. Log4j和JUL,都是Slf4j门面之前出现的日志实现,所有API不遵循Slf4j进行设计,因此需要通过适配桥接的技术,完成与日志门面的衔接
接下来,我们试着在原有依赖的基础上,引入Logback日志实现的依赖,并且我们将Logback依赖添加到simple依赖的下方,执行日志输出。
<!-- logback依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
从测试结果我们了解到,Slf4j发现了两个日志框架实现,并且日志输出使用的simple日志实现,我们继续将两个日志实现的依赖调换位置,继续测试
我们根据结果发现,Slf4j使用的日志实现变为了Logback,因此得出结论,如果有多个日志实现的话,默认使用先导入的日志实现,值得一提的是,当我们将simple依赖注释,只留下Logback日志实现时,Slf4j使用的就是Logback日志实现,并且没有了多余的提示信息,所以在实际应用时,我们一般情况下,仅仅只做一种日志实现的集成就可以了。
通过上述集成测试,我们发现虽然底层的日志实现变了,但是源代码完全没有改变,这就是日志门面给我们带来的最大的好处,在底层真实记录日志的时候,我们不需要应用去做任何的了解,应用只需要去记Slf4j的API就可以了。
接下来我们来讲解一下slf4j-nop,首先我们需要引入nop的实现依赖,根据上面进行的Slf4j关系图及上述集成测试,我们了解到slf4j-nop与Logback属于一类情况,只需依赖便可以与Slf4j无缝衔接,并且要想是nop发挥作用,由集成依赖的顺序而定,所以必须将slf4j-nop依赖放在所有日志实现的上方,进行日志的输出测试。
<!-- slf4j-nop依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.25</version>
</dependency>
根据测试结果我们发现Slf4j使用的日志实现时slf4j-nop,并且不实现日志的记录
接下来我们来绑定Log4j日志实现,由上面内容的讲解我们也了解到,Log4j是在Slf4j之前出品的日志框架实现,所以没有遵循Slf4j的API规范,因此如果我们要想将Log4j与Slf4j进行衔接,就需要绑定一个适配器slf4j-log4j12。
<!-- log4j适配器依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<!-- log4j依赖 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
值得注意的是,上面我们了解到Slf4j与多个日志实现衔接是由日志实现的引入顺序决定的,因此如果我们将Logback依赖放置到Log4j依赖与Log4j适配器之前时,Slf4j使用的是Logback的日志实现,如下图
上面我们既然引入绑定Log4j的日志实现,我们就需要在resources下创建Log4j的配置文件来控制日志的输出形式,我们简单配置一个Log4j的控制台输出。
接下来我们进行JUL日志实现的绑定,因为JUL是JDK内置的日志实现,并且JUL与Log4j属于一类情况,需要添加适配器才可以与Slf4j进行衔接,我们只需要引入slf4j-jdk14依赖即可完成JUL日志实现的绑定。
<!-- JUL适配器依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.25</version>
</dependency>
由上面的测试结果,我们发现,绑定多个日志实现时,会出现警告信息,接下来,我们通过源码来查看其原理(看看Slf4j的执行原理)
首先从LoggerFactory的getLogger()方法入手,进入该方法,我们发现
进入重载的方法,我们可以看到下图方法,该方法用来取得logger工厂实现的方法。
进入该方法,我们可以看到以双重检查锁的方法做判断,执行performInitialization(); 工厂的初始化方法,进入该方法,看到bind(); 就是用来绑定日志具体实现的方法。
进入bind()方法,因为当前有可能会出现N多个日志框架实现,看到下图代码
进入到findPossibleStaticLoggerBinderPathSet()方法,看到创建了一个有序不可重复的集合对象,声明了枚举类的路径,经过if else 判断,以获取系统中都有哪些日志实现。
我们主要观察常量STATIC_LOGGER_BINDER_PATH,通过常量我们会找到类StaticLoggerBinder,这个类是以静态的方式绑定Logger实现的类。
我们进入slf4j-JDK14适配器的StaticLoggerBinder,如下图,说明slf4j-jdk14使用的是JUL的Logger
继续回到findPossibleStaticLoggerBinderPathSet()方法进行观察。
while循环如果还有其他的日志实现,便将路径添加到集合中,回到bind()方法,我们发现reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);该方法表示对于绑定多实现的处理。
在该方法中我们就可以看到测试时控制台输出的提示信息打印操作,如下图
5.4需求分析
假设目前有这样一个需求,我们项目一直以来使用的是Log4j日志框架,但是随着技术和需求的更新换代,Log4j已然不能够满足我们的系统需求,我们现在需要将系统中的日志实现重构为Slf4j+Logback的组合实现,在不触碰源代码的情况下,如何将这个问题解决掉。
解决方案:
首先我们将所有的日志框架依赖注释掉,只留下Log4j依赖,并使用Log4j的Logger进行日志输出。
既然我们要使用Slf4j+Loback替代掉Log4j,所以我们需要在pom文件中引入Slf4j和Logback依赖,将Log4j日志依赖注释掉。
添加好后我们会发现没有了Log4j环境的支持,编译报错,这个时候,我们需要使用桥接器来解决这个问题,桥接器解决的是项目中日志重构的问题,当前系统中存在之前的日志API,可以通过桥接器转换到slf4j的实现。
添加桥接器依赖,桥接器加入之后,代码编译就不报错了。
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.25</version>
</dependency>
根据测试结果,我们发现日志的输出格式为Logback,证明了现在使用的是Slf4j+Logback实现。
在重构之后,就会为我们造成一种假象,使用的命名是Log4j包下的日志组件资源,但是真正日志的实现,却是使用了Logback的日志,这就是桥接器给我们带来的效果,有一点需要注意,在桥接器加入之后,适配器就没有必要加入了,桥接器和适配器不能同时导入依赖,桥接器配置在适配器上方,则运行报错,不能同时出现,桥接器配置在适配器下方,则不会执行桥接器,没有任何意义。
接下来我们通过对其底层原理的分析来证明配置桥接器后,底层就是使用Slf4j实现的日志,我们通过Logger的getLogger()方法来分析,进入方法,进入Log4jLoggerFactory,我们可以看到Logger newInstance = new Logger(name); 新建logger对象,进入构造方法并进入父类方法。
在父类的Category构造方法中我们可以看到slf4jLogger = LoggerFactory.getLogger(name);,而这里使用的LoggerFactory来自于org.slf4j。
5.5总结
1.在真实的生产环境当中,Slf4j只绑定一个日志实现框架就可以了。
2.在Slf4j绑定多个日志框架时,默认使用绑定依赖引入的第一个日志实现,并且会产生多个日志实现的警告提示信息。
3.Slf4j与多个日志实现的绑定情况有三种,一种是没有任何日志实现引入的情况,Slf4j不具备实现日志记录的功能,一种是对于Logback、Simple、nop均符合Slf4j基础上进行的设计,因此直接引入依赖便可与Slf4j进行绑定,并且值得注意的是,nop并没有实现日志记录的功能,第三种情况则是通过适配器进行Slf4j与日志实现的衔接,例如Log4j、JUL,在引入日志实现依赖的同时,还需要引入其相应的适配器依赖,才可以与Slf4j进行绑定使用。