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