探索java世界中的日志奥秘
java日志简单介绍
对于一个应用程序来说日志记录是必不可少的一部分。线上问题追踪,基于日志的业务逻辑统计分析等都离不日志。JAVA领域存在多种日志框架,目前常用的日志框架包括Log4j,Log4j 2,Commons Logging,Slf4j,Logback,Jul等。
一、java日志发展史
二、java 常用日志框架类别介绍
三、门面、实现、桥接
四、commons logging vs Slf4j
五、 log4j vs logback
六、 slf4j 源码解读
七、logback
八、MDC
一、java日志发展史
• 1996年早期,欧洲安全电子市场项目组决定编写它自己的程序跟踪API(Tracing API)。经过不断的完善,这个API终于成为一个十分受欢迎的Java日志软件包,即Log4j。后来Log4j成为Apache基金会项目中的一员。
• 期间Log4j近乎成了Java社区的日志标准。据说Apache基金会还曾经建议sun引入Log4j到java的标准库中,但Sun拒绝了。
• 2002年Java1.4发布,Sun推出了自己的日志库JUL(Java Util Logging),其实现基本模仿了Log4j的实现。在JUL出来以前,log4j就已经成为一项成熟的技术,使得log4j在选择上占据了一定的优势。
• 接着,Apache推出了Jakarta Commons Logging,JCL只是定义了一套日志接口(其内部也提供一个Simple Log的简单实现),支持运行时动态加载日志组件的实现,也就是说,在你应用代码里,只需调用Commons Logging的接口,底层实现可以是log4j,也可以是Java Util Logging。
• 后来(2006年),Ceki Gülcü不适应Apache的工作方式,离开了Apache。然后先后创建了slf4j(日志门面接口,类似于Commons Logging)和Logback(Slf4j的实现)两个项目,并回瑞典创建了QOS公司,QOS官网上是这样描述Logback的:The Generic,Reliable Fast&Flexible Logging Framework(一个通用,可靠,快速且灵活的日志框架)。
• 现今,Java日志领域被划分为两大阵营:Commons Logging阵营和SLF4J阵营。
Commons Logging在Apache大树的笼罩下,有很大的用户基数。但有证据表明,形式正在发生变化。2013年底有人分析了GitHub上30000个项目,统计出了最流行的100个Libraries,可以看出slf4j的发展趋势更好:
• Apache眼看有被Logback反超的势头,于2012-07重写了log4j 1.x,成立了新的项目Log4j 2。Log4j 2具有logback的所有特性。
二、java常用日志框架类别介绍
• Log4j Apache Log4j是一个基于Java的日志记录工具。现在则是Apache软件基金会的一个项目。 Log4j是几种Java日志框架之一。 Log4j应该说是Java领域资格最老,应用最广的日志工具。从诞生之日到现在一直广受业界欢迎。Log4j是高度可配置的,并可通过在运行时的外部文件配置。它根据记录的优先级别,并提供机制,以指示记录信息到许多的目的地,诸如:数据库,文件,控制台,UNIX系统日志等。
• Log4j 2 Apache Log4j 2是apache开发的一款Log4j的升级产品。
• Logback 一套日志组件的实现(slf4j阵营)。
• Jul (Java Util Logging),自Java1.4以来的官方日志实现。JDK1.4开始,通过java.util.logging提供日志功能。它能满足基本的日志需要,但是功能没有Log4j强大,而且使用范围也没有Log4j广泛。
• Commons Logging Apache基金会所属的项目,是一套Java日志接口,之前叫Jakarta Commons Logging,后更名为Commons Logging。
• Slf4j 类似于Commons Logging,是一套简易Java日志门面,本身并无日志的实现。(Simple Logging Facade for Java,缩写Slf4j)。
这个些所有的日志们都要归功于一个人 Ceki Gülcü !!!
三、门面、实现、桥接
经历了上述的发展,现在使用日志框架时往往会涉及三个层面的东西。
· 门面
· Slf4j: The simple logging facade for java.
· JCL: Jakarta Commons Logging.
· 实现类
· log4j-1.2
· log4j-2.x
· logback
· jul: java.util.logging
· …
· 桥接包
· SLF4J LOG4J 12 Binding
· JUL To SLF4J Bridge
· JCL 1.1.1 Implemented Over SLF4J ??
· SLF4J JDK14 Binding
· Apache Log4j Commons Logging Bridge
· …
门面主要只负责定义接口,实现类才负责具体的编码工作。
为什么要定义门面呢? 依赖接口而不依赖实现
桥接包顾名思义就是桥接门面和实现类。比如SLF4J LOG4J 12 Binding这个桥接包可以使Slf4j和log4j1.2结合起来正常工作。一个已经成型的系统如果使用了这个模式,底层又想将log4j1.2换成log4j2.0实现,则只需要替换实现包为log4j2.x以及桥接包为 Log4j 2 SLF4J Binding。
四、commons logging vs Slf4j
我们先看一下java日志框架之间的关系
• Commons Logging和Slf4j是日志门面(门面模式是软件工程中常用的一种软件设计模式,也被称为正面模式、外观模式。它为子系统中的一组接口提供一个统一的高层接口,使得子系统更容易使用)。log4j和Logback则是具体的日志实现方案。可以简单的理解为接口与接口的实现,调用这只需要关注接口而无需关注具体的实现,做到解耦。
• 比较常用的组合使用方式是Slf4j与Logback组合使用,Commons Logging与Log4j组合使用。
• Logback必须配合Slf4j使用。由于Logback和Slf4j是同一个作者,其兼容性不言而喻。(https://stackoverflow.com/questions/10117788/how-to-setup-commons-logging-to-use-logback)
Commons logging实现机制
Commons logging是通过动态查找机制,在程序运行时,使用自己的ClassLoader寻找和载入本地具体的实现。详细策略可以查看commons-logging-*.jar包中的org.apache.commons.logging.impl.LogFactoryImpl.java文件。
Slf4j实现机制
Slf4j在编译期间,静态绑定本地的LOG库。它是通过查找类路径下org.slf4j.impl.StaticLoggerBinder,然后绑定工作都在这类里面进行。
静态绑定 & 动态绑定
静态绑定又称编译时绑定,动态绑定又称运行时绑定。
JCL作为第一个log接口框架,使用了基于反射的动态绑定的方法,原理很简单,预先定义好支持的log实现的工厂类的全路径到一个数组中,遍历这个数组,调用Class.forName依次尝试寻找各个log实现,如果当前class loader没找到,就去父class loader去找,直到找到任意一个实现为止。
这种方法有致命的缺陷,这也正是SLF4J诞生的原因。Java EE的web容器,为了实现servlet规范中同一个容器中不同web app之间、web app和web容器之间的隔离,都使用的自己实现的class loader,逻辑和标准的class loader不同,导致一系列的无法正常发现log实现库的问题。
Taxonomy of class loader problems encountered when using Jakarta Commons Logging
这篇文章做了非常详尽的分析解释,文章的作者正是log4j和SLF4J的作者Ceki Gülcü,有兴趣的同学可以阅读。
另外一个小改进:
用 JCL 输出一个 debug 级别的 log:
logger.debug("start process request, url:" + url);
这个有什么问题呢?一般生产环境 log 级别都会设到 info 或者以上,那这条 log 是不会被输出的。然而不管会不会输出,这其中都会做一个字符串连接操作,然后生产一个新的字符串。如果这条语句在循环或者被调用很多次的函数中,就会多做很多无用的字符串连接,影响性能。
所以 JCL 的最佳实践推荐这么写:
if (logger.isDebugEnabled()) {
logger.debug("start process request, url:" + url);
}
然而开发者常常忽略这个问题或是觉得麻烦而不愿意这么写。
所以SLF4J提供了新的API,方便开发者使用:
logger.debug("start process request, url:{}", url);
这样的话,在不输出 log 的时候避免了字符串拼接的开销;在输出的时候需要做一个字符串format,代价比手工拼接字符串大一些,但是可以接受。
五、 log4j vs logback
logback算是log4j的升级版本 ,基本实现了所有log4j的功能。
logback比log4j有更多的优点
更快的实现
Logback的内核重写了,在一些关键执行路径上性能提升10倍以上。而且logback不仅性能提升了,初始化内存加载也更小了。
非常充分的测试
Logback经过了几年,数不清小时的测试。Logback的测试完全不同级别的。在作者的观点,这是简单重要的原因选择logback而不是log4j。
Logback-classic非常自然实现了SLF4j
Logback-classic实现了SLF4j。在使用SLF4j中,你都感觉不到logback-classic。而且因为logback-classic非常自然地实现了SLF4J,所以切换到log4j或者其他,非常容易,只需要提供成另一个jar包就OK,根本不需要去动那些通过SLF4JAPI实现的代码。
非常充分的文档
Logback文档免费。Logback的所有文档是全面免费提供的,不象Log4J那样只提供部分免费文档而需要用户去购买付费文档
Filters(过滤器)
有些时候,需要诊断一个问题,需要打出日志。在log4j,只有降低日志级别,不过这样会打出大量的日志,会影响应用性能。在Logback,你可以继续保持那个日志级别而除掉某种特殊情况,如alice这个用户登录,她的日志将打在DEBUG级别而其他用户可以继续打在WARN级别。要实现这个功能只需加4行XML配置。
SiftingAppender(一个非常多功能的Appender)
它可以用来分割日志文件根据任何一个给定的运行参数。如,SiftingAppender能够区别日志事件跟进用户的Session,然后每个用户会有一个日志文件。
自动压缩已经打出来的log
RollingFileAppender在产生新文件的时候,会自动压缩已经打出来的日志文件。压缩是个异步过程,所以甚至对于大的日志文件,在压缩过程中应用不会受任何影响。
堆栈树带有包版本
Logback在打出堆栈树日志时,会带上包的数据。
自动去除旧的日志文件
通过设置TimeBasedRollingPolicy或者SizeAndTimeBasedFNATP的maxHistory属性,你可以控制已经产生日志文件的最大数量。如果设置maxHistory为12,那那些log文件超过12个月的都会被自动移除。
六、 slf4j源码解读
我们写代码的时候是怎么打日志的呢?
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
LoggerFactory.getLogger(getClass());
这里看不到任何跟实现类有关联的代码,然而我们已经可以使用get到的logger打日志了。那么slf4j到底是怎么找到实现类的了?
根据Path = “org/slf4j/impl/StaticLoggerBinder.class”去加载相应的实现类
为什么要获取类的名字,而根据名字来获取对象呢?
因为每个类使用的日志处理实现可能不同,iLoggerFactory中也是根据名字来判断一个类的实现方式的。
那么这里会有一个问题,如果找到多个实现类,最终会绑定哪一个呢?
The warning emitted by SLF4J is just that, a warning. Even when multiple bindings are present, SLF4J will pick one logging framework/implementation and bind with it. The way SLF4J picks a binding is determined by the JVM and for all practical purposes should be considered random. As of version 1.6.6, SLF4J will name the framework/implementation class it is actually bound to.
Embedded components such as libraries or frameworks should not declare a dependency on any SLF4J binding but only depend on slf4j-api. When a library declares a compile-time dependency on a SLF4J binding, it imposes that binding on the end-user, thus negating SLF4J’s purpose. When you come across an embedded component declaring a compile-time dependency on any SLF4J binding, please take the time to contact the authors of said component/library and kindly ask them to mend their ways.
如果发现有多个实现类,那么slf4j会打印出warning信息。但是仅仅是warning而已。即使有多个实现类,slf4j也只会挑选其中一个,这个选择取决于JVM和所有其他实际因素,基本算是随机性的。同时,slf4j建议删除多余的实现类,仅仅保留一个。
七、logback
1、StaticLoggerBinder 初始化并创建logFactory ()
StaticLoggerBinder.init();
初始化 new ContextInitializer(defaultLoggerContext).autoConfig();
getLoggerFactory()
总结一下这个过程:
1、StaticLoggerBinder在加载的时候,会去读取配置文件,并根据配置文件对LoggerContext进行初始化
2、然后初始化ContextSelectorStaticBinder,在这个类内部new一个DefaultContextSelector,并把第一步中配置完毕的LoggerContext传给DefaultContextSelector
3、调用getLoggerFactory()方法,直接返回第一步中配置的LoggerContext,或者委托DefaultContextSelector类返回LoggerContext
2、loggerContext工厂类产出logger对象
Logger getLogger(final String name);
com.darcytech.controller.LoginController
Logger[com]、Logger[com.darcytec]、Logger[com.darcytech.controller]、Logger[com.darcytech.controller.LoginController]
总结一下创建Logger的完整流程:
1、如果请求ROOT logger,则直接返回root
2、如果请求的Logger已经存在,则直接返回
3、如果请求的Logger尚未创建,则从ROOT开始,级联创建所有Logger
4、每创建一个Logger,都要设置父子关系,继承生效级别
5、每创建一个Logger,都将其放入loggerCache,并将size++
3、Logger
transient private AppenderAttachableImpl<ILoggingEvent> aai;
Logger是委托这个类实现AppenderAttachable接口,也是委托这个类来调用Appender组件来实际记录日志,所以这个字段是最关键的。
主要方法
getChildByName
setLevel
createChildByName每创建一个Logger,都要设置父子关系,继承生效级别
info
callAppenders
如果子Logger和父Logger都关联了同样的Appender,则日志信息会重复记录
总结一下Logger类中定义的字段和方法,是出于以下目的:
1、定义parent和childList,用于实现父子Logger的树形结构
2、定义createChildByName()、getChildByName()方法,是供LoggerContext创建Logger
3、定义level、effectiveLevelInt,是为了判定日志级别是否足够
4、最后,filterAndLog()、buildLoggingEventAndAppend()、callAppenders()、appendLoopOnAppenders()方法,是Logger类的核心方法,一步步地委托AppenderAttachableImpl类来实际记录日志
4、Appender
实现类就是最常见的ConsoleAppender和FileAppender
doAppend()
最终writeOut()方法委托配置给它的Encoder组件来记录
5、简单了解一下RollingFileAppender,rollingPolicy
常用的RollingFileAppender, TimeBasedRollingPolicy
八、什么是MDC
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对,当需要记录日志时,只需要从 MDC 中获取所需的信息即可。
此外,对于一些线程池使用的应用场景,可能我们在最后使用结束时,需要调用clear方法来清洗将要丢弃的数据。
LogbackMDCAdapter