Java中有好多日志框架, 互相之前还有各种关系, 总有同学配不好日志, 之前就分享过, 重新整理一下.

小朋友你是否有很多问号?

你是否经常看到这种警告而感到莫名其妙:

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/Users/tzp/.m2/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.8.2/log4j-slf4j-impl-2.8.2.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/Users/tzp/.m2/repository/org/slf4j/slf4j-log4j12/1.7.25/slf4j-log4j12-1.7.25.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]

你是否知道log4j / slf4j / jul / jcl / logback / log4j2这些都是干嘛的?

为什么你的日志配置不生效, 为什么程序的日志不输出? 是道德的沦丧, 还是人性的扭曲?

Java日志框架历史

  • log4j 最早的java日志框架, 主要由Ceki Gülcü开发, 后加入Apache基金会, 在Apache页面看到的初始提交在1999-10-15.
  • jul 即java.util.logging, 在Java 1.4(2002年)才在jdk中出现自己的日志功能.
  • jcl 即common-logging, 大概也是2002年吧, 由Apache推出, Ceki也参与了. 可选的日志框架很多, 功能不同, 这个jcl就出现了, 作为一个接口适配器bridge between different logging implementations.
  • slf4j, Ceki大神离开Apache, 自己重新做了一个日志接口框架, 大概是2006年. 是最被广泛使用的java依赖之一.
  • logback, Ceki重新做的一个日志运行框架, 作为slf4j的默认实现.
  • log4j2, Apache重新搞出来的新版, 和log4j 1.x版本差别巨大, 2012年发布, 据说性能很高.

日志框架分类

所以, 我把上面提到的关于日志框架分为两类(都是我瞎起的名).

  1. 日志接口框架. 是定义了日志的接口, 本身并不实现打印日志的具体功能. 接口能桥接不同的日志实现. (slf中的f即Facade, 门面).
    • jcl
    • slf4j
    • log4j2
  2. 日志运行框架(日志实现框架). 是真实干活的框架, 真正执行日志功能. 有的直接实现了某种接口, 有的需要一些方法桥接.
    • log4j
    • jul
    • logback
    • log4j2
    • slf4j-simple

(这里log4j2有些特殊, 充当两种角色, 下面会提到)

深入SLF4J & 解决问题

问题发生在哪里

如果我们从头写一个项目, 不使用太多三方依赖, 那么对于日志框架的使用也不会出现什么问题, 无非就是选定一个框架:

  1. 添加这个日志框架的maven依赖.
  2. 配置好这个日志框架的配置文件.
  3. 在代码中使用这个日志框架的类来打印日志.

这样日志就会乖乖按你想要的方式打印到你想要的地方.

但是!! 正常的项目都会依赖很多开源的三方项目, 比如你一个web项目, 依赖了Spring, Mybatis. 那问题就来了: Spring/Mybatis使用了什么日志框架? 和你选择的日志框架是否一致? 冲突了怎么办? 开篇提到的各种问题, 都是这样产生的!

maven依赖多起来之后, 一层又一层的让人头大, 而且我作为同一个项目, 如果多套日志框架同时使用也太过精神分裂了吧??

初阶解法

作为接受良好OOP思想教育的coder, 我们都知道要面向接口而不是实现编程. 如果大家(Spring/Mybatis/我的项目)都是面向统一的日志接口来编码的, 那么我打包自己的项目的时候, 随心所欲的选择一种这个日志接口的实现就可以了.

这样最终jvm运行的classpath下, 有这个接口的api, 有一份接口的实现, 弄一份日志配置文件, Spring/Mybatis/我的项目这三者都乖乖的打印日志, 这多好啊!

现实中这种情况也有, 就是最广为使用的slf4j-api.jar就是这样一个接口. 所以最简情况下, 你的项目和你项目的依赖, 都使用slf4j-api, 然后引入logback作为实现框架, 整体就需要这三样东西就ok了:

  • org.slf4j:slf4j-api:jar:1.7.30
  • ch.qos.logback:logback-classic:jar:1.2.3
  • logback.xml

OOP大法真好啊. 这也是日志接口框架的第一个秒用.

再好的接口, 没人实现怎么办?

但是设计的再好的一个接口包, 如果没有人实现, 大家(比如Log4j)都玩自己的日志框架怎么办呢? 现实也是如此, 除了logback和slf4j-simple原生实现了SLF4J的接口, 其他日志实现框架都没有按照SLF4J的来!

这时候SLF4J拿出一招, 名曰binding或adaption(适配器). 实际上就是他们自己, 为每个没实现SLF4J接口的日志框架做了一个中间层: 应用代码是调用SLF4J接口打印日志的, SLF4J的接口是由中间bind层来实现的, bind层再把实际工作转发到具体的日志框架上.

如上图中的浅蓝色的adaption layer就是干这个的, 比如slf4j-log4j12.jar, 向上实现了slf4j-api.jar的接口, 向下调用了log4j.jar中的真正干活的类.

开篇提到的”multiple_bindings”的警告信息, 也是由于SLF4J加载的时候, 在classpath下面发现了多个自己的实现, 所以随机选择了一个binding, 并给出警告.

Embedded components such as libraries or frameworks should not declare a dependency on any SLF4J binding but only depend on slf4j-api. When a library declares a compile-time dependency on a SLF4J binding, it imposes that binding on the end-user, thus negating SLF4J’s purpose.

SLF4J的偷天换日之术

通过上面两节关于SLF4J的接口设计和适配器设计, 我们已经get到SLF4J的2/3的好处了. 这时另一个糟糕情况出现了, 你的项目和你项目的依赖并不都是基于slf4j-api编码的, 他们有的使用slf4j-api, 有的使用log4j!

这时候只用SLF4J+logback就不行了. 你的classpath下必须有log4j的jar包, 否则项目运行起来就会报错:NoClassDefFoundError: com.某.个.log4j.的.类! 如果使用log4j的jar的话, 日志又不受slf4j/logback的控制了, 你的logback.xml控制不了使用log4j的代码的日志, 就得在同一个项目中弄两套日志了.

为了解决这个问题, SLF4J祭出名曰桥接bridging的方法. 实际上就是在运行时classpath中, 将原本的Log4j的jar包移除, 然后加入一个和Log4j的类名,接口签名长的一样的”假”Log4j包log4j-over-slf4j.jar, 这样执行的时候, 代码以为自己在调用Log4j的类在打印日志, 殊不知此时正在jvm的类根本不是Log4j提供的, 而是SLF4J自己”伪造”的Log4j类, 只要类名/方法签名一样, jvm可不管你是否偷天换日了.

如图左上角的部分就是这种模式的示例, 应用同时包含对SLF4J的API, jcl的API, log4j的API, jul的API的调用, 但实际上通过几个桥接的jar包, 都转到SLF4J的API上了, SLF4J的API再由具体的logback实现. 这样几个jar包配合, 再来一个logback.xml就完事了.

图右上角的部分, 是同时使用桥接方法和上一小节的binding方法的示例.

SLF4J总结

综上, SLF4J的三板斧凑齐了:

  • 定义统一接口Facade. (相应jar包: slf4j-api.jar)
  • 向下(对于实际干活的日志实现框架), 通过binding, 不管你框架是否实现SLF4j, 我都能调用你干活. (相应jar包: slf4j-xxx.jar)
  • 向上(对于应用代码), 通过桥接, 把基于其他框架打印日志的代码, 都能桥接到自己这. (相应jar包: xxx-over-slf4j.jar, xxx-to-slf4j.jar)

SLF4提供的jar包也是分这三种, 而且也正对应三种设计模式: 门面, 桥接, 代理!

编译时和运行时?

SLF4J后两斧之所以能够实现, 是由于Java的编译时和运行时classpath的机制.

  • A类依赖B类, 编译时有B类, 但是A类的class文件和A类项目打成的jar包都不包含B类的代码. (用jar打包的时候是不包含依赖的, 用shade这种打包除外)
  • 在运行时jvm通过类名查找类, 找到就ok. 找不到? 了解一下NoClassDefFoundError和ClassNotFoundException

这个设计真的十分有趣, 值得学习.

正确的做法

所以如果你的项目依赖很多三方库, 搞的日子有点乱, 那么最好利用SLF4J大法来搞定此事.

  • 确定目标: 我想用哪个日志框架, 比如我想用log4j12.
  • 项目里必须要的依赖首先就是slf4j-api和log4j12.
  • 排除其他依赖, 分两类:
    • 除了log4j12以外的所有日志实现框架的jar, 比如logback-classic.jar, slf4j-simple.jar等等
    • 除了slf4j-log4j以外的所有的slf4j的binding, 比如slf4j-jdk14.jar
  • 根据需要增加一些桥接jar. 就是对上面排除的日志框架, 提供作为替换的桥接. 比如你项目或者某些依赖是使用jcl打印日志的, 那么则需要添加jcl-over-log4j.jar
  • 准备一份log4j12的配置文件如log4j.properties, 完成.

至于怎么从自己项目里排除依赖, 除了手动exclude, SLF4J又提供了两种其他方法.

来捣乱的Log4j2

之前一直都没提, 是因为加入它就更乱了. SLF4J的玩法溜的狠, Log4j2作为最后出现的日志框架, 自然有样学样, 所以他也把SLF4J这套完整的学过去了: 有自己的API, 有桥接和binding, 有自己的实际运行框架.

Log4j2作者和SLF4J作者在SO上激烈辩论

一个人有三板斧, 两个人有六板斧, Facade for Facade. 虽然原理差不多, 如果你的项目里都有的话, 也是够受了.

Log4j2向上把各个日志框架转向自己的桥, 都有很多包, 但是向下, 自己的api转给其他日志框架干活的binding好像没有? 只能是: Log4j2 API - log4j-slf4j-impl.jar - SLF4j API - 其他框架这样?

Log4j2的各项新功能, 性能什么的都挺不错, 但是如果不是碰上强需求, 谁有动力去改用它呢?

各项maven jar依赖分类

在maven依赖树里, 见到各种依赖不要慌:

  • slf4j-xxx.jar. slf4j binding到具体的日志实现用的. 比如slf4j-log4j12.jar, slf4j-jdk14.jar.
  • xxx-over-slf4j.jar. 其他日志框架桥接到slf4j用的. 比如jcl-over-slf4j.jar, log4j-over-slf4j.
  • slf4j-api.jar
  • group名为org.apache.logging.log4j都是Log4j 2.x版本的
  • Log4j 1.x版本的, 包名非常直接: log4j:log4j:jar:1.2.16
  • org.apache.logging.log4j:log4j-1.2-api:jar, Log4j 1.x转2.x的桥, 包名起的好差
  • org.apache.logging.log4j:log4j-api:jar, Log4j2的api
  • org.apache.logging.log4j:log4j-jcl:jar, Jcl到Log4j2的桥
  • org.apache.logging.log4j:log4j-slf4j-impl:jar, SLF4J转到Log4j2的桥
  • org.apache.logging.log4j:log4j-to-slf4j:jar, Log4j2转给SLF4J, facade for facade出现啦

一个实际的例子

我之前搭建了一个简单的基于MapReduce+Hive/HCatalog的测试项目, 本地跑起来的时候日志就不打印. 我们这里就分析一下它.

在没加maven exclude的时候, 初始情况下, 执行mvn dependency:tree可以看到项目的依赖树, 我把其中和日志有关的截取出来:

//SLF4J的api
[INFO] +- org.apache.thrift:libthrift:jar:0.13.0:compile
[INFO] |  +- org.slf4j:slf4j-api:jar:1.7.25:compile

//代码基于slf4j写的, 实际执行中用Log4j2打印
[INFO] +- org.apache.hive:hive-standalone-metastore:jar:3.1.2:compile
[INFO] |  +- org.apache.logging.log4j:log4j-slf4j-impl:jar:2.8.2:compile

//代码基于slf4j写的, 实际执行中用Log4j12打印
[INFO] +- org.apache.hadoop:hadoop-common:jar:3.1.2:provided
[INFO] |  +- org.slf4j:slf4j-log4j12:jar:1.7.25:compile

//代码基于SLF4J/Log4j12写的, 实际执行中用Log4j2打印
[INFO] +- org.apache.hive:hive-standalone-metastore:jar:3.1.2:compile
[INFO] |  +- org.apache.logging.log4j:log4j-slf4j-impl:jar:2.8.2:compile
[INFO] |  |  \- org.apache.logging.log4j:log4j-api:jar:2.8.2:compile
[INFO] |  +- org.apache.logging.log4j:log4j-1.2-api:jar:2.8.2:compile
[INFO] |  |  \- org.apache.logging.log4j:log4j-core:jar:2.8.2:compile

//Log4j2 support for web servlet containers
[INFO] +- org.apache.hive.hcatalog:hive-webhcat-java-client:jar:3.1.2:compile
[INFO] |  +- org.apache.hive.hcatalog:hive-hcatalog-core:jar:3.1.2:compile
[INFO] |  |  +- org.apache.hive:hive-common:jar:3.1.2:compile
[INFO] |  |  |  +- org.apache.logging.log4j:log4j-web:jar:2.10.0:compile

//Log4j12
[INFO] +- org.apache.hadoop:hadoop-hdfs:jar:3.1.2:provided
[INFO] |  +- log4j:log4j:jar:1.2.17:compile

可以看出Hadoop/Hive作为Apache的项目, 倒是很跟随自己的Log4j2走的, 采用的都是Log4j2, 并把别的日志框架都桥接了过来.

我这个项目确实很乱, SLF4J, Log4j1, Log4j2的东西都有. 如果打算采用log4j2打印日志:

  1. 移除log4j 1.x的实现, 主要是slf4j-log4j12:jar和log4j:log4j:jar都exclude即可.
  2. 由于其他框架桥接到Log4j2的依赖已经有了, 保留即可.
  3. 准备一份log4j2.properties.

其他

  • 关于具体日志框架的使用, logger/appender/layout之类的东西, 本文不再讨论
  • 据说性能上log4j2 > logback > log4j
  • 下面的参考资料都很不错

参考