日志框架Log4j源码解析(1)

这篇文章笔者准备和大家介绍一下日志框架Log4j,Log4j日志框架已经很老了,但是后面的日志框架和Log4j都差不多,所以Log4j还是一个很好的学习对象。大家在写代码的过程中肯定会使用到日志框架。大家应该都会用,但是用的对不对,等这个专题完成了相信读者就心里有数了。我们先看一张图
在这里插入图片描述
我们在使用的时候接触到的应该是Logger,然后调用Logger的方法输出日志。笔者先把图上出现的对象介绍一些以及捋一捋他们之间的关系。(下面很重要)

1.在我们打印日志的时候会输入需要打印的信息,这个信息就是我们的信息源,也就是上图中的LoggerEvent。

2.我们获取Logger的时候会传入对应的类的全限定名称,其实是通过这个名称去LoggerReposity中取对应的Logger(也就是说LoggerReposity是我们的一个Logger的容器)

3.获取到Logger之后我们要去输出日志,输出的目的地很多可能是控制台、文件、远程对象。可能一行日志我们要输出到多个地方,这个时候我们就要获取到Logger中所有的Appender的集合,然后遍历去调用输出方法。

4.输出的时候我们会指定日志的格式,比如说时间、线程、所属类。这里我们就需要格式化每个Appender都会有对应的格式化也就是Layout(对应一个)。

5.有可能在打印日志的时候需要一些自定义的处理,那我们就需要拦截需要打印的消息,根据消息做一些操作,这个就是Filter(可能有多个也可能没有)。

6.可以看到在我们输出日志的时候LoggerEvent实际上是在整个流程中传递的,可以看成这个是上下文。

在实际使用的时候我们更多的是使用slf4j来获取Logger对象,因为在项目中途可能需要换日志框架比如从log4j换成logback,如果我们使用的对应日志框架中的API,那就尴尬了,我的项目那么多类都得改代码。所以slf4j就出来了,它主要使用的是适配器模式。在项目中调用的API是slf4j的API,它在会根据实际使用的日志框架适配成统一的调用格式。也就是方便项目在中途换日志框架了。在log4j分析差不多之后,笔者就简单分析slf4j。

Log4j的主要源码还是比较少的。笔者就直接根据上面的图进行介绍,首先我们来看一下Logger类。
在这里插入图片描述首先看一下Logger的结构关系,继承了Category实现了AppenderAttachable。从类名来看Category这个应该是根据不同种类打印日志,AppenderAttachable这个应该是连接Appender。我们从AppenderAttachable开始看
在这里插入图片描述
确实有一个对应的实现类。这个里面还有另外一些成员变量,

name:Logger对应的名称
level:日志级别
parent:Logger的父亲(继承关系,如果子Logger没有指定Appender默认使用父亲的)
FQCN:全限名称指定了当前类
resourceBundle:国际化相关
repository:所属的容器,
aai:相关的Appender
additve:这个变量就是控制是否使用父类的Appender

成员变量介绍完了,这个类的方法太多了。Logger主要的方法也是在这个类里面,我们先看看Logger的相关方法,然后在回来介绍这个
在这里插入图片描述方法不多,主要就是获取Logger变量(从Logger容器中获取)。我们回去分析它的父类。先来介绍关联Appender类AppenderAttachableImpl实现了AppenderAttachable。这个类只有一个成员变量。

protected Vector  appenderList; //Appender集合

然后我们在看实现的方法,这里的方法笔者不打算全部介绍,因为就是简单的操作Vector。

  public
  void addAppender(Appender newAppender) { //添加Appender
    if(newAppender == null) 
      return;

    if(appenderList == null) { //如果appenderList为空初始化
      appenderList = new Vector(1);
    }
    if(!appenderList.contains(newAppender)) //如果不包含添加
      appenderList.addElement(newAppender);
  }

其他的方法也都差不多就是操作这个集合,到这里读者肯定已经发现了Logger和Appender之间的关系是一对多。我们后面通过方法来进行分析,就先分析常用的方法

  public
  void debug(Object message) {
    if(repository.isDisabled(Level.DEBUG_INT)) //判断配置的根日志级别是否可以输出
      return;
    if(Level.DEBUG.isGreaterOrEqual(this.getEffectiveLevel())) { //Debug级别是否可以在当前日志输出
      forcedLog(FQCN, Level.DEBUG, message, null); //输出日志
    }
  }
 public
  Level getEffectiveLevel() {
    for(Category c = this; c != null; c=c.parent) { //如果当前类没有级别获取父类的
      if(c.level != null)
  return c.level;
    }
    return null; // If reached will cause an NullPointerException.
  }

首先判断根日志级别可否输出,默认是debug的,然后我们在调用getEffectiveLevel方法获取Logger设置的输出日志级别,如果当前的Logger没有设置则获取父类的级别。如果日志级别大于或者等于则说明可以输出日志调用forcedLog输出日志。

 protected
  void forcedLog(String fqcn, Priority level, Object message, Throwable t) {
    callAppenders(new LoggingEvent(fqcn, this, level, message, t)); //创建日志并appender
  }
 public
  void callAppenders(LoggingEvent event) {
    int writes = 0; 

    for(Category c = this; c != null; c=c.parent) { //遍历类本身以及父类
      // Protected against simultaneous call to addAppender, removeAppender,...
      synchronized(c) {
  if(c.aai != null) { //如果有Appender则调用输出
    writes += c.aai.appendLoopOnAppenders(event);
  }
  if(!c.additive) { //如果不能使用父类的Appender则直接退出循环
    break;
  }
      }
    }

    if(writes == 0) { //处理没有Appender的情况
      repository.emitNoAppenderWarning(this);
    }
  }

这里创建了一个LoggerEvent并调用方法获取对应的Appender进行输出,

  public
  int appendLoopOnAppenders(LoggingEvent event) { //循环Appender
    int size = 0;
    Appender appender;

    if(appenderList != null) {
      size = appenderList.size();
      for(int i = 0; i < size; i++) {
  appender = (Appender) appenderList.elementAt(i); //获取所有Appender 
  appender.doAppend(event);  //执行输出
      }
    }    
    return size; //返回appender的个数
  }

到这里我们就跟踪完了Logger.debug()的方法,后面doAppend就是Appender的方法了,后面再介绍。这样其实我们已经把Logger介绍完了,其他的输出方法都差不多。后面我们的注意点应该就是在Appender、LoggerRepository了。我们来看看
Appender先来看接口的方法
在这里插入图片描述根据方法名我们可以看到有setFilter、setErrorHandler、setLayout。读者应该就可以猜到Appdener的组成了,包括Filter、Layout、以及ErrorHandler。我们来看实现类AppenderSkeleton,这个类是一个抽象类。这个类用了模板模式,将公共的方法抽出来在父类,父类调用子类的具体实现。我们先来看看对应的成员变量

 protected Layout layout; //布局
  protected String name; //名称
  protected Priority threshold; //日志级别比这个大才会打出
  protected ErrorHandler errorHandler = new OnlyOnceErrorHandler();
  protected Filter headFilter; //第一个拦截器
  protected Filter tailFilter;//最后一个拦截器
  protected boolean closed = false; //Appender是否关闭

成员变量应该不难理解,成员变量对应一些set、get方法这里就不做介绍。咱直接接着Logger调用的方法来分析。

  public
  synchronized 
  void doAppend(LoggingEvent event) {
    if(closed) {
      LogLog.error("Attempted to append to closed appender named ["+name+"].");
      return;
    }
    if(!isAsSevereAsThreshold(event.getLevel())) { //判断日志级别,如果不满足直接return
      return;
    }
    Filter f = this.headFilter; //拦截器
    FILTER_LOOP:
    while(f != null) {
      switch(f.decide(event)) { //执行拦截方法根据返回判断是否继续拦截
      case Filter.DENY: return;
      case Filter.ACCEPT: break FILTER_LOOP;
      case Filter.NEUTRAL: f = f.getNext();
      }
    }
    this.append(event);    
  }

这里先判断对应的日志级别是否能输出,如果不能输出就直接返回,能输出就先获取拦截器。拦截器有三种状态

DENY:拒绝 即不处理直接return。后面的拦截器也不会处理了,就是说不符合条件直接返回了
ACCEPT:接受 即满足条件我直接使用Appender去处理,不会再调用后面的拦截器了
NEUTRAL:中立 拦截器处理了一些事情,后面的拦截器继续处理。

上面拦截器应该很好理解,这里用了责任链模式,责任链模式是常用的设计模式之一,一个拦截器一个拦截器往下处理。最后调用了append方法这个方法是一个抽象方法也就是子类实现了。模板方法的设计模式就体现出来了,不关心子类怎么实现(也是多态的体现)。到这里我们就要去子类里面看对应的实现我们看WriterAppender这个子类,这个子类还有对应的子类,是我们常用的Console、File模式。我们看下成员变量。

  protected boolean immediateFlush = true; //写完一条日志立刻刷新标志
  protected String encoding; //编码方式
  protected QuietWriter qw; //写者

三个成员变量,主要注意的是qw这个是实际的写者,我们可以根据不同的输出创建不同的写者,比如文件,那就是使用输出流创建一个写文件的写者,如果是Console那就是System.out创建写者

public
  void append(LoggingEvent event) {
    if(!checkEntryConditions()) {
      return;
    }
    subAppend(event);
   }

  protected
  boolean checkEntryConditions() { //检查
    if(this.closed) {
      LogLog.warn("Not allowed to write to a closed appender.");
      return false;
    }

    if(this.qw == null) {
      errorHandler.error("No output stream or file set for the appender named ["+
      name+"].");
      return false;
    }

    if(this.layout == null) {
      errorHandler.error("No layout set for the appender named ["+ name+"].");
      return false;
    }
    return true;
  }

首先检查变量,检查是否关闭、写者是否为空、布局是否为空、三个条件一个不满足就不写了。

   protected
  void subAppend(LoggingEvent event) {
    this.qw.write(this.layout.format(event)); //写日志

    if(layout.ignoresThrowable()) {
      String[] s = event.getThrowableStrRep();
      if (s != null) {
  int len = s.length;
  for(int i = 0; i < len; i++) {
    this.qw.write(s[i]);
    this.qw.write(Layout.LINE_SEP);
  }
      }
    }

    if(shouldFlush(event)) { //判断是否需要刷新
      this.qw.flush(); //刷新流
    }
  }

这里就把日志格式化之后输出了。整个大体流程就串完了,还是比较简单的,后面就是具体的一些实现了,比如解析配置文件初始化容器、初始化Logger等一系列操作,下一篇就介绍一下初始化容器吧。码字不易、右下角点个好看哈.
笔者主要还是在微信公众号写文章,记得关注哈
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值