Java日志通关(一) - 前世今生
阿里妹导读
序
一、前言
接口层:或者叫日志门面(facade),就是interface,只定义接口,等着别人实现。 实现层:真正干活的、能够把日志内容记录下来的工具。但请注意它不是上边接口实现,因为它不感知也不直接实现接口,仅仅是独立的实现。 适配层:一般称为Adapter,它才是上边接口的implements。因为接口层和适配层并非都出自一家之手,它们之间无法直接匹配。而鲁迅曾经说过:「计算机科学领域的任何问题都可以通过增加一个中间层来解决」(All problems in computer science can be solved by another level of indirection. -- David Wheeler[1]),所以就有了适配层。
适配层又可以分为绑定(Binding)和桥接(Bridging)两种能力:
绑定(Binding):将接口层绑定到某个实现层(实现一个接口层,并调用实现层的方法) 桥接(Bridging):将接口层桥接到另一个接口层(实现一个接口层,并调用另一个接口层的接口),主要作用是方便用户低成本的在各接口层和适配层之间迁移
如果你觉得上面的描述比较抽象生硬,可以先跳过,等把本篇看完自然就明白了。
接下来我们就以时间顺序,回顾一下Java日志的发展史,这有助于指导我们后续的实践,真正做到知其所以然。
二、历史演进
2.1 标准输出 (<1999)
Java最开始并没有专门记录日志的工具,大家都是用System.out和System.err输出日志。但它们只是简单的信息输出,无法区分错误级别、无法控制输出粒度,也没有什么管理、过滤能力。随着Java工程化的深入,它们的能力就有些捉襟见肘了。
虽然System.out和System.err默认输出到控制台,但它们是有能力将输出保存到文件的:
System.setOut(new PrintStream(new FileOutputStream("log.txt", true)));
System.out.println("这句将输出到 log.txt 文件中");
System.setErr(new PrintStream(new FileOutputStream("error.txt", true)));
System.err.println("这句将输出到 error.txt 文件中");
2.2 Log4j (1999)
在1996年,一家名为SEMPER的欧洲公司决定开发一款用于记录日志的工具。经过多次迭代,最终发展成为Log4j。这款工具的主要作者是一位名叫Ceki Gülcü[2]的俄罗斯程序员,请记住他的名字:Ceki,后面还会多次提到他。
到了1999年,Log4j已经被广泛使用,随着用户规模的增长,用户诉求也开始多样化。于是Ceki在2001年选择将Log4j开源,希望借助社区的力量将Log4j发展壮大。不久之后Apache基金会向Log4j抛出了橄榄枝,自然Ceki也加入Apache继续从事 Log4j的开发,从此Log4j改名Apache Log4j[3]并进入发展的快车道。
Log4j相比于System.out提供了更强大的能力,甚至很多思想到现在仍被广泛接受,比如:
日志可以输出到控制台、文件、数据库,甚至远程服务器和电子邮件(被称做 Appender); 日志输出格式(被称做 Layout)允许定制,比如错误日志和普通日志使用不同的展现形式; 日志被分为5个级别(被称作Level),从低到高依次是debug, info, warn, error, fatal,输出前会校验配置的允许级别,小于此级别的日志将被忽略。除此之外还有all, off两个特殊级别,表示完全放开和完全关闭日志输出; 可以在工程中随时指定不同的记录器(被称做Logger),可以为之配置独立的记录位置、日志级别; 支持通过properties或者xml文件进行配置;
随着Log4j的成功,Apache又孵化了Log4Net[4]、Log4cxx[5]、Log4php[6]产品,开源社区也模仿推出了如Log4c[7]、Log4cpp[8]、Log4perl[9]等众多项目。从中也可以印证Log4j在日志处理领域的江湖影响力。
不过Log4j有比较明显的性能短板,在Logback和Log4j 2推出后逐渐式微,最终Apache在2015年宣布终止开发Log4j并全面迁移至Log4j 2[10](可参考【2.7 Log4j 2 (2012)】)。
2.3 JUL (2002.2)
随着Java工程的发展,Sun也意识到日志记录非常重要,认为这个能力应该由JRE原生支持。所以在1999年Sun提交了JSR 047[11]提案,标题就叫「Logging API Specification」。不过直到2年后的2002年,Java官方的日志系统才随Java 1.4发布。这套系统称做Java Logging API,包路径是java.util.logging,简称JUL。
在某些追溯历史的文章中提到,「Apache曾希望将 Log4j加入到JRE中作为默认日志实现,但傲慢的Sun没有答应,反而很快推出了自己的日志系统」。对于这个说法我并没有找到出处,无法确认其真实性。
不过从实际推出的产品来看,更晚面世的JUL无论是功能还是性能都落后于Log4j,颇有因被寄予厚望而仓促发布的味道,也许那个八卦并非空穴来风,哈哈。虽然在2004年推出的Java 5.0 (1.5) [12]上JUL进步不小,但它在Log4j面前仍无太多亮点,广大开发者并没有迁移的动力,导致JUL始终未成气候。
我们在后文没有推荐JUL的计划,所以这里也不多介绍了(主要是我也不会)。
2.4 JCL (2002.8)
在Log4j和JUL之外,当时市面上还有像Apache Avalon[13](一套服务端开发框架)、 Lumberjack[14](一套跑在JDK 1.2/1.3上的开源日志工具)等日志工具。
对于独立且轻量的项目来说,开发者可以根据喜好使用某个日志方案即可。但更多情况是一套业务系统依赖了大量的三方工具,而众多三方工具会各自使用不同的日志实现,当它们被集成在一起时,必然导致日志记录混乱。
为此Apache在2002年推出了一套接口Jakarta Commons Logging[15],简称 JCL,它的主要作者仍然是Ceki。这套接口主动支持了Log4j、JUL、Apache Avalon、Lumberjack等众多日志工具。开发者如果想打印日志,只需调用JCL的接口即可,至于最终使用的日志实现则由最上层的业务系统决定。我们可以看到,这其实就是典型的接口与实现分离设计。
但因为是先有的实现(Log4j、JUL)后有的接口(JCL),所以JCL配套提供了接口与实现的适配层(没有使用它的最新版,原因会在【1.2.7 Log4j2 (2012)】提到):
简单介绍一下JCL自带的几个适配层/实现层:
AvalonLogger/LogKitLogger:用于绑定Apache Avalon的适配层,因为Avalon 不同时期的日志包名不同,适配层也对应有两个
Jdk13LumberjackLogger:用于绑定Lumberjack的适配层
Jdk14Logger:用于绑定JUL(因为JUL从JDK 1.4开始提供)的适配层
Log4JLogger:用于绑定Log4j的适配层
NoOpLog:JCL自带的日志实现,但它是空实现,不做任何事情
SimpleLog:JCL自带的日志实现 ,让用户哪怕不依赖其他工具也能打印出日志来,只是功能非常简单
当时项目前缀取名Jakarta,是因为它属于Apache与Sun共同推出的Jakarta Project[16]项目(邮件[17])。现在JCL作为Apache Commons[18]的子项目,叫 Apache Commons Logging,与我们常用的Commons Lang[19]、Commons Collections [20]等是师兄弟。但JCL的简写命名被保留了下来,并没有改为ACL。
2.5 Slf4j (2005)
Log4j的作者Ceki看到了很多Log4j和JCL的不足,但又无力推动项目快速迭代,加上对Apache的管理不满,认为自己失去了对Log4j项目的控制权(博客[21]、邮件[22]),于是在2005年选择自立门户,并很快推出了一款新作品Simple Logging Facade for Java[23],简称Slf4j。
Slf4j也是一个接口层,接口设计与JCL非常接近(毕竟有师承关系)。相比JCL有一个重要的区别是日志实现层的绑定方式:JCL是动态绑定,即在运行时执行日志记录时判定合适的日志实现;而Slf4j选择的是静态绑定,应用编译时已经确定日志实现,性能自然更好。这就是常被提到的classloader问题,更详细地讨论可以参考What is the issue with the runtime discovery algorithm of Apache Commons Logging[24]以及Ceki自己写的文章Taxonomy of class loader problems encountered when using Jakarta Commons Logging[25]。
在推出Slf4j的时候,市面上已经有了另一套接口层JCL,为了将选择权交给用户(我猜也为了挖JCL的墙角),Slf4j推出了两个桥接层:
jcl-over-slf4j:作用是让已经在使用JCL的用户方便的迁移到Slf4j 上来,你以为调的是JCL接口,背后却又转到了Slf4j接口。我说这是在挖JCL的墙角不过分吧? slf4j-jcl:让在使用Slf4j的用户方便的迁移到JCL上,自己的墙角也挖,主打的就是一个公平公正公开。
Slf4j通过推出各种适配层,基本满足了用户的所有场景,我们来看一下它的全家桶:
网上介绍Slf4j的文章,经常会引用它官网上的两张图:
感兴趣的同学也可以参考。
这里解释一下slf4j-log4j12这个名字,它表示Slf4j + Log4j 1.2(Log4j的最后一个版本) 的适配层。类似的,slf4j-jdk14表示Slf4j + JDK 1.4(就是 JUL)的适配层。
2.6 Logback (2006)
然而Ceki的目标并不止于Slf4j,面对自己一手创造的Log4j,作为原作者自然是知道它存在哪些问题的。于是在2006年Ceki又推出了一款日志记录实现方案:Logback[26]。无论是易用度、功能、还是性能,Logback 都要优于Log4j,再加上天然支持Slf4j而不需要额外的适配层,自然拥趸者众。目前Logback已经成为Java社区最被广泛接受的日志实现层(Logback自己在2021年的统计是48%的市占率[27])。
相比于Log4j,Logback提供了很多我们现在看起来理所当然的新特性:
支持日志文件切割滚动记录、支持异步写入 针对历史日志,既支持按时间或按硬盘占用自动清理,也支持自动压缩以节省硬盘空间 支持分支语法,通过<if>, <then>, <else>可以按条件配置不同的日志输出逻辑,比如判断仅在开发环境输出更详细的日志信息 大量的日志过滤器,甚至可以做到通过登录用户Session识别每一位用户并输出独立的日志文件 异常堆栈支持打印jar包信息,让我们不但知道调用出自哪个文件哪一行,还可以知道这个文件来自哪个jar包
Logback主要由三部分组成(网上各种文章在介绍classic和access时都描述的语焉不详,我不得不直接翻官网文档找更明确的解释):
logback-core:记录/输出日志的核心实现 logback-classic:适配层,完整实现了Slf4j接口 logback-access[28]:用于将Logback集成到Servlet容器(Tomcat、Jetty)中,让这些容器的HTTP访问日志也可以经由强大的Logback输出
2.7 Log4j 2 (2012)
看着Slf4j + Logback搞的风生水起,Apache自然不会坐视不理,终于在2012年憋出一记大招:Apache Log4j 2[29],它自然也有不少亮点:
插件化结构[30],用户可以自己开发插件,实现Appender、Logger、Filter完成扩展 基于LMAX Disruptor的异步化输出[31],在多线程场景下相比Logback有10倍左右的性能提升,Apache官方也把这部分作为主要卖点加以宣传,详细可以看Log4j 2 Performance[32]。
Log4j 2主要由两部分组成:
log4j-core:核心实现,功能类似于logback-core log4j-api:接口层,功能类似于Slf4j,里面只包含Log4j 2的接口定义
不过目前大家一般把Log4j 2作为实现层看待,并引入JCL或Slf4j作为接口层。特别是JCL,在时隔近十年后,于2023年底推出了1.3.0 版[34],增加了针对Log4j 2的适配。还记得我们在【1.2.4 JCL (2002.8)】中没有用最新版的JCL做介绍吗,就是因为这个十年之后的版本把那些已经「作古」的日志适配层@Deprecated掉了。
多说一句,其实Logback和Slf4j就像log4j-core和log4j-api的关系一下,目前如果你想用Logback也只能借助Slf4j。但谁让它们生逢其时呢,大家就会分别讨论认为是两个产品。
虽然Log4j 2发布至今已有十年(本文写于2024年),但它仍然无法撼动Logback的江湖地位,我个人总结下来主要有两点:
Log4j 2虽然顶着Log4j的名号,但却是一套完全重写的日志系统,无法只通过修改Log4j版本号完成升级,历史用户升级意愿低 Log4j 2比Logback晚面世6年,却没有提供足够亮眼及差异化的能力(前边介绍的两个亮点对普通用户并没有足够吸引力),而Slf4j+Logback这套组合已经非常优秀,先发优势明显
比如,曾有人建议Spring Boot将日志系统从Logback切换到Log4j2[35],但被Phil Webb[36](Spring Boot核心贡献者)否决。他在回复中给出的原因包括:Spring Boot需要保证向前兼容以方便用户升级,而切换Log4j 2是破坏性的;目前绝大部分用户并未面临日志性能问题,Log4j 2所推崇的性能优势并非框架与用户的核心关切;以及如果用户想在Spring Boot中切换到Log4j 2也很方便(如需切换可参考 官方文档[37])。
2.8 spring-jcl (2017)
因为目前大部分应用都基于Spring/Spring Boot搭建,所以我额外介绍一下spring-jcl [38]这个包,目前Spring Boot用的就是spring-jcl + Logback这套方案。
Spring曾在它的官方Blog《Logging Dependencies in Spring》[39]中提到,如果可以重来,Spring会选择李白Slf4j而不是JCL作为默认日志接口。
现在Spring又想支持Slf4j,又要保证向前兼容以支持JCL,于是从5.0(Spring Boot 2.0)开始提供了spring-jcl这个包。它顶着Spring的名号,代码中包名却与JCL 一致(org.apache.commons.logging),作用自然也与JCL一致,但它额外适配了Slf4j,并将Slf4j放在查找的第一顺位,从而做到了「既要又要」(你可以回到【1.2.4 JCL (2002.8)】节做一下对比)。
如果你是基于Spring Initialize [40]新创建的应用,可以不必管这个包,它已经在背后默默工作了;如果你在项目开发过程中遇到包冲突,或者需要自己选择日志接口和实现,则可以把spring-jcl当作JCL对待,大胆排除即可。
2.9 其他
除了我们上边提到的日志解决方案,还有一些不那么常见的,比如:
Flogger[41]:由Google在2018年推出的日志接口层。首字母F的含义是Fluent,这也正是它的最大特点:链式调用(或者叫流式API,Slf4j 2.0也支持Fluent API 了,我们会在后续系列文章中介绍) JBoss Logging[42]:由RedHat在约2010年推出,包含完整的接口层、实现层、适配层 slf4j-reload4j[43]:Ceki基于Log4j 1.2.7 fork出的版本,旨在解决Log4j的安全问题,如果你的项目还在使用Log4j且不想迁移,建议平替为此版本。(但也不是所有安全问题都能解决,具体可以参考上边的链接)
因为这些日志框架我们在实际开发中用的很少,此文也不再赘述了(主要是我也不会)。
三、总结
历史介绍完了,但故事并没有结束。两个接口(JCL、Slf4j)四个实现(Log4j、JUL、Logback、Log4j2),再加上无数的适配层,它们之间串联成了一个网,我专门画了一张图:
解释/补充一下这张图:
相同颜色的模块拥有相同的groupId,可以参考图例中给出的具体值。 JCL的适配层是直接在它自己的包中提供的,详情我们在前边已经介绍过,可以回【1.2.4 JCL (2002.8)】查看。 要想使用Logback,就一定绕不开Slf4j(引用它的适配层也算);同样的,要想使用 Log4j 2,那它的log4j-api也绕不开。
如果你之前在看「1.1 前言」时觉得过于抽象,那么此时建议你再回头看一下,相信会有更多体会。
从这段历史,我也发现了几个有趣的细节:
在Log4j 2面世前后的很长一段时间,Slf4j及Logback因为没有竞争对手而更新缓慢。英雄没有对手只能慢慢垂暮,只有棋逢对手才能笑傲江湖。 技术人的善良与倔强:面世晚的产品都针对前辈产品提供支持;面世早的产品都不搭理它的「后辈」。 计算机科学领域的任何问题都可以通过增加一个中间层来解决,如果不行就两个(桥接层干的事儿)。 Ceki一人肩挑Java日志半壁江山25年(还在增长ing),真神人也。(当然在代码界有很多这样的神人,比如Linus Torvalds[44]维护Linux至今已有33年,虽然后期主要作为产品经理参与,再比如已故的Bram Moolenaar[45]老爷子持续维护 Vim 32年之久)。
微信扫码关注该文公众号作者