探索java世界中的日志奥秘

                                  java日志简单介绍

 

 

 

对于一个应用程序来说日志记录是必不可少的一部分。线上问题追踪,基于日志的业务逻辑统计分析等都离不日志。JAVA领域存在多种日志框架,目前常用的日志框架包括Log4jLog4j 2Commons LoggingSlf4jLogbackJul

 

 

一、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引入Log4jjava的标准库中,但Sun拒绝了。

2002Java1.4发布,Sun推出了自己的日志库JUL(Java Util Logging),其实现基本模仿了Log4j的实现。在JUL出来以前,log4j就已经成为一项成熟的技术,使得log4j在选择上占据了一定的优势。

接着,Apache推出了Jakarta Commons LoggingJCL只是定义了一套日志接口(其内部也提供一个Simple Log的简单实现),支持运行时动态加载日志组件的实现,也就是说,在你应用代码里,只需调用Commons Logging的接口,底层实现可以是log4j,也可以是Java Util Logging

后来(2006)Ceki Gülcü不适应Apache的工作方式,离开了Apache。然后先后创建了slf4j(日志门面接口,类似于Commons Logging)Logback(Slf4j的实现)两个项目,并回瑞典创建了QOS公司,QOS官网上是这样描述Logback的:The GenericReliable Fast&Flexible Logging Framework(一个通用,可靠,快速且灵活的日志框架)

现今,Java日志领域被划分为两大阵营:Commons Logging阵营和SLF4J阵营。
Commons LoggingApache大树的笼罩下,有很大的用户基数。但有证据表明,形式正在发生变化。2013年底有人分析了GitHub30000个项目,统计出了最流行的100Libraries,可以看出slf4j的发展趋势更好:

 

 

Apache眼看有被Logback反超的势头,于2012-07重写了log4j 1.x,成立了新的项目Log4j 2Log4j 2具有logback的所有特性。

 

 

二、java常用日志框架类别介绍

 

         • Log4j Apache Log4j是一个基于Java的日志记录工具。现在则是Apache软件基金会的一个项目。 Log4j是几种Java日志框架之一。 Log4j应该说是Java领域资格最老,应用最广的日志工具。从诞生之日到现在一直广受业界欢迎。Log4j是高度可配置的,并可通过在运行时的外部文件配置。它根据记录的优先级别,并提供机制,以指示记录信息到许多的目的地,诸如:数据库,文件,控制台,UNIX系统日志等。

Log4j 2 Apache Log4j 2apache开发的一款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 LoggingSlf4j是日志门面(门面模式是软件工程中常用的一种软件设计模式,也被称为正面模式、外观模式。它为子系统中的一组接口提供一个统一的高层接口,使得子系统更容易使用)log4jLogback则是具体的日志实现方案。可以简单的理解为接口与接口的实现,调用这只需要关注接口而无需关注具体的实现,做到解耦。

比较常用的组合使用方式是Slf4jLogback组合使用,Commons LoggingLog4j组合使用。

Logback必须配合Slf4j使用。由于LogbackSlf4j是同一个作者,其兼容性不言而喻。(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

 

 

1StaticLoggerBinder 初始化并创建logFactory ()

 

 

 

StaticLoggerBinder.init();

 

初始化 new ContextInitializer(defaultLoggerContext).autoConfig();

 

getLoggerFactory()

 

 

总结一下这个过程: 

1StaticLoggerBinder在加载的时候,会去读取配置文件,并根据配置文件对LoggerContext进行初始化 

2、然后初始化ContextSelectorStaticBinder,在这个类内部new一个DefaultContextSelector,并把第一步中配置完毕的LoggerContext传给DefaultContextSelector 

3、调用getLoggerFactory()方法,直接返回第一步中配置的LoggerContext,或者委托DefaultContextSelector类返回LoggerContext

 

 

2loggerContext工厂类产出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、定义parentchildList,用于实现父子Logger的树形结构 

2、定义createChildByName()getChildByName()方法,是供LoggerContext创建Logger 

3、定义leveleffectiveLevelInt,是为了判定日志级别是否足够 

4、最后,filterAndLog()buildLoggingEventAndAppend()callAppenders()appendLoopOnAppenders()方法,是Logger类的核心方法,一步步地委托AppenderAttachableImpl类来实际记录日志 

 

 

 

4Appender

 

 

实现类就是最常见的ConsoleAppender和FileAppender

 

doAppend()

 

最终writeOut()方法委托配置给它的Encoder组件来记录

 

 

 

5、简单了解一下RollingFileAppender,rollingPolicy

常用的RollingFileAppender, TimeBasedRollingPolicy

 

 

 

 

 

 

 

 

八、什么是MDC

 

 

 

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对,当需要记录日志时,只需要从 MDC 中获取所需的信息即可。

 

 

 

此外,对于一些线程池使用的应用场景,可能我们在最后使用结束时,需要调用clear方法来清洗将要丢弃的数据。

 

LogbackMDCAdapter

 

 

 

 

 

 

 

 

 

posted @ 2018-01-19 10:43  上街、找金莎  阅读(324)  评论(0编辑  收藏  举报