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

时间:2022-06-07 10:31:20

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

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

一、总体架构

log4j 使用如下:

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

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

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

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

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

这三个组件的关系如下:

java 日志体系(四)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)

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

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。

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

4.1 Appender 主要流程

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

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

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

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

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