Java Logging Framework 现状
某日笔者心情不错,写代码时没有复制粘贴,打算手敲logger的相关代码,在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):
groupId | artifactId | 使用次数 | 占比 | 最后更新日期 |
---|---|---|---|---|
org.slf4j | slf4j-api | 33401 | 2018年3月21日 | |
ch.qos.logback | logback-classic | 14366 | 2018年2月11日 | |
log4j | log4j | 13931 | 2012年5月26日 | |
commons-logging | commons-logging | 8278 | 2014年7月5日 | |
org.slf4j | slf4j-simple | 7368 | 2018年3月21日 | |
org.apache.logging.log4j | log4j-core | 3709 | 2018年7月30日 | |
org.apache.logging.log4j | log4j-api | 2930 | 2018年7月30日 | |
ch.qos.logback | logback-core | 2925 | 2018年2月11日 | |
org.jboss.logging | jboss-logging | 1734 | 2018年2月14日 | |
org.clojure | tools.logging | 1258 | 2018年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-classic | Log4j 1.x项目发起人离开apache后,主导开发出的一套日志框架实现,默认实现slf4j-api,现在已经是最热门的日志框架。 |
Log4j 1.x | 又名log4j12,意为1.x的最后一个版本1.2,apache早期推出的日志实现,奠定了Java日志框架的基础,大量老项目使用了该框架,目前已停止更新。 |
log4j 2.x | apache最近几年新推出的日志框架,功能齐全、性能强劲、文档齐全。 |
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 |
---|---|---|
root | Proot | Proot |
X | Px | Px |
X.Y | none | Px |
X.Y.Z | Pxyz | Pxyz |
Appender的可叠加性
子级Logger会从父级继承关联的Appender作为自己的Appender,与子级自身关联的Appender叠加起来(即Appender使是相同的),距离如下:
Logger名称 | 叠加的Appender | 叠加性是标识 | 输出目标 |
---|---|---|---|
root | A1 | 不可配置 | A1 |
x | A-x1, A-x2 | true | A1, A-x1, A-x2 |
x.y | none | true | A1, A-x1, A-x2 |
x.y.z | A-xyz1 | true | A1, A-x1, A-x2, A-xyz1 |
security | A-sec | false | A-sec |
security.access | none | true | A-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ü着实是个牛人,笔者特地找了其本人的两张照片放在下面(侵删)。
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-api和Apache Commons Logging(又名JCL,Jakarta Commons Logging)。一般情况下,通过API调用日志框架实现的流程如下:
3.1 JCL(Jakarta Commons Logging)(始于2002)
JCL是Apache与2002年发布的一个日志API,最后一次更新时间为2014年07月11日, 使用它可以隔离具体的日志实现,使底层日志实现代码不侵入项目,方便进行切换。当引入了common-logging之后,它将自动查找当前classpath下包含的,用户不需要任何配置。查找的顺序如下:
- 寻找配置文件中是否为org.apache.common.logging.Log配置的值
- 是否包含log4j
- 是否包含JDK Logger
- 使用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