这篇文章笔者准备和大家介绍一下日志框架Log4j,Log4j日志框架已经很老了,但是后面的日志框架和Log4j都差不多,所以Log4j还是一个很好的学习对象。大家在写代码的过程中肯定会使用到日志框架。大家应该都会用,但是用的对不对,等这个专题完成了相信读者就心里有数了。我们先看一张图
我们在使用的时候接触到的应该是Logger,然后调用Logger的方法输出日志。笔者先把图上出现的对象介绍一些以及捋一捋他们之间的关系。(下面很重要)
- 在我们打印日志的时候会输入需要打印的信息,这个信息就是我们的信息源,也就是上图中的LoggerEvent。
- 我们获取Logger的时候会传入对应的类的全限定名称,其实是通过这个名称去LoggerReposity中取对应的Logger(也就是说LoggerReposity是我们的一个Logger的容器)
- 获取到Logger之后我们要去输出日志,输出的目的地很多可能是控制台、文件、远程对象。可能一行日志我们要输出到多个地方,这个时候我们就要获取到Logger中所有的Appender的集合,然后遍历去调用输出方法。
- 输出的时候我们会指定日志的格式,比如说时间、线程、所属类。这里我们就需要格式化每个Appender都会有对应的格式化也就是Layout(对应一个)。
- 有可能在打印日志的时候需要一些自定义的处理,那我们就需要拦截需要打印的消息,根据消息做一些操作,这个就是Filter(可能有多个也可能没有)。
- 可以看到在我们输出日志的时候LoggerEvent实际上是在整个流程中传递的,可以看成这个是上下文。
在实际使用的时候我们更多的是使用slf4j来获取Logger对象,因为在项目中途可能需要换日志框架比如从log4j换成logback,如果我们使用的对应日志框架中的API,那就尴尬了,我的项目那么多类都得改代码。所以slf4j就出来了,它主要使用的是适配器模式。在项目中调用的API是slf4j的API,它在会根据实际使用的日志框架适配成统一的调用格式。也就是方便项目在中途换日志框架了。在log4j分析差不多之后,笔者就简单分析slf4j。
Log4j的主要源码还是比较少的。笔者就直接根据上面的图进行介绍,首先我们来看一下Logger类。
首先看一下Logger的结构关系,继承了Category实现了AppenderAttachable。从类名来看Category这个应该是根据不同种类打印日志,AppenderAttachable这个应该是连接Appender。我们从AppenderAttachable开始看
果然看这些方法add、get、getAll、remove这不就是在操作一个容器嘛。应该就是关联Appender的接口了。我们在看实现类Category
确实有一个对应的实现类。这个里面还有另外一些成员变量,
- name:Logger对应的名称
- level:日志级别
- parent:Logger的父亲(继承关系,如果子Logger没有指定Appender默认使用父亲的)
- FQCN:全限名称指定了当前类
- resourceBundle:国际化相关
- repository:所属的容器,
- aai:相关的Appender
- additve:这个变量就是控制是否使用父类的Appender
成员变量介绍完了,这个类的方法太多了。Logger主要的方法也是在这个类里面,我们先看看Logger的相关方法,然后在回来介绍这个
方法不多,主要就是获取Logger变量(从Logger容器中获取)。我们回去分析它的父类。先来介绍关联Appender类AppenderAttachableImpl实现了AppenderAttachable。这个类只有一个成员变量。
protected
然后我们在看实现的方法,这里的方法笔者不打算全部介绍,因为就是简单的操作Vector。
public
其他的方法也都差不多就是操作这个集合,到这里读者肯定已经发现了Logger和Appender之间的关系是一对多。我们后面通过方法来进行分析,就先分析常用的方法
public
首先判断根日志级别可否输出,默认是debug的,然后我们在调用getEffectiveLevel方法获取Logger设置的输出日志级别,如果当前的Logger没有设置则获取父类的级别。如果日志级别大于或者等于则说明可以输出日志调用forcedLog输出日志。
protected
这里创建了一个LoggerEvent并调用方法获取对应的Appender进行输出,
public
到这里我们就跟踪完了Logger.debug()的方法,后面doAppend就是Appender的方法了,后面再介绍。这样其实我们已经把Logger介绍完了,其他的输出方法都差不多。后面我们的注意点应该就是在Appender、LoggerRepository了。我们来看看
Appender先来看接口的方法
根据方法名我们可以看到有setFilter、setErrorHandler、setLayout。读者应该就可以猜到Appdener的组成了,包括Filter、Layout、以及ErrorHandler。我们来看实现类AppenderSkeleton,这个类是一个抽象类。这个类用了模板模式,将公共的方法抽出来在父类,父类调用子类的具体实现。我们先来看看对应的成员变量
protected
成员变量应该不难理解,成员变量对应一些set、get方法这里就不做介绍。咱直接接着Logger调用的方法来分析。
public
这里先判断对应的日志级别是否能输出,如果不能输出就直接返回,能输出就先获取拦截器。拦截器有三种状态
DENY:拒绝 即不处理直接return。后面的拦截器也不会处理了,就是说不符合条件直接返回了- ACCEPT:接受 即满足条件我直接使用Appender去处理,不会再调用后面的拦截器了
- EUTRAL:中立 拦截器处理了一些事情,后面的拦截器继续处理。
上面拦截器应该很好理解,这里用了责任链模式,责任链模式是常用的设计模式之一,一个拦截器一个拦截器往下处理。最后调用了append方法这个方法是一个抽象方法也就是子类实现了。模板方法的设计模式就体现出来了,不关心子类怎么实现(也是多态的体现)。到这里我们就要去子类里面看对应的实现我们看WriterAppender这个子类,这个子类还有对应的子类,是我们常用的Console、File模式。我们看下成员变量。
protected
三个成员变量,主要注意的是qw这个是实际的写者,我们可以根据不同的输出创建不同的写者,比如文件,那就是使用输出流创建一个写文件的写者,如果是Console那就是System.out创建写者
public
首先检查变量,检查是否关闭、写者是否为空、布局是否为空、三个条件一个不满足就不写了。
protected
这里就把日志格式化之后输出了。整个大体流程就串完了,还是比较简单的,后面就是具体的一些实现了,比如解析配置文件初始化容器、初始化Logger等一系列操作,下一篇就介绍一下初始化容器吧。如果对文章和作者感兴趣,可以关注作者的个人公众号。里面有更多的源码分析。