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 的配置文件,我们可以看到其三个最重要的组件:

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

这三个组件的关系如下:

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)

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。

4.1 Appender 主要流程

注意 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 类继承关系

  • 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)

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

java 日志体系(四)log4j 源码分析的更多相关文章

  1. java多线程系列(九)---ArrayBlockingQueue源码分析

    java多线程系列(九)---ArrayBlockingQueue源码分析 目录 认识cpu.核心与线程 java多线程系列(一)之java多线程技能 java多线程系列(二)之对象变量的并发访问 j ...

  2. Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式

    在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概 ...

  3. JAVA设计模式-动态代理(Proxy)源码分析

    在文章:JAVA设计模式-动态代理(Proxy)示例及说明中,为动态代理设计模式举了一个小小的例子,那么这篇文章就来分析一下源码的实现. 一,Proxy.newProxyInstance方法 @Cal ...

  4. Java集合系列[4]----LinkedHashMap源码分析

    这篇文章我们开始分析LinkedHashMap的源码,LinkedHashMap继承了HashMap,也就是说LinkedHashMap是在HashMap的基础上扩展而来的,因此在看LinkedHas ...

  5. Java并发系列[3]----AbstractQueuedSynchronizer源码分析之共享模式

    通过上一篇的分析,我们知道了独占模式获取锁有三种方式,分别是不响应线程中断获取,响应线程中断获取,设置超时时间获取.在共享模式下获取锁的方式也是这三种,而且基本上都是大同小异,我们搞清楚了一种就能很快 ...

  6. Java并发系列[5]----ReentrantLock源码分析

    在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可 ...

  7. java集合系列之LinkedList源码分析

    java集合系列之LinkedList源码分析 LinkedList数据结构简介 LinkedList底层是通过双端双向链表实现的,其基本数据结构如下,每一个节点类为Node对象,每个Node节点包含 ...

  8. java集合系列之ArrayList源码分析

    java集合系列之ArrayList源码分析(基于jdk1.8) ArrayList简介 ArrayList时List接口的一个非常重要的实现子类,它的底层是通过动态数组实现的,因此它具备查询速度快, ...

  9. commons-logging + log4j源码分析

    分析之前先理清楚几个概念 Log4J = Log For Java SLF4J = Simple Logging Facade for Java 看到Facade首先想到的就是设计模式中的门面(Fac ...

随机推荐

  1. python count函数

    描述 Python count() 方法用于统计字符串里某个字符出现的次数.可选参数为在字符串搜索的开始与结束位置. 语法 count()方法语法: str.count(sub, start= 0,e ...

  2. caffe学习笔记1

    博客 http://blog.csdn.net/seven_first/article/details/47378697 https://zhuanlan.zhihu.com/p/25127756?r ...

  3. leetcode每日刷题计划-简单篇day5

    刷题成习惯以后感觉挺好的 Num 27 移除元素 Remove Element 跟那个排序去掉相同的一样,len标记然后新的不重复的直接放到len class Solution { public: i ...

  4. Docker 多主机方案

    利用OpenVSwitch构建多主机Docker网络 [编者的话]当你在一台主机上成功运行Docker容器后,信心满满地打算将其扩展到多台主机时,却发现前面的尝试只相当于写了个Hello World的 ...

  5. cenos 修改静态ip

    修改为静态ip 1)在终端命令窗口中输入 [root@hadoop101 /]#vim /etc/udev/rules.d/70-persistent-net.rules 进入如下页面,删除eth0该 ...

  6. css 动态线条制作方案

    利用 :before   or    :after  在元素中添加线条样式: 设置样式的过渡效果属性值: 改变width,left,transform等属性值,设置鼠标移入:hover 效果: li: ...

  7. 吴裕雄 python 机器学习——回归决策树模型

    import numpy as np import matplotlib.pyplot as plt from sklearn import datasets from sklearn.model_s ...

  8. WEB的数据交互具体流程

    前一段时间小小的总结了一下,web的前后交互的各种方式可能没写全,后期再写,话不多说 前端传递数据到servlet,servlet获取数据后操作DAO修改数据库,然后servlet将某些参数返回到前端 ...

  9. OO第一单元小结

    写在前面 在接触OO课程之前,自己是完全没有学习过java语言的,因此作为一名初的不能再初的初学者,无论是在哪方面都会有许多茫然,但是我相信通过一次次认真的完成OO作业,我对面向对象的理解应该会渐渐的 ...

  10. Harbor私有镜像仓库(上)

    上图配置为工作环境 特别注意:win10现在不允许使用私有ca证书,到时登录浏览器会失败,可以选用火狐浏览器. 创建自己的CA证书 openssl req -newkey rsa:4096 -node ...