0x00 前言
作为后端开发,日志可能是我们最常用的功能之一了。平时大家也可能经常遇见日志冲突,常见的overflow报错,今天为大家详解一下,这其中的原理以及问题所在。本文涉及 jar 包有:log4j,log4j-over-slf4j,slf4j-api,slf4j-log4j12 等等。
0x01 背景
不知道大家在平时开发中,是否经常遇见以下几个问题:
- log4j,logback等等日志包冲突,然后再慢慢排除,不胜其烦。
-
使用了 slf4j ,但是又遇到 StackOverflwError 错误。
-
好不容易解决了StackOverflwError,可能又遇到了类似如图所示的错误:
- ......
0x02 概述
什么是 SLF4J 呢?简而言之,他就是一个日志的门面,市场上的日志系统非常多, SLF4J 想做的一件事情,就是将这多种的日志系统包装起来,提供统一的 API 供调用,从而解决日志的列国争霸的情况。
0x03 SLF4J 调用流程
在解决以上问题之前,让我们先对日志系统做一个大概的理解。到底什么是 log4j,什么是 slf4j-api,为什么有一个 slf4j-log4j12,又怎么有个log4j-over-slf4j,看上去头晕眼花,绕来绕去。先祭上一张官网图:相信大家都见过这张图,但是未必全都理解图上说的是什么意思。所以呢,我们先不讲图,先看看最上面的一排文字,SLF4J bound to xxxx,xxxx总共涉及:
- null
- logback-classic
- log4j
- java.util.logging
- simple
- no-operation
第1个顾名思义,不绑定,就是没有日志实现。第5,6可以看出来,是 SLF4J 自己的实现,这里就忽略不讲。所以着重讲一下2、3、4。他们有个共同的特点,就是他们都是日志的真正实现库,他们和SLF4J没有关系,你可以使用 logback 打印日志,也可以使用 log4j 打日志。
第一层, application 就是应用层。
看看图中第二层,统统都是 SLF4J API。那么这一层是干什么用的呢?这一层就是一个门面层,而和门面层息息相关的 jar 包是什么呢?对,就是图中的 slf4j-api.jar 。这个jar包中,提供了日志调用的所有接口,应用都是直接调用这个 jar 包中的接口进行日志调用。
第三层有点不同。第一列,无实现,第2,5,6都是原生的一个实现,而3,4都有一个适配层。这里说说 logback。logback 他为什么不需要适配层呢,因为他就是按照了 SLF4J 接口去实现的一个日志库,相当于亲儿子,自然不需要适配层。所以,logback-classic.jar 和 logback-core.jar 就是 logback 的底层实现。而3,4就不同了,他们的接口或多或少会有差异,调用方式也各不相同,所以,需要一个适配层。所以 slf4j-log412.jar 和 slf4j-jdk14.jar 包的作用就是一个适配,这里是一个桥接,应用通过 slf4j-api 的接口调用过来时,桥接类实际会调用其底层的实现,达到一个桥接的过程。所以,slf4j-log412.jar,slf4j-jdk14.jar,slf4j-simple.jar,slf4j-nop.jar 是同一类,就是把-
右边的实现进行一个桥接。
第四层,可以看到第2,3对应的实现,log4j.jar 和 jvm 就是最后的实现。
至此,做一个小总结:
- slf4j-api.jar 是上层的门面,里面提供接口供调用。
- logback-classic.jar, logback-core.jar, log4j.jar, 是同一类别,属于底层实现库。
- slf4j-log412.jar,slf4j-jdk14.jar,slf4j-simple.jar,slf4j-nop.jar,可以看成 slf4j-xxxx.jar,属于同一个类别,就是对 - 后面的库做一个桥接,更简单的理解,从左到右读:把 - 左边的调用用右边的库实现。
此时再看看上面那张图,是否已经全部理解了呢?
0x04 SLF4J 转换流程
如果看完上面,不觉得 SLF4J 有什么好处,就来看看下面这张图:那这张图又是什么意思呢?这就要说到 SLF4J 的一个强大之处了。设想一下以下一种情况:新建了一个工程,引入的第一个库使用的是 java.util.logging 日志库,引入的第二个库使用的是 log4j 日志库,而你自己的工程,老板规定,必须要用 logback 。你怎么办呢?这个时候, SLF4J 就出场了。他能帮你把所有日志归拢到你所指定的一种日志实现。就是说,他可以把 jul 日志实现转成 logback,还能把 log4j 实现转成 logback。那他是怎么做的呢?回过头来看图吧。
着重讲解左上角这一部分,其他的类似。
先看 application,这个是应用,可以看到,他也遇到了我说到的问题。他的依赖里面有使用 log4j 的,有用 commons logging 的,有用 java.util.logging 的。所以此时需要做一个替换,分别是通过 jcl-over-slf4j.jar 替换掉 commons-logging.jar,log4j-over-slf4j.jar 替换掉 log4j.jar, jul-to-slf4j.jar 包中安装 SLF4JBrindgeHandler 解决。替换掉之后,就把所有日志调用转接到 slf4j-api 上了,然后 api 接口再调用底层实现,图上是 logback。文中说的替换是什么意思呢?就是把原日志实现库排除掉,引入 xxx-over-slf4j.jar 。
那么,xxx-over-slf4j.jar 是什么原理呢?先给大家看这张图:
左边是 log4j.jar 的包结构,右边是 log4j-over-slf4j.jar 的包结构。发现猫腻了吗?他们的目录结构一模一样!所以用 log4j-over-slf4j 可以替换掉 log4j!且编译不会出错。log4j-over-slf4j.jar 实现了基本上所有 log4j 会被调用的 api 接口。所以替换之后,不会报错,编译也能通过,而底层实现却全转到 slf4j 这里去了。这里就是一个狸猫换太子的把戏。
再看其他两个图,底层分别是 log4j, jvm 实现。都是讲其他库 over 一下到 slf4j 。
特别注意一下, logback 不需要转,为什么?因为他是亲儿子。天生就带这些。
所以再小总结一下:
- jcl-over-slf4j.jar, log4j-over-slf4j.jar, jul-to-slf4j.jar,这种形式类似 xxx-over-slf4j.jar 的,就是将
-
前的太子用 slf4j 的狸猫代替。而 xxx-to-slf4j.jar 比较特殊,这是因为 xxx 这个包无法被替换掉,比如 java.util.logging,系统的库,无法替换,所以只能采用别的手段。此类别的实现读者有兴趣可以去看看,本文不再分析。
0x05 常见问题解决
StackOverflow 错误
为什么会出现这个问题呢?控制台输出上一般会比较清楚,就是你既使用了桥接库,又使用了over库(狸猫)。比如:试想一下:你先用 over 库把 log4j 转成了 slfj4 调用。紧接着,你又把 slf4j 适配到 log4j 上。这就构成了一个死循环,肯定是会出现堆栈溢出的问题。所以,一个工程里面,只能保留一个日志实现库,还有配套的桥接库,加上其他日志的 over 库,才是正确之道。
异常参数错误
上面提到的这个错误:
大致可以看出来吧,他缺少一个真正的实现。看你选择使用什么,就把什么库补充上去。加上 logback,去掉 slf4j-log4j 或者加上 log4j 都可以解决这个问题。
其他问题都比较类似,如果看懂了上文的介绍,应该可以着手解决此类问题了。
0x06 不算总结的总结
了解了日志的原理,以后妈妈再也不担心日志冲突了!