几乎所有的应用程序中,都会用到日志框架来记录程序的运行信息。日志虽然不影响应用程序的运行结果,但是没有日志的应用程序是不健全,不完整的。良好的日志系统可以帮助我们快速的定位到程序问题,还可以帮助我们分析系统的缺陷,比如近几年火起来的ELK。目前主流的日志框架也挺多,然而回顾以往,在开发过程中也没有深入了解,只是简单的引入一个依赖包,编码时log打印就完事了,不禁感慨,工具做的越来越便捷好用的同时,我们也不要忘了去探究更深层次的实现。
本文以探究java日志框架体系为出发点,回顾一下日志系统的发展历程,了解一下目前主流的几种日志框架的区别及特性,最后从使用的角度分析日志框架在系统中的应用。
参考链接
意义
- 开发调试: 在合适代码位置打印日志,并将程序运行中的一些参数信息也打印出来,可以更快的定位问题,也能在解决时提供必要的上下文信息。
- 系统维护: 记录大部分的异常信息,通过收集日志信息可以对系统的运行状态进行实时监控预警。
- 数据分析: 配合日渐成熟的大数据技术,对海量日志进行分析,可以获取有助于战略角色的信息,比如最火的ELK框架。
发展历程
其实Java的日志框架体系是很混乱的。早期 Java 日志框架没有制定统一的标准,开发时经常会引入第三方的库,这第三方库中又可能使用了不同的日志框架,那么这个应用程序中其实包含了多种日志框架。
比如经常会遇到,在项目启动的时候会出现以下红色字体的日志,提示有多个日志框架绑定到了slf4j,就是因为找到了多个日志框架的实现。关于这个问题如何解决会在后文中提到。
接下来看一下日志框架都有哪些,以及发展过程中经历了哪些阶段
在 jdk1.3 之前,还没有内置的日志功能,只能使用原始的 System.out.println()
, System.err.println()
或者 e.printStackTrace()
。通过把 debug 日志写到 StdOut 流,错误日志写到 ErrOut 流,以此记录应用程序的运行状态。这种原始的日志记录方式缺陷明显。后来慢慢出现了各种日志框架。整个发展历程也经历了如下五个阶段。
(一)Log4j
Apache基金会的一个项目。1999年由Ceki Gülcü 创建,Log4j几乎成为了Java日志框架的实际标准。
(二)JUL
Log4j 作为 Apache 基金会的一员,Apache 希望将 Log4j 引入 jdk,不过被 sun 公司拒绝了。随后,sun 模仿 Log4j,在 jdk1.4 中引入了 JUL(java.util.logging)。
(三)Commons Logging
如果有更换日志组件的想法,如 log4j 换成 JUL,因为 API 完全不同,就需要改动代码。为了将日志接口与实现解耦,2002 年 Apache 推出了 JCL(Jakarta Commons Logging)。Commons Logging 定义了一套日志接口,具体实现则由 Log4j 或 JUL 来完成。Commons Logging 基于动态绑定来实现日志的记录,在使用时只需要用它定义的接口编码即可,程序运行时会使用 ClassLoader 寻找和载入底层的日志库,因此可以自由选择由 log4j 或 JUL 来实现日志功能。
(四)Slf4j & Logback
Ceki Gülcü 与 Apache 基金会关于 Commons-Logging 制定的标准存在分歧,后来, Ceki Gülcü 离开 Apache 并先后创建了 Slf4j 和 Logback 两个项目。Slf4j 是一个日志门面,只提供接口,可以支持 Logback、JUL、log4j 等日志实现,Logback 提供具体的实现,它相较于 log4j 有更快的执行速度和更完善的功能。当前分为三个目标模块:
- logback-core:核心模块,是其它两个模块的基础模块
- logback-classic:是log4j的一个改良版本,同时完整实现 SLF4J API,可以很方便地更换成其它日记系统如log4j 或 JDK Logging
- logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能,是logback不可或缺的组成部分
(五)Log4j2
为了维护在 Java 日志江湖的地位,防止 JCL、Log4j 被 Slf4j、Logback 组合取代 ,2014 年 Apache 推出了 Log4j 2。Log4j 2 与 log4j 不兼容,经过大量深度优化,其性能也有显著提升。
分类
上文中提到的日志框架有 Log4j,Log4j2,Commons Logging,Slf4j,Logback,JUL。可以分为两种类型:门面日志 和 日志系统。
- 日志门面: 只提供日志相关的接口定义,即相应的 API,而不提供具体的接口实现。日志门面在使用时,可以动态或者静态地指定具体的日志框架实现,解除了接口和实现的耦合,使用者可以灵活地选择日志的具体实现框架。
- 日志系统: 与日志门面相对,它提供了具体的日志接口实现,应用程序通过它执行日志打印的功能。
框架
在整个日志框架中主要包括日志门面、日志适配器、日志库三个部分,它们之间的关系如下图所示:
日志门面
使用日志门面的目的就是将接口和实现解耦,使用的就是设计模式中的门面模式,而门面模式的核心思想就是: 外部客户端与一个子系统的通信,必须通过一个统一的外观对象进行,使得子系统更易于使用,其本质就是为子系统中的一组接口提供一个统一的高层接口, 在这种日志门面下,使用者可以随意切换底层使用的日志系统了。
比如有这样的使用场景:系统开发时使用的是 Logback,引入了第三方A依赖,A依赖中使用的日志系统为 Log4j,又引入了 B.jar,而 B.jar 中使用的日志系统为 JUL。
在这种场景下,如果每使用到一种日志系统,我们的系统都需要同时支持和维护的话,其繁琐程度不言而喻。为了解决这个问题,可以引入一个适配层,由适配层决定具体使用哪一种日志系统,应用程序中的调用者只管打印日志,而不必关心日志是如何被打印出来的,这样就避免了需要维护复杂日志系统的问题。Slf4j 和 Commons-Logging 就是适配层,而 JUL、Log4j 和 Logback 等就是打印日志的具体实现。
日志适配器
Slf4j 的作者 Ceki Gülcü 当年因为觉得Commons-Logging的API设计以及性能都不够好,因而设计了 Slf4j。为了 Slf4j 能够兼容各种类型的日志系统实现,还设计了相当多的 adapter 和 bridge 来连接:
日志库
日志库就是对日志门面中接口的具体实现,市面上的日志库有很多,比如log4j2,logback等等。比较常用的组合使用方式是:
- Slf4j 与 Logback 组合使用,Logback 也必须配合 Slf4j 使用。由于 Logback 和 Slf4j 是同一个作者,兼容性非常好。
- Commons Logging 与 Log4j 组合使用。
看上面的组合方式也看得出来,一个门面搭配一个日志库使用,那为什么会出现两个门面呢?其实曾经Apache 试图说服 Log4j 以及其它的日志来按照 Commons-Logging 的标准编写,但是由于 Commons-Logging 的类加载机制在实际应用中存在问题(它使用 ClassLoader 寻找和载入底层的日志库),实现起来也不友好,因此 Log4j 的作者便开发了 Slf4j,与 Commons-Logging 两分天下。
对于上面两种组合方式,logback对比log4j有如下的几个优点:
- Slf4j 实现机制决定 Slf4j 限制较少,使用范围更广。相较于 Commons-Logging,Slf4j 在编译期间便静态绑定本地的 Log 库,其通用性要好得多。
- Logback 拥有更好的性能。Logback 声称:某些关键操作,比如判定是否记录一条日志语句的操作,其性能得到了显著的提高,这个操作在 Logback 中只需 3 纳秒,而在 Log4j 则需要 30 纳秒。
- Slf4j 支持参数化,可以使用{}占位符
- Logback 的所有文档是免费提供的,Log4j 只提供部分免费文档而需要用户去购买付费文档。
- MDC (Mapped Diagnostic Contexts) 用 Filter,将当前用户名等业务信息放入MDC 中,在日志 format 定义中即可使用该变量。具体而言,在诊断问题时,通常需要打出日志。如果使用 Log4j,则只能降低日志级别,但是这样会打出大量的日志,影响应用性能;如果使用 Logback,通过配置log的pattern即可将MDC中的业务信息跟随日志打印在同一行。
应用
如果是新项目使用日志框架,建议使用 Slf4j 与 Logback 组合,可通过如下配置进行集成:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.0-alpha1</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.10</version>
</dependency>
对于已有工程,需要根据所使用的日志库来确定门面适配器从而使用 Slf4j。Slf4j 的设计思想比较简洁,使用了 Facade 设计模式,Slf4j 本身只提供了一个 slf4j-api.jar 包,主要是日志的抽象接口,jar 包中本身并没有对抽象出来的接口做实现。对于不同的日志实现方案(例如 Logback,Log4j 等),封装出不同的桥接组件(例如 logback-classic-version.jar,slf4j-log4j12-version.jar),这样使用过程中可以灵活地选取自己项目里的日志实现。
举例说明,如果已有工程中使用了 Log4j 日志库,可通过如下配置进行集成:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.0-alpha1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.32</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
Slf4j 与其它日志组件调用关系图如下:
具体的接入方式如下:
如果在同一项目中使用不同的组件时,会出现不同组件依赖的日志组件不一致的情况,这就需要统一日志方案,统一使用 Slf4j,把他们的日志输出重定向到 Slf4j,然后 Slf4j 又会根据绑定器把日志交给具体的日志实现工具。Slf4j 带有几个桥接模块,可以重定向 Log4j,JCL 和 java.util.logging 中的 Api 到 Slf4j。
举例说明:如果老代码中直接使用了 Log4j 日志库接口打印日志,需引入如下配置:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.32</version>
</dependency>
桥接方式如下:
常见问题
日志组件冲突
如第二章提到的那个问题,Spring 本身的日志实现使用了 Commons Logging,如果想使用 Slf4j+Logback 组合,这时候需要在项目中将 Commons Logging 排除掉,通常会用到以下 3 种方案,各有利弊,可以根据项目的实际情况选择最适合自己项目的解决方案:
- 方案一:采用 maven 的 exclusion 方案,如果有多个组件都依赖了 commons-logging,则需要在很多处增加 exclusion,比较繁琐。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.14</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
- 在 maven 声明 commons-logging 的 scope 为 provided,在调试代码时有可能导致 IDE 将 commons-logging 放置在 classpath 下,从而导致程序运行时出现异常。
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
<scope>provided</scope>
</dependency>