Java Logging Framework 现状

某日笔者心情不错,写代码时没有复制粘贴,打算手敲logger的相关代码,在IDE获得的提示是这样的:

IDE的提示

尽管知道ch.qos.logback.classic.Logger在项目中是正确的选择,上图中Logger同名类的数量确实确实让笔者惊讶了一番,为什么这么多同名类?于是就有了这一篇文章。

逐一观察上述的Logger列表后,笔者发现很多jar包的编译版本是Java5、Java6,以com.alibaba.nacos.client.logger.Logger接口为例,它有这几个三个实现类Log4j2Logger,NopLogger,Slf4jLogger,不难看出alibaba这个Logger类是为了灵活切换Log4j2和Slf4j的调用。

由此笔者推测,老旧的Logger如此之多,多半是由于java.util.logging提供的功能太弱,且当时市面上流行的日志框互相之间的兼容性并不好,开发者们只好自己动手,丰衣足食。而时至今日,Java主流的日志框架发展到了什么程度?旧版本的兼容问题是如何处理的?带着这些问题,我们一起来分析下Java Logging Framework现状。

本文将从流行的日志框架、日志框架实现、日志框架通用API、常见问题这四个方面进行叙述。

1. 流行的Java日志框架

如何确定哪些框架是最流行的日志框架呢,mvnrepository是一个很好的参考,该网站logging-frameworks分类下被使用次数最多的artifact整理如下(统计日期为2018-12-22):

groupIdartifactId使用次数占比最后更新日期
org.slf4jslf4j-api334012018年3月21日
ch.qos.logbacklogback-classic143662018年2月11日
log4jlog4j139312012年5月26日
commons-loggingcommons-logging82782014年7月5日
org.slf4jslf4j-simple73682018年3月21日
org.apache.logging.log4jlog4j-core37092018年7月30日
org.apache.logging.log4jlog4j-api29302018年7月30日
ch.qos.logbacklogback-core29252018年2月11日
org.jboss.loggingjboss-logging17342018年2月14日
org.clojuretools.logging12582018年3月19日

其中slf4j-api、logback-classic和log4j遥遥领先其它框架;commons-logging和log4j的最后更新时间都是几年前,明显已停止维护了。另外,apache大名鼎鼎的log4j-api和log4j-core竟然排在后面,确实出人意料。至于排在末两位的org.jboss.logging和org.clojure在此略作说明,后续不再做讨论。

上述日志框架可以分为两类:具体实现、通用API。日志通用API仅提供一套通用API,不提供具体实现,也不包装具体实现的配置(logback.xml或log4j.xml等),从而达到快速切换具体实现的目的。

日志框架具体实现有:

名称说明
logback-classicLog4j 1.x项目发起人离开apache后,主导开发出的一套日志框架实现,默认实现slf4j-api,现在已经是最热门的日志框架。
Log4j 1.x又名log4j12,意为1.x的最后一个版本1.2,apache早期推出的日志实现,奠定了Java日志框架的基础,大量老项目使用了该框架,目前已停止更新。
log4j 2.xapache最近几年新推出的日志框架,功能齐全、性能强劲、文档齐全。
Java Util Logging又名JUL(首字母缩写)、JDK logging、jdk14(发布于jdk1.4),包含在JDK内部java.util.logging包下的日志实现,每个服务都包含这个实现,因此不包含在上面的排行榜中。

日志框架通用API(又名Logging Shim或Logging Bridge)有:

名称说明
slf4j-api当下如日中天的日志API,大量的开发者依赖此api编写日志相关代码,后文将展开详细解读。
commons-logging又名JCL(Jakarta Commons Logging),apache早期提供的日志api,由于种种原因没有发展起来,现在已停止更新。
org.jboss.logging由于其支持国际化功能,hibernate从4.0开始一直使用该框架,本文后续不讨论该框架。
org.clojure在Clojure语言中使用,本文后续不讨论该框架。

2. 日志框架具体实现

日志框架一般主要由三部分构成:Logger、Formatter和Handler(又名Appender);其中Logger负责收集需要记录的信息和一些元数据,随后Formatter将收集到的信息进行格式化,最后由Handler(又名Appender)决定日志输出的方式,输出方式多种多样,可以控制台、磁盘文件等。

一个Logger可以同时关联到多个Appender,因此一份日志可以同时以多种方式输出;一个Appender再关联到一个Formatter以指定其格式。多个Logger之间具有特定的层次结构,下面笔者对这种层次结构进行详细介绍。

Named的层次结构

Logger的名称通常具有层次结构,如下方所示,com.sun是com的子级,com.sun.some是com.sun的子级。

  • com
  • com.sun
  • com.sun.some

Level的层次结构

若当前Logger没有设置日志级别,则从父级继承;若当前Logger已设置日志级别,则忽略父级的日志级别,举例如下:

Logger名称声明的Level继承的Level
rootProotProot
XPxPx
X.YnonePx
X.Y.ZPxyzPxyz

Appender的可叠加性

子级Logger会从父级继承关联的Appender作为自己的Appender,与子级自身关联的Appender叠加起来(即Appender使是相同的),距离如下:

Logger名称叠加的Appender叠加性是标识输出目标
rootA1不可配置A1
xA-x1, A-x2trueA1, A-x1, A-x2
x.ynonetrueA1, A-x1, A-x2
x.y.zA-xyz1trueA1, A-x1, A-x2, A-xyz1
securityA-secfalseA-sec
security.accessnonetrueA-sec

最后,让我们回到上文所述的四个日志框架的具体实现上来,下面将按照框架诞生的时间顺序,进行逐一介绍;其中logback和log4j 2.x社区都非常活跃,是Java日志框架的主力军,笔者将进行重点介绍。

2.1 Log4j 1.x(始于1999)

Log4j 1.x是由Ceki Gülcü在ASF(Apache Software Foundation)发起的开源项目,其第一个版本发布于1999年,一经发布就在开源社区中得到了广泛的使用,包括一些大名鼎鼎的项目,如JBoss和Hibernate。Log4j的体系结构是围绕三个主要概念构建的:loggers、appenders和layouts,在此之后的日志框架,大多也采用了这个结构。

Log4j 1.x最后更新时间为2012年5月26日(版本为1.2.17),于2015年8月5日正式宣布停止更新。 时至今日,Log4j 1.x的引用比例仍有16%,开发者们更多的考虑的是如何兼容旧版本,除了这个理由之外,不会再选择该框架了。

2.2 Java Util Logging(始于2002)

Java Util Logging是2002年发布的Java1.4新增的特性,其模仿log4j实现了基本的日志输出功能,但由于发布时间相比Log4j 1.x晚一些,且功能上不如Log4j完善,一直没有真正流行起来;目前对于该框架,多数项目考虑的也是兼容性。

2.3 logback(始于2006)

Log4j 1.x项目开展到后期,项目发起者Ceki Gülcü觉得自己失去了对框架的控制,新特性的决定变的复杂,提意见的人太多了,陷入了无休止的邮件往来之中;后来Ceki Gülcü决定脱离ASF,打算重头开始再写一个日志框架,于是logback诞生了。

2006年7月26日,logback的第一个release版本正式发布,随后快速地迭代发布新版本,目前是mvnrepository上显示的日志框架中最为流行的;而该项目发起人Ceki Gülcü着实是个牛人,笔者特地找了其本人的两张照片放在下面(侵删)。

Ceki Gülcü

logback分为三个模块,logback-core, logback-classic和logback-access。其中logback-core模块为其他两个模块提供了基础功能;logback-classic模块可以视为log4j的改进版本,同时logback-classic天生就实现了SLF4J API,这样开发者就可以在不修改客户端代码的前提下,随时切换底层具体实现。

logback-access模块可以与Servlet容器(如Tomcat和Jetty)集成,以提供HTTP-access日志功能。logback具有以下优秀特性:

  • 速度快且占用内存少
  • 经过了大量的测试
  • 天生实现了SLF4J API,使用SLF4J API时不会存在任何性能损失
  • 丰富完善的文档
  • 配置文件支持XML或Groovy
  • 自动重新加载配置文件
  • 可以从I/O故障中进行优雅的恢复
  • 可以自动删除旧日志档案
  • 可以压缩日志文件
  • 谨慎模式下,可以让多个JVM写入同一个文件
  • 提供名为Lilith的日志查看器,可以查看大日志文件
  • 配置文件支持if-then-else的判断
  • 过滤器(Filter)机制提供的扩展,比如可以在不改变日志level的前提下,输出某些level更低的日志
  • SiftingAppender可以根据运行时属性拆分日志文件,比如为每个用户单独创建一份日志文件
  • 发生异常时,打印完整的堆栈信息,精确到对应jar包的版本号
  • logback-access对HTTP-access日志提供了强有力的支持

2.4 Log4j 2.x(始于2014)

Log4j 2.x(又名log4j2)是ASF对Log4j 1.x的重构和升级,并且不再与之前的版本兼容。该项目始于2012年07月29日,2014年07月12日发布了第一个release版本,Log4j2是这四个日志实现框架中最年轻的一个,它综合了Log4j 1.x和logback的优点,并改进它们已知的不足,可以说是这四个日志框架中最新先进的。它具有以下优秀特性:

  • API与实现分离,使开发者清楚地知道可以使用哪些类和方法,同时确保向前兼容
  • 性能显著提示,官网文档宣称在多线程场景下,性能明显优于其他三个日志框架
  • 支持多种API,支持Log4j 1.2、SLF4J、Commons Logging和java.util.logging (JUL) APIs
  • 避免锁定实现,提供log4j-to-slf4j适配器,可以重定向访问Log4j2的请求
  • 支持配置文件自动重载,并且解决了logback自动重载配置文件时的问题
  • 更加先进的过滤器(Filter),在logback之上做了优化
  • 支持插件机制
  • 配置属性支持
  • Java 8 Lambda表达式支持
  • 自定义日志级别
  • 在运行的程序中不产生内存垃圾,在web程序中产生少量内存垃圾,减轻了垃圾回收的压力
  • 提供与应用服务器集成的能力,如tomcat和netty

作为最晚出现的日志框架,Log4j 2.x的细节特性优于以往的任何框架,但为何mvnrepository中使用量如此低呢?笔者分析,可能是SLF4J API的深入人心,而logback默认实现了该API,使用起来性能不存在损耗,所以多数开发者习惯性选择了logback作为其实现。

不管怎样,Log4j2是一个强大而健壮的日志记录框架,具有非常灵活的配置选项,相信未来会有越来越多的人使用该框架。

3. 日志框架API

日志框架API,又名Logging Shim或Logging Bridge,设计目的是解决应用程序切换日志实现框架不方便的问题,API不提供日志输出的配置功能,如何编写配置文件需要具体的日志实现框架决定。 使用最广泛的API是slf4j-apiApache Commons Logging(又名JCL,Jakarta Commons Logging)。一般情况下,通过API调用日志框架实现的流程如下:
调用日志框架API流程

3.1 JCL(Jakarta Commons Logging)(始于2002)

JCL是Apache与2002年发布的一个日志API,最后一次更新时间为2014年07月11日, 使用它可以隔离具体的日志实现,使底层日志实现代码不侵入项目,方便进行切换。当引入了common-logging之后,它将自动查找当前classpath下包含的,用户不需要任何配置。查找的顺序如下:

  1. 寻找配置文件中是否为org.apache.common.logging.Log配置的值
  2. 是否包含log4j
  3. 是否包含JDK Logger
  4. 使用Common-logging自带的SimpleLog

如上所述,如果想要指定使用哪个实现,可以在classpath目录下新增common-logging.properties文件,指定Log4j示例如下:

org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger

3.2 SLF4J(Simple Logging Facade for Java)(始于2005)

SLF4J始于2005年,是目前使用最为广泛的日志API,该API也是由Ceki Gülcü主导发起的;与JCL不同的是,API转换过程不包含在SLF4J API中,SLF4J运用了Java提供的Service Loader机制,将SLF4J API和实际的转换映射过程在依赖上完全分离,在runtime时才使用具体实现。

3.2.1 SLF4J API绑定日志实现

SLF4J API需要一些用于绑定日志实现的jar包,来确定到底使用哪个日志框架,这些Jar包也包含了对应日志实现框架的依赖;已有的Jar包整理汇总如下:

JAR描述
slf4j-log4j12用于绑定Log4j 1.2版本,即调用SLF4J API时,重定向到Log4j 1.2
slf4j-jdk14用于绑定JUL,即调用SLF4J API时,重定向到JUL(java.util.logging)
slf4j-jcl用于绑定JCL,调用SLF4J API时,重定向到JCL(apache的Jakarta Commons Logging)
slf4j-nop绑定一个空实现,默认忽略掉所有的日志
slf4j-simple绑定简单实现,直接输入日志到控制台
logback-classic天生实现SLF4J API,调用SLF4J API时,使用logback相关库
log4j-slf4j-impl该库是由apache提供,作用是调用SLF4J API时,重定向到Log4J 2.x

SLF4J API在运行时会进行自动检测当前classpath中是否包含绑定日志实现的Jar包,若存在则实例化该Jar包指向的的日志实现。注意SLF4J API不支持同时使用多个日志实现,当classpath中出现多个实现时,SLF4J API只会选择使用其中一个并打印警告信息。下图说明了绑定过程:

3.2.2 SLF4J 桥接遗留的API

SLF4J除了以API的身份包装底层接口外,还有桥接遗留API的功能;假设某个项目中包含了JUL、JCL、Log4j 1.x这些日志框架的日志调用,其中明确引入了具体的框架实现,SLF4J可以把这些框架的调用强行重定向到已经绑定的日志实现,从而使项目中所有的日志调用全部指向一个唯一的实现。可以用桥接遗留API的Jar包整理如下:

JAR描述
jcl-over-slf4j桥接JCL到SLF4J,将使用JCL相关代码的日志调用重定向到SLF4J
jul-to-slf4j桥接JUL到SLF4J,将使用JUL相关代码的日志调用重定向到SLF4J;此转换会有额外的性能开销,需要做一定处理才行,详见Bridging legacy APIs
log4j-over-slf4j桥接Log4j 1.x到SLF4J,将使用Log4j 1.x相关代码的日志调用重定向到SLF4J

jcl-over-slf4j vs slf4j-jcl

注意这两个Jar不能同时使用,前者是将JCL的调用重定向到SLF4J绑定的日志实现,后者又将日志实现指定为了JCL,这是自相矛盾的;而JUL和Log4j 1.x也是如此。

若读者觉得不好理解,可以直接查看笔者在码云上的示例代码SLF4jLog4jCoverOthers。该功能对于SLF4J API的快速传播起到了重要作用,它的流程是这样的:

4. 常见问题

常见问题中的相关代码,笔者已提交至码云,有需要可以查看sample-logging-framework

4.1 为SLF4J绑定多个日志实现

由于SLF4J API仅支持绑定一个日志实现,在pom中同时指定两个日志实现会得到如下的错误信息,此时去掉一种一个实现的依赖即可,比如下面的场景去除slf4j-log4j12的依赖。

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/Users/ypk/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/Users/ypk/.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 [ch.qos.logback.classic.util.ContextSelectorStaticBinder]

4.2 日志重复打印

有时候我们会发现一行日志被重复打印了多次,这是由于Appender的可叠加性导致的(详见上文),子级和父级Logger同时绑定了Appender;解决办法一般是在子级关闭Appender的可叠加性,以Log4j 2.x的配置为例,指定additivity为false:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Logger name="com.learn.log4j2.trace" level="trace" additivity="false"><!-- additivity关闭继承特性 -->
            <AppenderRef ref="Console"/>
        </Logger>
        <Root level="info">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

4.3 自相矛盾的依赖

同时指定自相矛盾的 jcl-over-slf4j 和 slf4j-jcl,将会得到下面的异常;解决办法是去掉一种的一个。

Caused by: java.lang.IllegalStateException: Detected both jcl-over-slf4j.jar AND bound slf4j-jcl.jar on the class path, preempting StackOverflowError. See also http://www.slf4j.org/codes.html#jclDelegationLoop for more details.
	at org.slf4j.impl.JCLLoggerFactory.<clinit>(JCLLoggerFactory.java:54)
	... 30 more

5. 小结

本文介绍了Java当下流行的Logging Framework,旨在使读者对Java日志框架有一个整体印象。Logger实现一般分为三部分:Logger、Formatter和Handler(又名Appender),Logger之间存在层级关系。

在日志框架的具体实现中,可选的有logback和Log4j 2.x;目前选择logback选择的人更多,其与SL4J API无缝集成;Log4j 2.x出现时间较晚,改进了前面的问题并更进一步做了很多特性提升,预期使用的人会越来越多。

而日志API,SLF4J API使用最为广泛,它不仅解决了通常意义上的日志框架切换问题,还提供了“桥接遗留的API”的功能,使项目内所有的日志输出被统一到了同一个日志实现中。

最后,我们列出了几个日志配置常见的异常,希望可以帮到读者。

关于转载

原创文章,转载请注明出处: http://www.ypk1226.com/2018/12/22/java/java-loging-framework/

参考的文章:

Java logging framework - Wikipedia
Apache log4j 1.2 -Short introduction to log4j
Cover Story: Log4j vs java.util.logging
Curious, why Ceki Gülcü (Log4J author) is no longer in team? | Hacker News
Which Java Logging Framework Has the Best Performance?
The State of Logging in Java
Bridging legacy APIs

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值