Java日志体系日志门面(Slf4j)日志实现(Log4j、Log4j2)详解

1、背景

近日发生两次因日志使用不当导致的线上问题:
1、应用明明配置了info日志级别,却打印大量的debug日志,导致磁盘IO较高,很快就报磁盘空间不足告警

2、应用服务启动后报StackOverflowError错误,无法正常提供服务

这两个问题的本质其实都是Java的日志体系较为庞杂、混乱且使用方式多变导致,如果能对Java的日志体系有一个全面的了解,那这两个问题也就比较容易解决了。

下面我带着大家理清庞杂的Java日志体系,本文意在帮大家梳理Java各日志框架的作用,以及常见日志框架的组合使用,不会讲解各日志实现的具体配置,如想了解各日志实现的配置请前往各日志实现官网查阅。

2、Java日志框架介绍

使用过Java的同学应该都听说过Log4j、Slf4j、Log4j2、Logback、commons-logging等日志框架,了解的多一些的同学应该还会听说过日志门面、日志实现等概念,有的同学可能就有疑问了,为什么Java的日志框架这么多?日志门面、日志实现又是什么?他们之间又有什么关系呢?下面我通过这几个问题来给大家介绍一个Java庞杂的日志体系。

1、日志门面、日志实现是什么?

计算机程序设计中有一句至理名言:没有什么是抽象一层不能解决的,如果有那就再抽象一层。

日志门面就是这样的一个存在,由于日志实现太多了,所以导致应用依赖于具体的日志实现,而很多的第三方包引用的日志实现不同,最终导致应用的引用日志实现混乱不堪,甚至影响整个应用的正常使用,这时候JCL和Slf4j就站出来了,他们的原理是抽象一层高层API,应用无需具体的日志实现,应用只需要依赖API,具体的实现使用SPI的方式实现。

日志实现就是具体的日志功能的实现了,勤勤恳恳的、一丝不苟的打印应用中的各种日志的实现。

2、Java日志体系都包含哪些日志框架?

上文我们将日志框架的作用分为了两类,分别是日志实现和日志门面,下面我们将这些常见的日志框架对号入座,如下图所示:
在这里插入图片描述

  • 1996年早期,欧洲安全电子市场项目组决定编写它自己的程序跟踪API(Tracing API)。经过不断的完善,这个API终于成为一个十分受欢迎的Java日志软件包,即Log4j(由Ceki创建)。
  • 后来Log4j成为Apache基金会项目中的一员,Ceki也加入Apache组织。后来Log4j近乎成了Java社区的日志标准。据说Apache基金会还曾经建议Sun引入Log4j到Java的标准库中,但Sun拒绝了。
  • 2002年Java1.4发布,Sun推出了自己的日志库JUL(Java Util Logging),其实现基本模仿了Log4j的实现。在JUL出来以前,Log4j就已经成为一项成熟的技术,使得Log4j在选择上占据了一定的优势。
  • 接着,Apache推出了Jakarta Commons Logging,JCL只是定义了一套日志接口(其内部也提供一个Simple Log的简单实现),支持运行时动态加载日志组件的实现,也就是说,在你应用代码里,只需调用Commons Logging的接口,底层实现可以是Log4j,也可以是Java Util Logging。
  • 后来(2006年),Ceki不适应Apache的工作方式,离开了Apache。然后先后创建了Slf4j(日志门面接口,类似于Commons Logging)和Logback(Slf4j的实现)两个项目,并回瑞典创建了QOS公司,QOS官网上是这样描述Logback的:The Generic,Reliable Fast&Flexible Logging Framework(一个通用,可靠,快速且灵活的日志框架)。
  • Java日志领域被划分为两大阵营:Commons Logging阵营和Slf4j阵营。
  • Commons Logging在Apache大树的笼罩下,有很大的用户基数。但有证据表明,形式正在发生变化。2013年底有人分析了GitHub上30000个项目,统计出了最流行的100个Libraries,可以看出Slf4j的发展趋势更好。
  • Apache眼看有被Logback反超的势头,于2012-07重写了Log4j,成立了新的项目Log4j2, Log4j2具有Logback的所有特性。
  • 如今日志框架已经发展为:Slf4j作为API,实现分为Logback与Log4j、Log4j2(Commons Logging因为效率和API设计等问题,现在逐渐淡出舞台了)

Slf4j就是在这样混乱的情况下诞生的,本想可以在这日志框架群雄割据的年代成为日志标准一统江湖、评定天下,无奈各大枭雄根本不鸟他,继续群雄割据自己玩自己的,无奈Slf4j只好“曲线救国”,以一种委婉的方式不断提高市场份额,最终成为了事实上的日志标准,下面我们就来看看Slf4j是如何“曲线救国”的。

3、各框架之间有什么关系?我们该怎么用?

废话不多说,我们先上图:
在这里插入图片描述

上图是各框架之间的关系也是各框架组合使用的方式(由上至下一共分为6层),所有的使用方式都由两部分组成,一部分是日志绑定包(上图第5层),负责将具体的日志实现绑定到Slf4j上,上文提到过Slf4j想成为日志标准,奈何其他日志框架不认可他,不去实现他提供出来的API,Slf4j无奈只能自己去实现,所以除了自己的日志实现Logback外使用其他的日志框架的时候都需要使用一层日志绑定包来绑定具体的日志实现。

另一部分是日志桥接包,负责将应用或第三方包中直接使用的日志实现桥接到Slf4j上,再由Slf4j具体绑定的日志实现统一输出,这样就解决了遗留项目中直接使用了具体的日志实现但是还想要接入Slf4j进行统一的日志管理,也解决了如业务应用使用Log4j2某些第三方包使用Log4j或jul等不同日志框架的日志输出问题,可以将其他日志实现桥接到Slf4j上,然后由统一的日志实现完成日志输出。

Slf4j就是这样通过绑定包、桥接包的方式“曲线救国”实现了大一统,最终市场份额不断扩大,成为了事实标准,Slf4j本来是没有为Log4j2提供绑定包、桥接包的,但是Log4j2也不得不被趋势所迫,自己提供了Slf4j的绑定包(log4j-slf4j-impl-xxx.jar)和桥接包(log4j-to-slf4j-xxx.jar),整个日志体系就形成了上图所示的关系图。

关于怎么用,我用三个例子给大家讲解一下:

①新业务应用,想要使用Slf4j当做日志API,使用Log4j2作为日志实现

这种情况比较简单,我们只需要引入Slf4j的API包(slf4j-api-xxx.jar)、Log4j2的绑定包(log4j-slf4j-impl-xxx.jar)和Log4j2的日志实现包(log4j-core-xxx.jar)即可,比较简单就不在赘述了。

②遗留业务应用,遗留代码使用Log4j作为日志框架或其他第三方包使用Log4j,现在想要统一日志框架接入Slf4j作为日志API,使用Log4j2作为日志实现

这种情况相较于前一种情况需要多引入一个Log4j的桥接包(log4j-over-slf4j-xxx.jar)来把直接使用Log4j的遗留代码桥接到Slf4j上,然后再引入Slf4j的API包(slf4j-api-xxx.jar)、Log4j2的绑定包(log4j-slf4j-impl-xxx.jar)和Log4j2的日志实现包(log4j-core-xxx.jar),注意这里因为Log4j的桥接包的实现特性,所以需要将原来引入的Log4j的日志实现包(log4j-xxx.jar)在pom文件中删除,如果有其他第三方包也使用到了需要将其exclude掉即可

③遗留业务应用,遗留代码使用Log4j作为日志框架,某些第三方包使用Log4j2,现在想要统一日志框架接入Slf4j作为日志API,使用Log4j2作为日志实现

这种情况和第二种情况一样,需要引入Log4j的桥接包(log4j-over-slf4j-xxx.jar)来把直接使用Log4j的遗留代码桥接到Slf4j上,然后再引入Slf4j的API包(slf4j-api-xxx.jar)、Log4j2的绑定包(log4j-slf4j-impl-xxx.jar)和Log4j2的日志实现包(log4j-core-xxx.jar),然后排除Log4j的日志实现包(log4j-xxx.jar),切记不可将Log4j2的桥接包(log4j-to-slf4j-2.xx.jar)引入,如果引入会造成循环调用最终导致栈溢出,具体细节我们在第四部分问题分析时在详细讨论。

其他桥接包的使用因为实现原理不一样,所以在使用的时候也会有所不同,大家在遇到具体问题的时候可以参考Slf4j官网的文档查阅:http://www.slf4j.org/legacy.html

3、日志框架原理及问题排查

通过上面的几个问题我们应该已经对Java日志体系有一定的了解了,Slf4j通过桥接包、绑定包“曲线救国”实现了大一统成为了事实标准,所以说到Slf4j就不得不说他的两个核心:桥接、绑定,为什么Slf4j可以做到不更改代码实现日志桥接的呢?Slf4j又是怎么做到绑定不同日志实现的呢?还有的同学可能会有疑问,桥接包这么多,如果有有错误我们怎么排查呢?下面我通过这两个问题来给大家介绍一个日志桥接的实现原理和问题排查思路。

1、为什么Slf4j可以做到不更改代码实现日志桥接的呢?

首先我们来看一下Slf4j官网给出的图:
在这里插入图片描述

从上图可以看出,Slf4j的桥接能力是通过log4j-over-slf4j.jar、jcl-over-slf4j.jar、jul-to-slf4j.jar等jar包实现的,由于jcl和jul基本已经退出日志舞台了,在此我们不在赘述,如果感兴趣可以去Slf4j官网查阅。

下面我们重点介绍一个Log4j和Log4j2的的桥接包实现方式:

①Log4j的桥接包(log4j-over-slf4j-xxx.jar)的实现方式

Log4j的桥接包是有Slf4j实现的,Log4j官方是不承认的,这就导致Log4j的代码是没有给Slf4j开这个实现特权的,所以Slf4j就采用了一种比较粗暴的方式:使用桥接包中的类替换Log4j中的核心类,我们看下图桥接包中的类与日志实现包中的类的包名和类名是完全一致的:
在这里插入图片描述
在这里插入图片描述

这样具体的类实现就由Slf4j说了算了,所以上文中介绍Log4j桥接包的使用方式的时候特别说明需要将Log4j原有的依赖全部排除,因为不排除就会出现重复的类,这样JVM加载的时候就不一定会加载到桥接包中的类,桥接包就有可能失效了,其实还有另一种解决方案,依赖于JVM的类加载机制,这个方案我们在下面问题分析的时候会详细介绍。

②Log4j2的桥接包(log4j-to-slf4j-2.xx.jar)的实现方式

Log4j2的桥接包是由Log4j2官方实现的,所以是官方代码层面进行支持的,使用的方式也相对柔和一下,采用了Java的SPI机制进行的实现,如下图所示,对SPI具体的实现方式大家有兴趣可以深入了解一下,这里不再展开说明了。
在这里插入图片描述

2、Slf4j是怎么做到绑定不同日志实现的呢?

首先我们还是来看一下Slf4j官网给出的图:
在这里插入图片描述

绑定包的实现就比较统一了,因为是Slf4j代码层面进行支持的,这也是Slf4j的设计的一部分,所以实现方式也比较柔和,使用了类似SPI的机制,下面我以Log4j2的绑定包()为例简单介绍一下。

首先我们使用Slf4j的时候所有的Logger的获取都是从LoggerFactory.getLogger()开始的,所以我们也从这个方法开始,如下图所示:
在这里插入图片描述

这里我们主要关注getILoggerFactory()方法,可以看到首先是通过getILoggerFactory()方法获取ILoggerFactory的,然后通过ILoggerFactory的getLogger()方法获取最终的Logger实现,现在我们看一下getILoggerFactory()方法:
在这里插入图片描述

这里我们主要关注红线圈出来的部分,这里我们就确定了,是StaticLoggerBinder来包装的LoggerFactory,那是不是所有的绑定包都提供一个这个同包名、类名的类就可以实现不同的日志实现的绑定了,下面我们来验证一下:
在这里插入图片描述

果然我们在Log4j2的绑定包里发现了这个同包名、类名的类StaticLoggerBinder,也发现了具体的LoggerFactory和Logger实现(红框圈出来的部分),至此我们明白了日志绑定是如何实现的了,具体的细节大家如果有兴趣可以自己翻阅源码深入了解。

我们可以看到日志绑定的实现也挺粗暴的,我也很好奇为什么作者不使用Java的SPI机制来实现,大家有兴趣也可以自己深入了解一下。

这里其实有个小问题可以探讨一下,如果我们的应用添加了多个日志绑定包Slf4j会选择哪一个呢?Slf4j官网给出的解答是会随机选取一个,其实这里并不是一定随机的,作者这里的随机指的是依赖于JVM的版本,不同的版本可能绑定的日志实现是有差别的,比如我们现在用的JDK1.8版本在JVM加载类的时候如果遇见了相同包名、类名的类只会加载第一个(由上面的介绍我们知道日志绑定包的实现是由一个同包名、类名的类StaticLoggerBinder实现的),后面扫描到的类不会加载,所以在这个版本是第一个加载的类生效,但是这个规则在后续的版本中也可能会有变化,所以在不同版本中可能是随机的。还有我们使用的启动方式比如SpringBoot项目的启动和普通Spring项目的启动绑定的日志实现也是可能有差异的,SpringBoot使用FatJar技术将所有的第三方都封装到了一个Jar包下,同时保证了pom.xml中的依赖引入顺序,但是普通的Spring项目使用的是Java -cp的方式加载某个目录下的第三方包,这时候pom.xml中的顺序可能就被打乱了,这个顺序依赖于操作系统的文件系统排序,所以这两种启动方式绑定的日志实现也是有差异的。但是当我们的应用工程使用固定的JDK版本、固定的启动方式的情况下绑定的日志实现也是固定的,只有修改启动方式、启动参数、JDK版本的时候才可能会发生变化。

3、出现日志问题我们应该怎么排查?

常见的日志问题分为两类,一类是日志没有按照我们的日志配置文件的配置进行打印,这种问题的原因通常是应用中有直接使用日志实现导致的,另一类的因为引入了同一个日志的桥接包与绑定包,导致打印日志时进入了无限循环调用,最终导致抛出StackOverflowError。

排查思路基本是一致的,首先我们需要分析一下当前项目中的日志依赖,我们用maven的命令(用三个命令分析全部的日志包)分析一下:

mvn dependency:tree -Dincludes=':*log*:'
mvn dependency:tree -Dincludes=':*jul*:'
mvn dependency:tree -Dincludes=':*jcl*:'

在这里插入图片描述

分析结果大致如上图所示,然后我们就可以根据第三部分介绍的搭配使用规则来排查一下具体的问题了,除此之外我们还可以借助一个工具Arthas来分析一下我们的日志类到底是从哪个Jar包加载的、StaticLoggerBinder到底是从哪个包加载的,具体使用方法大家可以去Arthas官网(https://alibaba.github.io/arthas/)查看,结果如下图所示:
在这里插入图片描述

到此基本就可以完全确定问题产生的原因了,我们只需要做相应的更改即可。

4、线上问题分析

当大家读到了这里可以说对Java的日志体系已经了解的比较透彻了,对当前的各日志框架之间的关系,如何将他们组合使用,桥接的实现原理,以及日志问题的排查有了一定的认识,现在我们再反过来看我们线上服务遇见的两个问题,看看这两个问题究竟是因为什么引起的,又是如何解决的。

1、应用明明配置了info日志级别,却打印大量的debug日志,导致磁盘IO较高,很快就报磁盘空间不足告警

通过分析日志依赖确定了是因为项目中有第三方包使用的Log4j作为日志框架,依赖分析如下图所示:
在这里插入图片描述

在上图我们发现虽然有Log4j的实现,但是没有Log4j的桥接包,所以问题基本可以确定是因为缺少桥接包所致了,所以我们引入桥接包,排除Log4j的依赖即可。

其实这里还有另一种方案,我在上文的时候也说过,在这里我简单介绍一下,因为我们在项目的整个生命周期里会不断地引入新的第三方包和升级第三方包,所以我们可能这次排除Log4j依赖排除干净了,但是不能保证下次还会引入这个问题,所以这里有个一劳永逸的办法,那就是我们让桥接包中的类优先被JVM加载即可,如果是StringBoot项目,我们只需要把pom.xml中的桥接包依赖移动到第一个即可,普通Spring项目的话就比较麻烦了,我们需要改动一下启动参数,改动后如下图所示:
在这里插入图片描述

上图的cpath变量最终会被java -cp $cpath使用,这样的话就可以保证JVM优先加载桥接包中的类了,这个问题就解决了。

2、应用服务启动后报StackOverflowError错误,无法正常提供服务

服务在启动的时候会抛出StackOverflowError错误,报错如下图所示:
在这里插入图片描述

从上图的报错栈中我们可以看到是发生了循环调用导致,仔细看包名和类名会发现是因为Slf4j和Log4j之间发生了循环调用,所以我们只需要使用mvn来分析一下依赖,把Log4j的依赖一一排除就可以了,也可以使用我们上面说的另一种方案让桥接包被JVM第一个加载。

5、参考资料

Slf4j官网:http://www.slf4j.org/
Log4j2官网:http://logging.apache.org/log4j/2.x/
Logback官网:http://logback.qos.ch/
JCL官网:http://commons.apache.org/proper/commons-logging/
Arthas官网:https://alibaba.github.io/arthas/

  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值