介绍
Java 的日志框架伴随 Java 的发展,到目前为止也有着丰富的历史。在日常研发过程中,或多或少遇到过如下问题:
- SLF4J 日志实现冲突
- 日志打印不受 logback.xml 或 log4j2.xml 控制
这篇文章是根据我的研发经验总结的关于 Java 日志门面与实现的关系,相信它可以帮助大家解决研发中遇到的关于日志框架选择的问题。
本文的切入视角是日志门面与实现之间的组合关系,如果你遇到某个具体的日志实现的配置问题,本文无法帮到你什么,你应该查阅对应的官方文档解决问题。
日志框架介绍
在 Java 里通常强调面向接口编程,所以在日志领域也被设计成了门面(接口)与实现。由于存在多种门面和实现的组合,因此也存在这多种适配器。
一般来说,开发人员面向接口编程,然后根据需要选择底层的实现或适配器。
以下从不同的角度来看这些日志框架之间的关系。
常见门面(接口/API):
- slf4j-api
- log4j-api
- JCL (commons-logging 1.2)
- JUL
常见实现:
- slf4j-simple
- logback
- log4j-core
- log4j 1.2
常见适配器:
- log4j-slf4j-impl: 让 Log4j2 作为 SLF4J 的实现
- log4j-to-slf4j: 让 Log4j2 输出到 SLF4J / 让 SLF4J 作为 Log4j2 的实现
- log4j-1.2-api: 提供与 Log4j 1.2 同名的 API, 让其输出到 Log4j2
- log4j-over-slf4j: 提供与 Log4j 1.2 同名的 API, 让其输出到 SLF4J
- jcl-over-slf4j: 提供与 JCL 同名的 API, 让其输出到 SLF4J
- spring-jcl: 提供与 JCL 同名的 API, 让其输出到 SLF4J 或 Log4j2
- slf4j-jcl: 让 SLF4J 作为 JCL 的实现
- log4j-jcl: 让 Log4j2 作为 JCL 的实现
- slf4j-log4j12: 让 Log4j 1.2 作为 SLF4J 的实现
下面介绍几个重要的日志框架。
Log4j 1.2
-
Log4j 1.2 创建于 1996 年,算得上是蛮荒时代了。
-
Log4j 1.2 出现了性能或设计上的缺陷,逐渐被其他日志框架取代。
Log4j 1.2 自身是一个打印日志的实现,它没有自己的门面层,一般需要搭配其他门面使用。如果你直接面向 Log4j 1.2 编程的话,那么你就强依赖 Log4j 1.2 了,将来你不容易摆脱它。
JCL
JCL = Jakarta Commons Logging
- 当年日志实现众多,JCL 作为门面应运而生
- JCL 现在也出现了性能或设计问题,SLF4J 开始取代它
- JCL 在很多项目里被直接或间接引用
SLF4J
我封它为目前 Java 应用打印日志的实际门面标准。
Logback
一个 SLF4J 的实现,一般我们对日志实现不做深入了解。
Log4j2
Log4j 1.2 的后继,号称高性能,经常搭配 Disruptor 使用。
由于是新设计的日志框架,因此它自带门面与实现,从而用户不用担心强依赖 Log4j2 的实现。
发现机制
日志门面是如何找到日志实现的呢?总的来说有 2 个办法:
- ServiceLoader 服务发现机制
- 静态绑定机制 (又称二进制兼容机制,通过提供同类名、同方法签名方式来做到)
SLF4j
- SLF4J <= 1.7 使用的是静态绑定机制
- SLF4j >= 1.8 使用的是 ServiceLoader 机制
Log4j2
使用的是 ServiceLoader 机制
JCL
JCL 支持从环境变量、配置文件强制干预日志实现类的选择。
默认情况下 JCL 使用ServiceLoader 机制寻找日志实现。
由于 JCL 已经不再发展了,为了让已经使用 JCL 的代码能够无缝转换到新的日志实现上,一些框架提供了如下的组件:
- jcl-over-slf4j: 提供 JCL 二进制兼容的类,将 JCL 的逻辑转发到 SLF4J上
- spring-jcj: 提供 JCL 二进制兼容的类,将 JCL 的逻辑转发到 SLF4J/Log4j2 上
- log4j-jcl: 通过 ServiceLoader 机制让 Log4j2 作为 JCL 的实现
几个组合场景
为什么很多组件自己包装 Logger
你可以在你的项目里搜索一下包含 Logger 关键字的类,肯定搜出很多结果:
- mybatis
- jboss-logging
- reactor
- 各式各样 …
这些包是闲着没事吗?又自己造了一个门面,把常见的日志门面又适配了一遍。
根据经验猜测它们的目的是:
- 减少外部依赖,做到完全独立
- 为了拦截打印日志的行为,比如每次 log 时往 MDC 里放一些东西。。。
- 为了能够具体控制日志输出
一般来说,根据SLF4J 官网说的,不推荐这么做,这样做只会把事情搞复杂。
但关于第三点我展开说一下,一般情况下日志的实现与配置是由最终用户去配的,但某些组件(比如公司内部的 RPC框架)想显式控制自己的日志打印到哪里,用什么pattern,如果这都交给用户去配的话,那很难保证所有用户都按照该组件的想法去配置,大家打印日志的位置、格式可能是五花八门的。因此这些框架才想说自己去识别日志实现,根据不同的日志实现动态创建不同的 Logger,并进行配置,从而获得对自己的日志输出的控制力。如果你有这样的需求那你不妨看一下这个开源项目 https://github.com/sofastack/sofa-common-tools 它可以帮会组你做到这点。
你需要提供3个常见的日志实现的配置文件,该框架会根据运行时生效的日志框架让配置文件在运行时生效。并且使用该框架创建出来的 Logger 具有隔离性,你不用担心框架内部日志与用户的日志混乱在一起。
实现原理也很简单,就是自己手动 new 了 logger 并配置,这里的实现比较麻烦,因为要适配各种框架的各种版本,但原理是简单的。
空依赖
重点看一下上面关于 JCL 的部分,可以发现它很容易发生类冲突,不确定哪个 jar 里的 JCL 会最终生效。
在实践中我遇到有一个组件它依赖了 JCL 的加载行为,对 logger 类做了判断,因此当 spring-jcl 实际生效时,那个组件原有的行为就失效了。这显然是一个很不好的行为。
一般在最佳实践中中,是需要将原生 JCL 排除掉,然后引入 jcl-over-slf4j 将实现转发到 SLF4J。
但如何将整个项目里的 JCL 都排除掉呢?这是很难的,你可以使用 dependencyManagement 挨个排除 JCL。但工作量巨大,而且依赖变化后也需要跟着变化。
我见过一种做法是官方会提供一个 version=99(或者类似的)JCL 包,我们通过 dependencyManagement 将 JCL 的版本强制覆盖成上述版本,如此依赖,我们没有阻止引入 JCL 的包,但引入的 JCL 包必定是一个空包。从而我们保证 Java 最终会加载其他的 JCL 实现。
最佳实践
- 如果你正在开发 SDK,那么请一定要面向门面(接口)编程,最简单的做法是面向 SLF4J 的 API 进行编程
- 如果你正在开发 SDK,并且想要显式控制日志的输出(位置以及格式),那么你可以使用这个开源项目 https://github.com/sofastack/sofa-common-tools
- 其余情况你应该是在开发能直接运行的 Java 应用,你也应该面向门面(接口)编程,选择合适的日志实现,同时还需要适当排除会引起冲突的日志组件(一般是由其他组件间接引入的)