java 日志体系(四)log4j 源码分析

java 日志体系(四)log4j 源码分析

logback、log4j2、jul 都是在 log4j 的基础上扩展的,其实现的逻辑都差不多,下面以 log4j 为例剖析一下日志框架的基本组件。

一、总体架构

log4j 使用如下:

@Test
public void test() {
    Log log = LogFactory.getLog(JclTest.class);
    log.info("jcl log");
}

log.info 时调用的时序图如下:

log4j 时序图

在 log4j 的配置文件,我们可以看到其三个最重要的组件:

  1. Logger 每个 logger 可以单独配置
  2. Appender 每个 appender 可以将日志输出到它想要的任何地方(文件、数据库、消息等等)
  3. Layout 日志格式布局

这三个组件的关系如下:

Log4J 类图

Log4j API(核心)

  • 日志对象(org.apache.log4j.Logger):供程序员输出日志信息
  • 日志附加器(org.apache.log4j.Appender):把格式化好的日志信息输出到指定的地方去
    • ConsoleAppender - 目的地为控制台的 Appender
    • FileAppender - 目的地为文件的 Appender
    • RollingFileAppender - 目的地为大小受限的文件的 Appender
  • 日志格式布局(org.apache.log4j.Layout):用来把程序员的 message 格式化成字符串
    • PatternLayout - 用指定的 pattern 格式化 message的 Layout
  • 日志过滤器(org.apache.log4j.spi.Filter)
  • 日志事件(org.apache.log4j.LoggingEvent)
  • 日志级别(org.apache.log4j.Level)
  • 日志管理器(org.apache.log4j.LogManager)
  • 日志仓储(org.apache.log4j.spi.LoggerRepository)
  • 日志配置器(org.apache.log4j.spi.Configurator)
  • 日志诊断上下文(org.apache.log4j.NDC、org.apache.log4j.MDC)

二、日志管理器(org.apache.log4j.LogManager)

主要职责:

  • 初始化默认 log4j 配置
  • 维护日志仓储(org.apache.log4j.spi.LoggerRepository)
  • 获取日志对象(org.apache.log4j.Logger)

LogManager

2.1 初始化默认 log4j 配置

LogManager 的静态代码块加载配置文件。

static {
    // 1. 初始化默认的日志仓库 Hierarchy(实现了 LoggerRepository 接口) 
    //    DefaultRepositorySelector#getLoggerRepository 简单的封装了 LoggerRepository
    Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
    repositorySelector = new DefaultRepositorySelector(h);

    // 2. DEFAULT_CONFIGURATION_KEY=log4j.configuration 配置文件
    //    CONFIGURATOR_CLASS_KEY=log4j.configuratorClass 配置文件解析器,
    //    分 DOMConfigurator 和 PropertyConfigurator 两类
    String configurationOptionStr = OptionConverter.getSystemProperty(
            DEFAULT_CONFIGURATION_KEY, null);
    String configuratorClassName = OptionConverter.getSystemProperty(
            CONFIGURATOR_CLASS_KEY, null);

    // 3. 根据配置文件路径加载资源文件
    URL url = null;
    if (configurationOptionStr == null) {
        url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE);
        if (url == null) {
            url = Loader.getResource(DEFAULT_CONFIGURATION_FILE);
        }
    } else {
        try {
            url = new URL(configurationOptionStr);
        } catch (MalformedURLException ex) {
            // so, resource is not a URL:
            // attempt to get the resource from the class path
            url = Loader.getResource(configurationOptionStr);
        }
    }

    // 4. Configurator 解析配置文件
    if (url != null) {
        try {
            OptionConverter.selectAndConfigure(url, configuratorClassName,
                    LogManager.getLoggerRepository());
        } catch (NoClassDefFoundError e) {
            LogLog.warn("Error during default initialization", e);
        }
    } 
}

2.2 日志仓储(org.apache.log4j.spi.LoggerRepository)

主要职责:

  • 管理日志级别阈值(org.apache.log4j.Level)
  • 管理日志对象(org.apache.log4j.Logger)

LoggerRepository 的主要方法是 getLogger(name),创建一个日志对象。

// ht 通过 key/value 的形式保存了所有的 logger,其中 key 为类的全路径,value 为 logger
// logger 有父子关系,每个 logger 的父节点为前一个包名,如果父节点不存在则一直向上查找,直到 rootLogger
// 如果其父节点不存在,使用 ProvisionNode 先进行占位,ProvisionNode 保存有其全部的子节点
// 即 com.github.binarylei.log4j.Log4jTest1 的父节点为 com.github.binarylei.log4j,直到 rootLogger 为止
Hashtable ht;

public Logger getLogger(String name, LoggerFactory factory) {
    CategoryKey key = new CategoryKey(name);
    Logger logger;

    synchronized (ht) {
        Object o = ht.get(key);
        // 1. 日志仓库中没有创建一个
        if (o == null) {
            logger = factory.makeNewLoggerInstance(name);
            logger.setHierarchy(this);
            ht.put(key, logger);
            updateParents(logger);
            return logger;
        // 2. 存在直接返回
        } else if (o instanceof Logger) {
            return (Logger) o;
        // 3. ProvisionNode 占位用
        } else if (o instanceof ProvisionNode) {
            //System.out.println("("+name+") ht.get(this) returned ProvisionNode");
            logger = factory.makeNewLoggerInstance(name);
            logger.setHierarchy(this);
            ht.put(key, logger);
            // ProvisionNode 中的是子节点元素,logger 为当前的父节点
            updateChildren((ProvisionNode) o, logger);
            updateParents(logger);
            return logger;
        } else {
            // It should be impossible to arrive here
            return null;
        }
    }
}

其中有两个相对比较重要的方法,updateParents 和 updateChildren

// 轮询父节点,如果存在则直接指定其父节点
// 如果不存在则创建一个 ProvisionNode 用于占位,并设置 ProvisionNode 的子节点
final private void updateParents(Logger cat) {
    String name = cat.name;
    int length = name.length();
    boolean parentFound = false;

    // if name = "w.x.y.z", loop thourgh "w.x.y", "w.x" and "w", but not "w.x.y.z"
    // 轮询父节点
    for (int i = name.lastIndexOf('.', length - 1); i >= 0;
         i = name.lastIndexOf('.', i - 1)) {
        String substr = name.substring(0, i);
        CategoryKey key = new CategoryKey(substr); // simple constructor
        Object o = ht.get(key);
        // 1. 不存在父节点,创建一个 ProvisionNode 用于占位,设置其子节点为 cat
        if (o == null) {
            ProvisionNode pn = new ProvisionNode(cat);
            ht.put(key, pn);
        // 2. 存在父节点则指定当前 logger 的父节点
        } else if (o instanceof Category) {
            parentFound = true;
            cat.parent = (Category) o;
            break; // no need to update the ancestors of the closest ancestor
        // 3. 如果是 ProvisionNode 直接添加其子节点
        } else if (o instanceof ) {
            ((ProvisionNode) o).addElement(cat);
        } else {
            Exception e = new IllegalStateException("unexpected object type " +
                    o.getClass() + " in ht.");
            e.printStackTrace();
        }
    }
    // If we could not find any existing parents, then link with root.
    if (!parentFound)
        cat.parent = root;
}

// ProvisionNode 保存有当前 logger 的所有子节点
// 创建 logger 时如果找不到父节点则默认为 root,即 l.parent.name=root
// 如果 l.parent 已经是正确的父节点则忽略,否则就需要更新其父节点
final private void updateChildren(ProvisionNode pn, Logger logger) {
    final int last = pn.size();

    for (int i = 0; i < last; i++) {
        Logger l = (Logger) pn.elementAt(i);
        if (!l.parent.name.startsWith(logger.name)) {
            logger.parent = l.parent;
            l.parent = logger;
        }
    }
}

三、日志对象(org.apache.log4j.Logger)

Logger 继承自 org.apache.log4j.Priority。Logger 日志级别: OFF、FATAL、ERROR、INFO、DEBUG、TRACE、ALL。

Logger 最终要的方法是输出日志,持有 Appender 才能输出日志。

3.1 Logger 管理 Appender

AppenderAttachableImpl 用来管理所有的 Appender,对 logger 上的所有 Appender 进行增删改查,当前还一个最重要的方法 appendLoopOnAppenders 用于输出日志。

AppenderAttachableImpl aai;
public synchronized void addAppender(Appender newAppender) {
    if (aai == null) {
        aai = new AppenderAttachableImpl();
    }
    aai.addAppender(newAppender);
    repository.fireAddAppenderEvent(this, newAppender);
}

3.2 Logger 日志输出

public void info(Object message) {
    if (repository.isDisabled(Level.INFO_INT))
        return;
    if (Level.INFO.isGreaterOrEqual(this.getEffectiveLevel()))
        forcedLog(FQCN, Level.INFO, message, null);
}
protected void forcedLog(String fqcn, Priority level, Object message, Throwable t) {
	callAppenders(new LoggingEvent(fqcn, this, level, message, t));
}

callAppenders 最终调用 appender.doAppend(event) 进行日志输出。

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) {
        	// 1. 日志输出
            if (c.aai != null) {
                writes += c.aai.appendLoopOnAppenders(event);
            }
            // 2. 如果 logger.additive=false 则不会将日志向上传递给父节点 logger
            //    也就是说 additive=false 时日志不会重复输出,默认为 true
            //    类似 spring 子容器的事件传递给父容器
            if (!c.additive) {
                break;
            }
        }
    }
    // 没有日志输出
    if (writes == 0) {
        repository.emitNoAppenderWarning(this);
    }
}

// AppenderAttachableImpl#appendLoopOnAppenders 用于日志输出
public int appendLoopOnAppenders(LoggingEvent event) {
    int size = 0;
    Appender appender;

    if (appenderList != null) {
        size = appenderList.size();
        for (int i = 0; i < size; i++) {
            appender = (Appender) appenderList.elementAt(i);
            // 真正输出日志
            appender.doAppend(event);
        }
    }
    return size;
}

3.3 日志事件(org.apache.log4j.LoggingEvent)

日志事件是用于承载日志信息的对象,其中包括:日志名称、日志内容、日志级别、异常信息(可选)、当前线程名称、时间戳、嵌套诊断上下文(NDC)、映射诊断上下文(MDC)。

四、日志附加器(org.apache.log4j.Appender)

日志附加器是日志事件(org.apache.log4j.LoggingEvent)具体输出的介质,如:控制台、文件系统、网络套接字等。

日志附加器(org.apache.log4j.Appender)关联零个或多个日志过滤器(org.apache.log4j.Filter),这些过滤器形成过滤链。

主要职责:

  • 附加日志事件(org.apache.log4j.LoggingEvent)
  • 关联日志布局(org.apache.log4j.Layout)
  • 关联日志过滤器(org.apache.log4j.Filter)
  • 关联错误处理器(org.apache.log4j.spi.ErrorHandler)

相关组件的关系如下,Append 持有 Layout、Filter、ErrorHandler。

Appender 组件关系

4.1 Appender 主要流程

log info流程

注意 logger#info 调用 doAppend 时加 synchronized 锁了,所以是线程安全的,但了同时造成多线程时效率低下。所以才有了后来的 log4j2 和 logback 的出现。

public synchronized void doAppend(LoggingEvent event) {
  	// 1. 日志级别判断
    if (!isAsSevereAsThreshold(event.getLevel())) {
        return;
    }

  	// 2. Filter 过滤
    Filter f = this.headFilter;
    FILTER_LOOP:
    while (f != null) {
        switch (f.decide(event)) {
        	// 1. 日志事件跳过日志附加器的执行
            case Filter.DENY:
                return;
        	// 2. 日志附加器立即执行日志事件
            case Filter.ACCEPT:
                break FILTER_LOOP;
        	// 3. 跳过当前过滤器,让下一个过滤器决策
            case Filter.NEUTRAL:
                f = f.getNext();
        }
    }
    // 3. 子类实现,日志输出
    this.append(event);
}

doAppend 做日志过滤,是否进行日志输出,真实的日志输出则直接委托给了 append 方法。append -> subAppend -> qw.write,QuietWriter 增加了对日志输出错误时的 ErrorHandler 处理。

public void append(LoggingEvent event) {
	subAppend(event);
}
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();
    }
}

4.2 日志过滤器(org.apache.log4j.spi.Filter)

日志过滤器用于决策当前日志事件(org.apache.log4j.spi.LoggingEvent)是否需要在执行所关联的日志附加器(org.apache.log4j.Appender)中执行。

决策结果有三种:

  • DENY:日志事件跳过日志附加器的执行
  • ACCEPT:日志附加器立即执行日志事件
  • NEUTRAL:跳过当前过滤器,让下一个过滤器决策
public void addFilter(Filter newFilter) {
    if (headFilter == null) {
        headFilter = tailFilter = newFilter;
    } else {
        tailFilter.setNext(newFilter);
        tailFilter = newFilter;
    }
}

4.3 Appender 类继承关系

Appender 类图

  • ConsoleAppender - 目的地为控制台的 Appender
  • FileAppender - 目的地为文件的 Appender
  • RollingFileAppender - 目的地为大小受限的文件的 Appender

WriterAppender 不关心日志到底写到那个流中,子类调用 createWriter 来创建一个具体的 Writer,这个 Writer 最终会被 QuietWriter 进行包装。

// WriterAppender#createWriter
protected OutputStreamWriter createWriter(OutputStream os) {
    OutputStreamWriter retval = null;

    String enc = getEncoding();
    if (enc != null) {
        try {
            retval = new OutputStreamWriter(os, enc);
        } catch (IOException e) {
        }
    }
    if (retval == null) {
        retval = new OutputStreamWriter(os);
    }
    return retval;
}

4.3.1 FileAppender

FileAppender 通过 setFile 方法创建一个 QuietWriter 进行文件定入。

public synchronized void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize)
            throws IOException {
    if (bufferedIO) {
        setImmediateFlush(false);
    }

    reset();
    FileOutputStream ostream = null;
    try {
        ostream = new FileOutputStream(fileName, append);
    } catch (FileNotFoundException ex) {
     	...
    }
    Writer fw = createWriter(ostream);
    if (bufferedIO) {
        fw = new BufferedWriter(fw, bufferSize);
    }
    this.setQWForFiles(fw);
    this.fileName = fileName;
    this.fileAppend = append;
    this.bufferedIO = bufferedIO;
    this.bufferSize = bufferSize;
    writeHeader();
    LogLog.debug("setFile ended");
}

4.3.2 RollingFileAppender 文件大小滚动

RollingFileAppender 根据文件大小进行滚动,有一个重要的属性 maxFileSize 控制文件大小。RollingFileAppender#subAppend 每次写日志时都会判断是否达到回滚的条件。

protected void subAppend(LoggingEvent event) {
    super.subAppend(event);
    if (fileName != null && qw != null) {
        long size = ((CountingQuietWriter) qw).getCount();
        if (size >= maxFileSize && size >= nextRollover) {
        	// 滚动生成新的日志文件
            rollOver();
        }
    }
}

4.3.3 DailyRollingFileAppender 时间滚动

DailyRollingFileAppender(根据时间滚动) 和 RollingFileAppender(根据文件大小滚动) 差不多,只是回滚的条件不一样吧了。DailyRollingFileAppender 有一个重要的属性 datePattern = "'.'yyyy-MM-dd" 用于控制多长时间滚动一次,具体配制规则见类注释。

protected void subAppend(LoggingEvent event) {
    long n = System.currentTimeMillis();
    if (n >= nextCheck) {
        now.setTime(n);
        // 计算一次滚动的时间
        nextCheck = rc.getNextCheckMillis(now);
        try {
            rollOver();
        } catch (IOException ioe) {
            ...
        }
    }
    super.subAppend(event);
}

五、日志格式布局(org.apache.log4j.Layout)

日志格式布局用于格式化日志事件(org.apache.log4j.spi.LoggingEvent)为可读性的文本内容。

Layout 最重要的方法是 format,将 LoggingEvent 转换成可读性的文本内容。

5.1 SimpleLayout

public String format(LoggingEvent event) {
	sbuf.setLength(0);
	sbuf.append(event.getLevel().toString());
	sbuf.append(" - ");
	sbuf.append(event.getRenderedMessage());
	sbuf.append(LINE_SEP);
	return sbuf.toString();
}

5.2 PatternLayout

PatternLayout 可以自定义 LoggingEvent 输出格式,如 "%r [%t] %p %c %x - %m%n",初始化时会将 pattern 解析为 PatternConverter,PatternConverter 是一个链式结构。PatternLayout 自定义规则详见 PatternLayout 类注释。

public final static String DEFAULT_CONVERSION_PATTERN = "%m%n";
private StringBuffer sbuf = new StringBuffer(BUF_SIZE);
private String pattern;
private PatternConverter head;

public PatternLayout(String pattern) {
    this.pattern = pattern;
    head = createPatternParser((pattern == null) ? DEFAULT_CONVERSION_PATTERN :
            pattern).parse();
}
protected PatternParser createPatternParser(String pattern) {
    return new PatternParser(pattern);
}

LoggingEvent 格式化时调用 PatternConverter#format 方法,PatternConverter 具体格式化的实现以后有时间再看一下。

public String format(LoggingEvent event) {
    // Reset working stringbuffer
    if (sbuf.capacity() > MAX_CAPACITY) {
        sbuf = new StringBuffer(BUF_SIZE);
    } else {
        sbuf.setLength(0);
    }

    PatternConverter c = head;
    while (c != null) {
        c.format(sbuf, event);
        c = c.next;
    }
    return sbuf.toString();
}

六、日志配置器(org.apache.log4j.spi.Configurator)

日志配置器提供外部配置文件配置 log4j 行为的 API,log4j 内建了两种实现:

  • Properties 文件方式(org.apache.log4j.PropertyConfigurator)
  • XML 文件方式(org.apache.log4j.xml.DOMConfigurator)

每天用心记录一点点。内容也许不重要,但习惯很重要!

posted on 2019-04-29 08:21  binarylei  阅读(2421)  评论(0编辑  收藏  举报

导航