Log4j

在 2001 年以前,Java 是没有日志库的,打印日志全凭 System.out 和 System.err,有非常大的局限性。此时有一位叫做 Ceki 的巨佬站出来,说这个不好用,接着在 2001 年掏出了 Log4j,直接引入使用即可,用起来也确实比 System 系列香,Log4j 一度成为业内的日志标准。

后来 Log4j 成为 Apache 项目,Ceki 也加入 Apache 组织,Apache 还曾经建议 Sun 公司引入 Log4j 到 Java 的标准库中,但被 Sun 拒绝了。
image.png
Log4j 有三个主要的组件:Loggers(记录器),Appenders(输出源)和 Layouts(布局)。这里可简单理解为日志类别、日志要输出的地方以及日志以何种形式输出。综合使用这三个组件可以轻松地记录信息的类型和级别,并可以在运行时控制日志输出的样式和位置。

Log4j 的架构大致如下:
image.png
当使用 Log4j 输出一条日志时,Log4j 会自动通过不同的 Appender(输出源)把同一条日志输出到不同的目的地。在输出日志的过程中,通过 Filter 来过滤哪些 log 需要被输出,哪些 log 不需要被输出。在 Loggers(记录器)组件中,级别分五种:DEBUG、INFO、WARN、ERROR 和 FATAL。Log4j 只输出级别不低于设定级别的日志信息。最后通过 Layout 来格式化日志信息,例如,自动添加日期、方法名等信息。

JUL

原来 Sun 公司自己也搞一个日志框架,在 2002 年 2 月发布 JDK 1.4 版本时,推出了自己的日志标准库 JUL (java.util.logging),代码其实是照着 Log4j 抄的,而且还没抄好,还是在 JDK 1.5 优化以后性能和可用性才有所提升。

JCL

由于在 JUL 出来以前,Log4j 就已经成为一项成熟的技术,使得 Log4j 在选择上占据了一定的优势。但 JUL 毕竟是 JDK 自带的,凭借其影响力也有很多人使用 JUL。于是现在市面上有了两款 Java 的日志标准库,分别是 Log4j 与 JUL,但这两个框架并不兼容,如果想把 Log4j 替换成 JUL,因为其 API 完全不同,就需要改动代码。此时 Apache 组织就想统一抽象日志标准接口规范,就像 JDBC 统一数据库访问层那样,让其他日志标准库去实现它的抽象接口,这样日志操作都是统一的接口。
image.png
于是在 JUL 刚出来不久,2002 年 8 月 Apache 就推出了 JCL(Jakarta Commons Logging),它只是一个日志抽象层,支持运行时动态加载日志组件,JCL 会先在 ClassLoader 中进行查找,如果能找到 Log4j 则默认使用 Log4j 实现,如果没有则使用 JUL 实现,再没有则使用 JCL 内部默认提供的 Simple Log 来实现。
image.png

Slf4j

2006 年巨佬 Ceki(Log4j 的作者)因为一些原因离开了 Apache 组织,之后 Ceki 觉得 JCL 不好用,自己撸了一套新的日志标准接口规范 Slf4j(Simple Logging Facade for Java),也可以称为日志门面,很明显 Slf4j 是对标 JCL。但由于 Slf4j 这套接口规范出来的较晚,JUL 和 Log4j 都是没有实现 Slf4j 的,于是巨佬 Ceki 后续提供了一系列的桥接包来帮助 Slf4j 接口与其他日志库建立关系,这种方式称为桥接设计模式
image.png
有了桥接包配合,其他的问题都迎刃而解,先看看有那些问题吧。
image.png
从上图可以看出,不同时期的项目使用的日志标准库是不一样的,以 Slf4j 接口作为划分线,考虑两个问题,一个是 Slf4j 之前的项目怎么统一日志标准,另一个是 Slf4j 之后的项目怎么统一日志标准。

先来看 Slf4j 之后的项目怎么统一日志标准,项目 D、E 都使用 Slf4j 接口,首先在代码层已经统一了,如果要做到日志标准统一也十分简单,直接替换日志标准库与对应的桥接包即可,如下图所示
image.png
再来看 Slf4j 之前的项目怎么统一日志标准,项目 A、B、C 都使用了不同的日志标准,所以它们的 API 并不一样,如果要统一标准就要改代码,这样侵入性太强了,有什么办法能在不改代码的情况下,让 A、B、C 项目统一日志标准吗?

办法当然有,Slf4j 接口能通过桥接包勾搭上具体的日志标准库,为什么日志标准库不能通过桥接包勾搭 Slf4j 接口呢?要想把 A、B、C 项目都统一成 Log4j 日志输出,只需要做如下调整即可:
image.png

Logback

Ceki 巨佬觉得市场上的日志标准库都是间接实现 Slf4j 接口,也就是说每次都需要配合桥接包,因此在 2006年,Ceki 巨佬基于 Slf4j 接口撸出了 Logback 日志标准库,做为 Slf4j 接口的默认实现,Logback 也十分给力,在功能完整度和性能上超越了所有已有的日志标准库。

Logback 可以认为是 Log4j 的改进版本,更加推荐使用,并且截止目前 Spring Boot 2.2.6.RELEASE 版本的日志框架也用的是 Logback。
image.png
查看 Spring Boot 的 Maven 依赖树,可以发现 spring-boot-starter-logging 模块帮我们自动引入了 logback-classic(包含了 SLF4J 和 Logback 日志框架)和 SLF4J 的一些适配器。其中 log4j-to-slf4j 用于实现 Log4j2 API 到 SLF4J 的桥接,jul-to-slf4j 则是实现 java.util.logging API 到 SLF4J 的桥接。

Log4j2

自从 Logback 出来后,可以说 Slf4j + Logback 的组合如日中天,强力冲击着 JCL + Log4j 的组合,Apache 眼看有被 Logback 反超的势头。于是在 2012 年的时候,Apache 直接推出新项目 Log4j2(不兼容 Log4j),Log4j2 全面借鉴了 Slf4j + Logback 组合,因为 Log4j2 不仅具有 Logback 的所有特性,还做了分离设计,分为 log4j-api 和 log4j-core,前者是日志接口,后者是日志标准库,并且 Apache 也为 Log4j2 提供了各种桥接包。

与 Logback 类似,Log4j2 也提供了对 Slf4j 的支持,可以动态加载日志配置,并支持高级过滤选项。除了这些特性外,它还允许基于 Lambda 表达式对日志语句进行惰性计算,为低延迟系统提供异步日志记录器,并提供无垃圾模式以避免垃圾收集器操作引起的任何延迟。

最重要的是,Log4j2 相比 Logback 的性能更加强劲,在多线程场景下,Log4j2 的吞吐量比 Logback 高出了 近 10 倍,且延迟降低了几个数量级。下图来自 Log4j2 官方测试数据:
image.png
Log4j2 除了提供 Async Append 的异步实现外,还提供了 Async Log 的异步实现,其中 Async Append 异步实现方式和 Logback 的异步实现差不多,使用的是 ArrayBlockingQueue,对于阻塞队列,多线程应用程序在尝试使日志事件入队时通常会遇到锁争用。而 Async Log 则基于 LMAX Disruptor 库,使用无锁数据结构实现了一个高性能的异步记录器,具有更多线程的应用程序可以记录更多的日志。

可通过如下系统配置开启全异步(all async)模式:

  1. -Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

官方测试链接:https://logging.apache.org/log4j/log4j-2.3/manual/async.html

总结

到目前为止,Java 日志体系可以总结为下图所示:
1.png
其中 SLF4J 实现了三种功能:

  • 一是提供了统一的日志门面 API,即图中紫色部分,实现了中立的日志记录 API。


  • 二是桥接功能,即图中蓝色部分,用来把各种日志框架的 API(图中绿色部分)桥接到 SLF4J API。这样一来,即便你的程序中使用了各种日志 API 记录日志,最终都可以桥接到 SLF4J 门面 API。


  • 三是适配功能,即图中红色部分,可以实现 SLF4J API 和实际日志框架(图中灰色部分)的绑定。SLF4J 只是日志标准,我们还需要一个实际的日志框架。日志框架本身没有实现 SLF4J API,所以需要有一个前置转换。由于 Logback 是直接实现了 SLF4J API 的,因此不需要进行适配。

需要理清楚的是,虽然我们可以使用 log4j-over-slf4j 来实现 Log4j 桥接到 SLF4J,也可以使用 slf4j-log4j12 实现 SLF4J 适配到 Log4j,也把它们画到了一列,但是它不能同时使用它们,否则就会产生死循环。jcl 和 jul 也是同样的道理。