一、前言
日志是所有软件系统非常重要的一部分;良好统一的日志规范和严格执行会大大提高系统的可维护性、可用性、可靠性,并进而提高开发效率, 指导业务。
- 过程监控--通过分析程序的执行过程,用于验证程序的执行是否按照既定的方式运行。
- 问题定位--通过查看程序错误日志的详细信息以及产生位置,迅速定位问题产生的原因。
- 业务指导,通过统计和分析相关的业务、用户行为日志,对业务进行预测指导。
- 数据恢复--通过反向执行过程日志,可以将数据可以会滚到之前的状态。
- 健康检查以及系统优化--通过查看系统日志,确定程序运行的健康状况,调节系统参数,优化程序性能。
二、背景
猛地发现自己一直错误地把Sl4j和java.util.logging、Logback、Log4j理解为同一层次的概念。 继而查了相关的资料,站在更高一层,理解一下Java的日志管理相关的框架————Log4j、JUL、JCL、Slf4j、Logback 、Log4j2。
分为两个层次的概念:
- 日志框架(Implement):日志系统是日志的具体实现。如Log4j、java.util.Logging、Logback、Log4j2。
- 日志门面(Facade):为了解决多个日志系统的兼容问题,日志门面应运而生。如Commons logging和Slf4j。
三、Java常见日志库的历史
Logging frameworks出现之前(Java 1.3及以前),Java打日志依赖System.out.println(),System.err.println()或者 e.printStackTrace()。Debug日志被写入STDOUT流,错误日志被写入STDERR流。
Logging frameworks的出现: 先来一幅图从整体上看一下Java常见日志库的时间先后顺序:
1. Log4j
Log4j可以说是一个里程碑式的框架,它提出的一些基本理念,深深地影响了后来者,直至今天,这些理念也依然在被广泛使用:
- Logger--Logger负责捕捉事件并将其发送给合适的Appender。
- Appender--也被称为Handler,负责将日志事件记录到目标位置。在将日志事件输出之前,Appenders使用Layouts来对事件进行格式化处理。
- Layout--也被称为Formatter,它负责对日志事件中的数据进行转换和格式化。Layouts决定了数据在一条日志记录中的最终形式。
2. Java Util Log
Sun公司开始意识到JDK需要一个记录日志的特性。受Log4j的启发,Sun在Java 1.4版本中引入了一个新的API, 叫java.util.logging, 但是,JUL功能远不如Log4j完善,如果开发者要使用它,就意味着需要自己写Appenders(Sun称它为Handlers),而且,只有两个Handlers 可被使用:Console和File,这就意味着,开发者只能将日志写入Console和文件。
如前面所述,JUL在Java 1.4才被引入,在这之前,并没有官方的日志库供开发者使用。于是便有了很多日志相关的”轮子”。我想这应该是当前 会有如此多日志框架的一个很重要的原因。
3. Commons Logging
由于项目的日志打印必然依赖以上两个框架中至少一个,无论是jul还是Log4j,开发者必须去两个都配置。这时候,Apache的 Commons logging(Jakarta Commons Logging)
出现了。
它的主要作用是提供一个日志门面,使用者可以使用不同的日志实现。用户可以自由选择第三方的日志组件作为具体实现(Log4j或JUL), JCL会通过动态查找的机制,在程序运行时自动找出真正使用的日志库。JCL内部有一个Simple logger的简单实现,但是功能很弱。
4. Slf4j
JCL是在运行期间查找符合条件的日志类,然后使用类加载的方式加载指定的日志类。这种机制带来了不可预见的后果,如类加载问题, 这会让开发者遇到各种奇怪的问题,增加了调试的困难度。 也正是因为如此,Log4j 的作者发起了另一个项目,也就是 Slf4j(Simple Logging Facade for JAVA)。
Slf4j 同 JCL类似,也是提供了一个公共的接口,开发者只需要关注接口,不用关心下层实现。同时 Slf4j 的实现只要遵循这个接口, 就可以做到各个日志系统之间的无缝兼容。
5. Logback
Logback是Slf4j接口的一套具体实现,又是同一个作者,因而保证了其和Log4j相近的使用方式,也具有Slf4j的全部特性。
现在我们有了两个流行的 Log Facade,以及三个流行的 Log Implementation。Gülcü 是个追求完美的人,他决定让这些Log之间 都能够方便的互相替换,所以做了各种 Adapter 和 Bridge 来连接。
在这里需要注意不能搞出循环的桥接,比如下面这些依赖就不能同时存在:
- jcl-over-slf4j 和 slf4j-jcl
- log4j-over-slf4j 和 slf4j-log4j12
- jul-to-slf4j 和 slf4j-jdk14
6. Log4j2
Log4j2 是 Log4j、Slf4j、Logback 的作者的又一个作品,它对 Log4j 做了很大的改进,提供了多种现代特性,而且它的异步性能优越, 在分布式系统中性能要远好于 Logback,同时也支持 Slf4j 与 JCL。
Log4j2 也做了 Facade/Implementation 分离的设计,分成了 log4j-api 和 log4j-core。
现在好了,我们有了三个流行的Log Facade和四个流行的Log Implementation,这时候桥接关系的图如下图:
四、Java 日志框架对比
这里主要讲述一下几个日志框架(Log4j、JUL、Logback、Log4j2)的对比。
1. Log4j VS JUL
- 处理器
JUL包含4种具体的handler的实现,而Log4j则包括超过12个的appender实现。
JUL的handler足够用来进行基本的日志记录 - 他们允许你写入到一个buffer,一个console,一个socket,和一个file中。 Log4j的appenders,另一方面,大概覆盖了所有logging输出目的地你可以想到的。他们可以写到NT日志或者Unix syslog中,或者甚至发送Email。
- 格式化器
JUL包含了两个格式化类:XMLFormatter和SimpleFormatter。Log4j包含了对应的布局器:XMLLayout和SimpleLayout.Log4j还提供了 TTCCLayout
,它格式化LoggingEvents到富内容字符串,和HTMLLayout,它可格式化LoggingEvent到HMTL表格中。
2. Logback VS Log4j
Logback当前分成三个模块:logback-core,logback- classic和logback-access:
- logback-core是其它两个模块的基础模块。
- logback-classic是log4j的一个改良版本。此外logback-classic完整实现Slf4j API使你可以很方便地更换成其它日记系统如Log4j或 JDK14 Logging。
- logback-access访问模块与Servlet容器集成提供通过Http来访问日记的功能。
3. Log4j2 VS Logback
Log4j2是Log4j的升级版,与之前的版本Log4j 1.x相比、有重大的改进,在修正了Logback固有的架构问题的同时,改进了许多 Logback所具有的功能。log4j2与log4j1发生了很大的变化,不兼容。
- 性能
由于采用了更先进的锁机制和LMAX Disruptor库,Log4j2的性能优于Logback,特别是在多线程环境下和使用异步日志的环境下。
- 垃圾
Log4j2实现了”无垃圾”和”低垃圾”模式。Log4j2在记录日志时,能够重用对象(如String等),尽可能避免实例化新的临时对象, 减少因日志记录产生的垃圾对象,减少垃圾回收带来的性能下降。
Logback能够自动压缩/删除旧日志。
- 与Slf4j的适配
二者都能够适配Slf4j,Logback与Slf4j的适配应该会更好一些,毕竟省掉了一层适配库
五、Java 日志门面对比
这里主要对比一下日志门面Commons logging和Slf4j:
Common logging通过动态查找的机制,在程序运行时自动找出真正使用的日志库。由于它使用了ClassLoader寻找和载入底层的日志库, 导致了象OSGI这样的框架无法正常工作,因为OSGI的不同的插件使用自己的ClassLoader。 OSGI的这种机制保证了插件互相独立, 然而却使Apache Common-Logging无法工作。
Slf4j在编译时静态绑定真正的Log库,因此可以在OSGI中使用。另外,Slf4j 支持参数化的log字符串,避免了之前为了减少字符串拼接 的性能损耗而不得不写的if(logger.isDebugEnable()),现在你可以直接写:logger.debug(“current user is: {}”, user)。 拼装消息被推迟到了它能够确定是不是要显示这条消息的时候,但是获取参数的代价并没有幸免。
六、日志框架与日志门面组合
使用日志门面可以方便的切换具体的日志实现。而且如果依赖多个项目,使用了不同的Log Facade,还可以方便的通过Adapter 转接到同一个实现上。 因此为了考虑扩展性,一般我们在程序开发的时候,会选择使用commons-logging或者slf4j这些日志门面, 而不是直接使用log4j或者logback这些实现。
以下是几种比较常见的日志方案组合:
1. JCL + Log4j
需要的jar包:
- commons-logging
- log4j
2. Slf4j + Logback
需要的jar包:
- slf4j-api
- logback-core
- logback-classic(集成包)
3. Log4j2
Log4j2需要的jar分成2个:
- log4j-api: 作为日志接口层,用于统一底层日志系统
- log4j-core : 作为上述日志接口的实现,是一个实际的日志框架
4. JCL + log4j2
需要的jar包:
- commons-logging
- log4j-api
- log4j-core
- log4j-jcl(log4j2与commons-logging的集成包)
现在的方式JCL + Log4j组合在性能和部分功能上都比较弱,如果要改进可以考虑以下几点:
- 考虑性能和占位符等功能方面
推荐slf4j + logback方式 或者log4j2方式,这种方式对现有系统迁移改动较大,无论是代码内log声明还是配置文件上,而且slf4j不支持fatal打印;
- 考虑系统迁移性
推荐commons-logging+log4j2,代码不需要改动,只需要改动对应log4j配置文件即可,但是无法利用其占位符功能;
- 新系统搭建
不涉及到系统迁移的情况,新系统搭建可以采用纯log4j2方式,其既提供了接口也提供了实现,在性能上也得到了比较大提升。
七、NDC、MDC、ThreadContext
当处理多线程应用程序,特别是web服务时,跟踪事件可能会变得困难。当针对多个同时存在的多个用户生成日志记录时, 你如何区分哪个行为和哪个日志事件有关呢?如何两个用户没有成功打开一个相同的文件,或者在同一时间没有成功登陆,那么怎么处理日志记录?你可能需要一种方式来将日志记录和程序中的唯一标示符关联起来,这些标识符可能是用户ID,会话ID或者设备ID。而这就是NDC、MDC以及ThreadContext的用武之地。
NDC、MDC和ThreadContext通过向单独的日志记录中添加独一无二的数据戳,来创建日志足迹(log trails)。 这些数据戳也被称为鱼标记(fish tagging),我们可以通过一个或者多个独一无二的值来区分日志。这些数据戳在每个线程级别上进行管理,并且一直持续到线程结束,或者直到数据戳被删掉。例如,如果你的Web应用程序为每个用户生成一个新的线程,那么你可以使用这个用户的ID来标记日志记录。当你想在一个复杂的系统中跟踪特定的请求、事务或者用户,这是一种非常有用的方法。
1. NDC
NDC或者嵌套诊断上下文(Nested Diagnostic Context)是基于栈的思想,信息可以被放到栈上或者从栈中移除。 而栈中的值可以被Logger访问,并且Logger无需显示想日志方法中传入任何值。
- NDC.push()方法将值存储在栈中;
- NDC.pop()方法将一些项从栈中移除;
- NDC.remove()方法让Java回收内存,以免造成内存溢出。
2. MDC
MDC或者映射诊断上下文和NDC很相似,不同之处在于MDC将值存储在键值对中,而不是栈中。这样你可以很容易的在Layout中引用一个单独的键。
- MDC.put(key,value) 方法将一个新的键值对添加到上下文中;
- MDC.remove(key) 方法会移除指定的键值对;
- MDC.clear()方法将所有的键值对从MDC中移除,这样会降低内存的使用量,并阻止MDC在后面试图调用那些已经过期的数据。
Slf4j 只有 MDC,没有 NDC Logback内置没有实现NDC,但是slf4j-ext包提供了一个NDC实现,它使用MDC作为基础。
3. ThreadContext
Log4j版本2中将MDC和NDC合并到一个单独的组件中,这个组件被称为ThreadContext(线程上下文)。
ThreadContext可以看成是NDC和MDC的结合体,它分别用Thread Context Stack
和Thread Context Map
来表示NDC
和MDC
。
- ThreadContext.clearStack(),清除NDC;
- ThreadContext.clearMap(),清除MDC;
- ThreadContext.clearAll(),清除所有。
八、总结
本文从日志的好处讲起,先总体上概述了日志相关库分为:日志框架和日志门面;然后按照时间顺序列举了这些日志常见库; 随后对它们进行简单的对比;提出了几个常见的日之框架与日志门面的组合使用; 最后提到了NDC、MDC、ThreadContext的概念为下一篇文章提供一点理论基础。
针对每个库更详细的特性或者具体的使用步骤,大家可以自行去对应的官网查看。