Java 的日志门面与实现

本文深入探讨Java日志框架的历史与发展,重点分析SLF4J、Log4j、Logback等框架的特性与应用场景,以及如何避免日志实现冲突。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

介绍

Java 的日志框架伴随 Java 的发展,到目前为止也有着丰富的历史。在日常研发过程中,或多或少遇到过如下问题:

  1. SLF4J 日志实现冲突
  2. 日志打印不受 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 应用,你也应该面向门面(接口)编程,选择合适的日志实现,同时还需要适当排除会引起冲突的日志组件(一般是由其他组件间接引入的)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值