该让log4j退休了 - 论Java日志组件的选择

历史

log4j可以当之无愧地说是Java日志框架的元老,1999年发布首个版本,2012年发布最后一个版本,2015年正式宣布终止,至今还有无数的系统在使用log4j,甚至很多新系统的日志框架选型仍在选择log4j。

然而老的不等于好的,在IT技术层面更是如此。尽管log4j有着出色的历史战绩,但早已不是Java日志框架的最优选择。

在log4j被Apache Foundation收入门下之后,由于理念不合,log4j的作者Ceki离开并开发了slf4j和logback。

slf4j因其优秀的性能和理念很快受到了广泛欢迎,2016年的统计显示,github上的热门Java项目中,slf4j是使用率第二名的类库(第一名是junit)。

logback则吸取了log4j的经验,实现了很多强大的新功能,再加上它和slf4j能够无缝集成,也受到了欢迎。

在这期间,Apache Logging则一直在关门憋大招,log4j2在beta版鼓捣了几年,终于在2014年发布了GA版,不仅吸收了logback的先进功能,更通过优秀的锁机制、LMAX Disruptor、"无垃圾"机制等先进特性,在性能上全面超越了log4j和logback。

slf4j

slf4j是一个“日志门面”(Logging Facade),而不是一个完整的日志框架。它提供了一套记录日志的api,但不提供输出日志的功能,而是通过对接如log4j、java.util.logging等日志框架,来实现日志的输出。

所以说slf4j的对标选手实际上是Jakarta Commons Logging(简称JCL),二者的最大区别在于与日志服务的绑定机制

JCL采用的动态绑定机制:

  1. 在进程启动时尝试获取名为"org.apache.commons.logging.Log"的配置属性(可与在commons-logging.properties文件中配置,或使用Java代码进行配置),按配置选取对应的日志输出服务
  2. 如果没有获取到对应配置属性,会尝试在系统参数中寻找名为"org.apache.commons.logging.Log"的参数项
  3. 如果1,2均没有获取到,会在classpath下寻找log4j的相关class,如果找到,则使用log4j作为日志输出服务
  4. 如果没有找到log4j,则尝试使用java.util.logging包作为日志输出服务
  5. 如果上述都失败,则使用SimpleLog作为日志输出服务,即将所有日志输出至控制台标准输出System.err

JCL的动态绑定机制基于ClassLoader实现,缺点一是效率较低,二是容易引发混乱,在一个复杂甚至混乱的依赖环境下,确定当前正在生效的日志服务是很费力的,特别是在程序开发和设计人员并不理解JCL的机制时,三是最致命的问题:在使用了自定义ClassLoader的程序中,使用JCL会引发各类问题,例如内存泄露、与OSGI冲突等。

而slf4j则简单得多,采用静态绑定机制:

  • slf4j为各类日志输出服务提供了适配库,如slf4j-log4j12,slf4j-simple,slf4j-jdk14等。一个Java工程下只能引入一个slf4j适配库
  • slf4j会加载org.slf4j.impl.StaticLoggerBinder作为输出日志的实现类。这个类在每个适配库中都存在,所以slf4j不需要像JCL一样主动去寻找日志输出实现,自然而然地就能与具体的日志输出实现绑定起来
  • 当需要更换日志输出服务时(比如从logback切换回log4j),只需要替换掉适配库即可

所以slf4j不仅对比JCL有性能上的优势,使用slf4j的程序员也不需要去翻找配置文件或追踪启动过程就能够清除明白地了解当前使用的是什么日志输出服务。

slf4j的优势还不止此:

强制输出String,避免不规范代码

传统的日志api都接收Object类型的参数,在程序员不遵守规范时容易引发一些错误,比如:

SomeObject obj;
//...
logger.info(obj); //如果SomeObject并未覆盖toString()方法,这里就只记下来了hashcode

又如:

try {
  //...
} catch(Exception e) {
  logger.error(e); //未记录异常stacktrace
}

slf4j的api强制要求传入String类型的参数,能够在一定程度上避免此类不规范的代码出现。

日志模板功能

在使用传统的日志api时,可能会有这样的代码:

logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));

这引发了两个问题:

  1. 需要编写拼接字符串的代码,使开发效率降低
  2. 即使不需要输出这条日志(比如当前日志级别是ERROR时),也会执行拼接字符串的操作,消耗额外性能,占用额外内存

而slf4j使用日志模板功能解决了这两个问题:

logger.debug("Entry number: {} is {}", i, String.valueOf(entry[i]));

不仅开发变得简单了,而且slf4j只会在此条日志确实需要输出时才会去拼装字符串。
并且在输出异常信息时也可以使用模板,不会妨碍stacktrace的输出:

String s = "Hello world";
try {
  Integer i = Integer.valueOf(s);
} catch (NumberFormatException e) {
  logger.error("Failed to format {}", s, e);
}

slf4j的桥接功能

虽然slf4j如此优秀,但一些类库因为历史原因仍然在使用JCL作为日志api(如Spring等),为此slf4j还推出了jcl-over-slf4j桥接库,能够把使用JCL的API输出的日志桥接到slf4j上,方便那些想要使用slf4j作为日志门面但同时又要使用Spring等需要依赖JCL的类库的系统。

对于自动依赖JCL的类库,如要桥接至slf4j的话,除了引入jcl-over-slf4j适配库之外,还需要把JCL库从classpath中移除。可以在maven配置中将JCL库标记为provided:


commons-logging
commons-logging
1.1.1
provided

同时slf4j还提供了log4j-over-slf4j等桥接库,能够在不改动代码的前提下把使用各种日志框架输出的日志都桥接到slf4j,如下图:

该让log4j退休了 - 论Java日志组件的选择_第1张图片
legacy.png

slf4j提供的适配库和桥接库

适配库:

  • slf4j-log4j12:使用log4j-1.2作为日志输出服务
  • slf4j-jdk14:使用java.util.logging作为日志输出服务
  • slf4j-jcl:使用JCL作为日志输出服务
  • slf4j-simple:日志输出至System.err
  • slf4j-nop:不输出日志
  • log4j-slf4j-impl:使用log4j2作为日志输出服务

logback天然与slf4j适配,不需要额外引入适配库(毕竟是一个作者写的)

桥接库:

  • log4j-over-slf4j:将使用log4j api输出的日志桥接至slf4j
  • jcl-over-slf4j:将使用JCL api输出的日志桥接至slf4j
  • jul-to-slf4j:将使用java.util.logging输出的日志桥接至slf4j
  • log4j-to-slf4j:将使用log4j2输出的日志桥接至slf4j

题外话:
slf4j唯独没有提供log4j2的适配库和桥接库,log4j-slf4j-impl和log4j-to-slf4j都是Apache Logging自己开发的,看样子Ceki和Apache Logging的梁子真的很深啊……倒是Apache没有端架子,可能也是因为slf4j太火了吧

logback与log4j2

logback和log4j2都宣称自己是log4j的后代,一个是出于同一个作者,另一个则是在名字上根正苗红。

撇开血统不谈,比较一下log4j2和logback:

  • log4j2比logback更新:log4j2的GA版在2014年底才推出,比logback晚了好几年,这期间log4j2确实吸收了slf4j和logback的一些优点(比如日志模板),同时应用了不少的新技术
  • 由于采用了更先进的锁机制和LMAX Disruptor库,log4j2的性能优于logback,特别是在多线程环境下和使用异步日志的环境下
  • 二者都支持Filter(应该说是log4j2借鉴了logback的Filter),能够实现灵活的日志记录规则(例如仅对一部分用户记录debug级别的日志)
  • 二者都支持对配置文件的动态更新
  • 二者都能够适配slf4j,logback与slf4j的适配应该会更好一些,毕竟省掉了一层适配库
  • logback能够自动压缩/删除旧日志
  • logback提供了对日志的HTTP访问功能
  • log4j2实现了“无垃圾”和“低垃圾”模式。简单地说,log4j2在记录日志时,能够重用对象(如String等),尽可能避免实例化新的临时对象,减少因日志记录产生的垃圾对象,减少垃圾回收带来的性能下降

log4j2和logback各有长处,总体来说,如果对性能要求比较高的话,log4j2相对还是较优的选择。

附上log4j2与logback性能对比的benchmark,这份benchmark是Apache Logging出的,有多大水分不知道,仅供参考

同步写文件日志的benchmark:

该让log4j退休了 - 论Java日志组件的选择_第2张图片
图片.png

异步写日志的benchmark:

该让log4j退休了 - 论Java日志组件的选择_第3张图片
图片.png

当然,这些benchmark都是在日志Pattern中不包含Location信息(如日志代码行号 ,调用者信息,Class名/源码文件名等)时测定的,如果输出Location信息的话,性能谁也拯救不了:

该让log4j退休了 - 论Java日志组件的选择_第4张图片
图片.png

对Java日志组件选型的建议

  1. slf4j已经成为了Java日志组件的明星选手,可以完美替代JCL,使用JCL桥接库也能完美兼容一切使用JCL作为日志门面的类库,现在的新系统已经没有不使用slf4j作为日志API的理由了
  2. 日志记录服务方面,log4j在功能上输于logback和log4j2,在性能方面log4j2则全面超越log4j和logback。所以新系统应该在logback和log4j2中做出选择,对于性能有很高要求的系统,应优先考虑log4j2

对现有系统日志架构的改造建议

  1. 如果现有系统使用JCL作为日志门面,又确实面临着JCL的ClassLoader机制带来的问题,完全可以引入slf4j并通过桥接库将JCL api输出的日志桥接至slf4j,再通过适配库适配至现有的日志输出服务(如log4j),如下图:


    该让log4j退休了 - 论Java日志组件的选择_第5张图片
    图片.png

    这样做不需要任何代码级的改造,就可以解决JCL的ClassLoader带来的问题,但没有办法享受日志模板等slf4j的api带来的优点。不过之后在现系统上开发的新功能就可以使用slf4j的api了,老代码也可以分批进行改造。

  2. 如果现有系统使用JCL作为日志门面,又头疼JCL不支持logback和log4j2等新的日志服务,也可以通过桥接库以slf4j替代JCL,但同样无法直接享受slf4j api的优点。

  3. 如果想要使用slf4j的api,那么就不得不进行代码改造了,当然改造也可以参考1中提到的方式逐步进行。

  4. 如果现系统面临着log4j的性能问题,可以使用Apache Logging提供的log4j到log4j2的桥接库log4j-1.2-api,把通过log4j api输出的日志桥接至log4j2。这样可以最快地使用上log4j2的先进性能,但组件中缺失了slf4j,对后续进行日志架构改造的灵活性有影响。另一种办法是先把log4j桥接至slf4j,再使用slf4j到log4j2的适配库。这样做稍微麻烦了一点,但可以逐步将系统中的日志输出标准化为使用slf4j的api,为后面的工作打好基础。

最后附上一些链接
slf4j官网: https://www.slf4j.org/
logback官网: https://logback.qos.ch/
log4j2官网: http://logging.apache.org/log4j/2.x/

你可能感兴趣的:(该让log4j退休了 - 论Java日志组件的选择)