log4j日志框架源码——总览

研究了许久,作为半路出家的码农,还是记下学习的成果。(本文针对的版本是1.2.17)

初学者在记录某段程序结果或者某段记录时,最常用的就是:

System.out.println("test String...");

但是这个输出方法是系统控制台的输出,在实际生产环境会被埋没在海量的日志中。所以有了很多日志框架,这里结合自己学习的Apache的log4j日志框架,从源码阶段分享下该框架的实现和使用!(第一次写博客,忘批评指正!)

开始前,请思考以下几个问题:

  1. 输出的内容有格式要求吗?
  2. 日志输出到哪里?控制台?页面?还是文件中?
  3. 可以选择有些日志输出,有些日志不输出吗?
  4. 系统日志那么多,我怎么找到我要的日志?

针对这些疑问,很容易想到:

  1. 原来的test String...可能会在不同的场景中会有不同的格式要求,——log4j抽象出Layout来描述日志的格式
  2. 日志的输出位置也不仅仅限制于控制台,——log4j抽象出Appender来描述日志的输出形式
  3. 在开发时,可以debug打日志调试,但是在生产环境就不需要,——log4j抽象出Level来描述日志的级别
  4. 给一个Logger名字是不是更好管理他们?————log4j抽象出Category和LoggerRepository来描述日志和日志管理容器

那么,log4j的工作流程以及它工作的源码细节又是如何呢?我们通过debug追溯他的执行轨迹。

先简述下流程:

  1. 入口:Logger log = Logger.getLogger(AppTest.class);
  2.  Logger.getLogger(Class clazz);实际上启动LoggerManager日志管理器,该管理器主要工作是加载log4j配置文件,并进行初始化;
  3. 配置文件解析,初始化rootLogger,添加Appender,Layout等配置;
  4. 注册logger实例到LoggerRepository,更新层级节点关系;
  5. 执行日志记录行为,debug?info?error?
  6. 判别级别后,让所有日志输出器执行输出;
  7. 输出前会进行过滤和样式渲染;
  8. 指定形式输出。

到这里流程大体就是这样了,看起来不清晰,而且流程中的处理似乎和开头问题中出现的抽象的类关联不大,没关系,我们先大体宏观上清楚log4j是如何工作的。 接下来,将重点结合源码逐步解析。

一、首先啰嗦之前出现的所有的类:

  1. Logger——日志行为类,debug,info等日志记录行为,
  2. Category——Logger的父类
  3. LoggerRepository——日志容器,是管理Logger的容器,内部维护一个Hashtable,储存所有的Logger
  4. LoggerManager——日志管理,日志框架的入口,由他开始启动日志系统,加载配置,初始化根日志
  5. Appender——日志输出器
  6. Layout——日志形式
  7. Level——日志级别
  8. rootLogger——根日志,每个Logger都有父级Logger,是层级关系,如果没有,则根日志就是父级,根日志是最后节点

二、详解:

    1、Logger

public class Logger extends Category { // 继承Category
    protected Logger(String name) {
        super(name);
    }
}

Logger在Category的基础上扩展了一个trace()行为,低于debug,重点看Category类

public class Category implements AppenderAttachable {    // 实现AppenderAttachable接口,改接口是日志输出器的管理接口
        
        protected String name;        // logger的名字
 
        volatile protected Level level;    // logger的级别
 
        volatile protected Category parent;     // 父logger,log4j支持a.b.c这样的命名logger,但是会继续解析a.b和a为临时节点
 
    private static final String FQCN = Category.class.getName();    // 默认命名
 
    protected ResourceBundle resourceBundle;    // 资源绑定对象
 
    protected LoggerRepository repository;    // logger容器,每个日志也知道自己属于哪个容器
 
    AppenderAttachableImpl aai;    // 日志输出器的默认管理接口实现类,每一个Logger都会把自己的Appender交给aai来管理,Logger在记录日志行为实际上都是aai在管理输出器的输出
 
    protected boolean additive = true;    // 继承标记,如果为false,表示不能使用父类Logger的Appender
 
    protected Category(String name) {
            this.name = name;
    }
}

由源码可以看出,每一个logger实际上都是一个层级的有行为的日志输出器管理者,只是他的所有输出器都交由aai来管理,如下源码:

public void debug(Object message, Throwable t) {    // 所有日志记录行为,都是判断级别后,调用forceLog方法
    if(repository.isDisabled(Level.DEBUG_INT))
        return;
    if(Level.DEBUG.isGreaterOrEqual(this.getEffectiveLevel()))
        forcedLog(FQCN, Level.DEBUG, message, t);
}
 
// 而forceLog则是调用callAppenders方法,同时把日志内容封装为LoggerEvent对象
protected void forcedLog(String fqcn, Priority level, Object message, Throwable t) {
    callAppenders(new LoggingEvent(fqcn, this, level, message, t));
}

,在执行日志行为时,实际上都是aai管理的Appender在输出

public void callAppenders(LoggingEvent event) {    // loggerEvent是对日志内容的封装,把要输出的内容封装成该对象
    int writes = 0;
        // 遍历logger及父级,每个logger的aai输出管理器输出
    for(Category c = this; c != null; c=c.parent) {
     
        synchronized(c) {
            if(c.aai != null) {
                writes += c.aai.appendLoopOnAppenders(event);    // 每级logger的aai执行appendLoopOnAppenders
            }
            if(!c.additive) {    //如果父级的additive为false,则不再执行父级logger的Appender
                break;
            }
        }
    }
        // 如果Appender数为0,监听警告
    if(writes == 0) {
        repository.emitNoAppenderWarning(this);
    }
}

由上可以看出,总结如下:

  1.  Logger是有层级的,内部维护aai,管理所有的Appender,并且可以调用父级的Appender,aai后面讲Appender时详解。
  2.  Logger可以用父类的Appender,通过设置父类的additive为false,如果不让子级用本级的Appender,则本级设为false
  3.  Logger的日志内容是经过包装的,包装后的内容是不是可以添加样式Layout,是不是便于Appender输出呢?
  4.  Logger的日志行为是有级别的,级别下篇详解。

猜你喜欢

转载自blog.csdn.net/hchhan/article/details/82562873