前言
Log4j2
诞生于2012年,是Apache推出用于对标Logback
的日志框架,本篇文章将对Log4j2
的源码进行学习。
Log4j2
版本:2.17.1
正文
一. Logger获取
通常,使用Slf4j
的LoggerFactory
获取Log4j2
的Logger
的代码如下所示。
private static final Logger logger_log4j2
= LoggerFactory.getLogger(LearnLog4j2.class);
跟进LoggerFactory
的getLogger()
方法,如下所示。
public static Logger getLogger(Class<?> clazz) {
Logger logger = getLogger(clazz.getName());
......
return logger;
}
public static Logger getLogger(String name) {
// 使用Log4j2日志框架时,这里得到的ILoggerFactory实际为Log4jLoggerFactory
ILoggerFactory iLoggerFactory = getILoggerFactory();
// 通过Log4jLoggerFactory的getLogger()方法获取Logger
return iLoggerFactory.getLogger(name);
}
已知LoggerFactory
的getILoggerFactory()
方法会自动绑定实际使用的日志框架,所以这里getILoggerFactory()
方法会返回Log4jLoggerFactory
,其是Log4j2
提供的ILoggerFactory
接口的实现类。Log4jLoggerFactory
继承于AbstractLoggerAdapter
,所以继续会调用到AbstractLoggerAdapter
的getLogger()
方法,如下所示。
public L getLogger(final String name) {
// 获取到LoggerContext
final LoggerContext context = getContext();
// 拿到当前LoggerContext关联的Logger的map
final ConcurrentMap<String, L> loggers = getLoggersInContext(context);
// 根据name从Logger的map中拿到Logger
final L logger = loggers.get(name);
if (logger != null) {
// 拿到的Logger不为空则直接返回
return logger;
}
// 拿到的Logger为空则新建一个Logger并存储到Logger的map中
loggers.putIfAbsent(name, newLogger(name, context));
// 然后返回新建的Logger
return loggers.get(name);
}
在AbstractLoggerAdapter
有一个叫做registry的字段,其表示如下。
protected final Map<LoggerContext, ConcurrentMap<String, L>> registry = new ConcurrentHashMap<>();
即一个LoggerContext
会对应一个Logger
的map,AbstractLoggerAdapter
的getLoggersInContext()
方法会将这个map初始化并获取出来,最后从map中根据name字段获取Logger
。
LoggerContext
是Log4j2
中的锚点,其维护了应用请求的所有Logger
的列表和Log4j2
的配置信息Configuration
,LoggerContext
的获取将在下一小节进行分析,现在继续跟进Logger
的创建,即Log4jLoggerFactory
提供的newLogger()
方法,如下所示。
protected Logger newLogger(final String name, final LoggerContext context) {
final String key = Logger.ROOT_LOGGER_NAME.equals(name) ? LogManager.ROOT_LOGGER_NAME : name;
// 创建出来的Logger实际为Log4jLogger
return new Log4jLogger(validateContext(context).getLogger(key), name);
}
在Log4jLoggerFactory
的newLogger()
方法中,创建出来的Logger
实际为Log4jLogger
,同时validateContext()
方法仅会校验一下LoggerContext
的全限定名是否为org.apache.logging.slf4j.SLF4JLoggerContext,如果不是则校验通过,否则报错,校验通过后会返回这个被校验的LoggerContext
,所以继续看LoggerContext
的getLogger()
方法的实现,如下所示。
public Logger getLogger(final String name) {
return getLogger(name, null);
}
public Logger getLogger(final String name, final MessageFactory messageFactory) {
Logger logger = loggerRegistry.getLogger(name, messageFactory);
if (logger != null) {
AbstractLogger.checkMessageFactory(logger, messageFactory);
return logger;
}
// Logger在这里实例化
logger = newInstance(this, name, messageFactory);
loggerRegistry.putIfAbsent(name, messageFactory, logger);
return loggerRegistry.getLogger(name, messageFactory);
}
继续跟进LoggerContext
的newInstance()
方法,如下所示。
protected Logger newInstance(final LoggerContext ctx, final String name, final MessageFactory messageFactory) {
return new Logger(ctx, name, messageFactory);
}
protected Logger(final LoggerContext context, final String name, final MessageFactory messageFactory) {
super(name, messageFactory);
this.context = context;
// 创建PrivateConfig,PrivateConfig是Logger与其配置之间的绑定
privateConfig = new PrivateConfig(context.getConfiguration(), this);
}
在LoggerContext
的newInstance()
方法中会调用Logger
的构造函数,在Logger
的构造函数中,会为Logger
创建一个PrivateConfig
对象,该对象的官方注释直译过来如下所示。
PrivateConfig
是Logger
与其配置之间的绑定。
PrivateConfig
内部持有一个Configuration
对象,和一个LoggerConfig
对象,其中Configuration
对象从LoggerContext
拿到,LoggerConfig
对象从Configuration
中拿到,Logger
会将打印日志的任务委派给其持有的PrivateConfig
中的LoggerConfig
对象,所以真正打印日志的对象是LoggerConfig
。
下面继续跟进如何从Configuration
对象中获取到LoggerConfig
,由于是通过XML文件进行配置,所以Configuration
对象的实际类型是XmlConfiguration
,调用XmlConfiguration
的getLoggerConfig()
方法时会实际调用到其父类AbstractConfiguration
的getLoggerConfig()
方法,实现如下所示。
public LoggerConfig getLoggerConfig(final String loggerName) {
// loggerConfigs是一个map结构,key是Logger的name,value是LoggerConfig
LoggerConfig loggerConfig = loggerConfigs.get(loggerName);
if (loggerConfig != null) {
// 根据loggerName能够直接从map中获取到LoggerConfig,那么直接返回这个LoggerConfig
return loggerConfig;
}
// 如果获取不到,那么循环的通过Logger的name找到父name的LoggerConfig
// 比如loggerName为A.B.C.D,且直接根据A.B.C.D从map中获取不到LoggerConfig
// 那么会循环的依次以A.B.C,A.B,A从map中获取LoggerConfig
String substr = loggerName;
while ((substr = NameUtil.getSubName(substr)) != null) {
loggerConfig = loggerConfigs.get(substr);
if (loggerConfig != null) {
return loggerConfig;
}
}
// 如果都获取不到LoggerConfig,那么返回根LoggerConfig,即root
return root;
}
从Configuration
对象中获取LoggerConfig
时,就是从Configuration
对象持有的LoggerConfig
的map中根据loggerName将LoggerConfig
获取出来,如果获取不到,则依次以loggerName的父name去从map中获取,如果都获取不到,则会使用根LoggerConfig
。下面以一个简单例子说明这个获取过程,假如配置文件中,<Configuration>
标签下的<Loggers>
标签配置如下所示。
<Loggers>
<Logger name="com.lee" level="INFO"/>
<Root level="INFO">
<appender-ref ref="MyConsole"/>
<appender-ref ref="MyFile"/>
</Root>
</Loggers>
那么Configuration
对象持有的loggerConfigs如下所示。
那么一开始以com.lee.learn.log4j2.Log4j2Test
去loggerConfigs获取LoggerConfig
时会获取不到,然后再以com.lee.learn.log4j2
去获取,还是获取不到,依次直到以com.learn
去获取,此时会获取到。
至此,Logger
的获取分析完毕,由于在整个过程中,出现了多个Logger
,下面以一个类图进行概括示意,展示这些Logger
之间的关联关系。
简单做一个小结,应用程序中,最终获取到的日志打印器Logger
就是org.apache.logging.slf4j.Log4jLogger
,Log4jLogger
会将打印日志的任务委派给其持有的org.apache.logging.log4j.core.Logger
中的PrivateConfig
,而PrivateConfig
又会将打印日志的任务委派给其持有的LoggerConfig
,所以实际执行打印任务的是LoggerConfig
。同时,每个日志打印器都有独属于自己的PrivateConfig
,而Configuration
则是全局共享的。最后LoggerConfig
还有一个成员变量为parent,在LoggerConfig
完成日志打印后,还会判断是否让其parent也打印日志,这个在第三节会详细进行说明。
二. LoggerContext获取
上一节中已知,LoggerContext
是Log4j2
中的锚点,其维护了应用请求的所有Logger
的列表和Log4j2
的配置信息Configuration
。LoggerContext
是在Log4jLoggerFactory
的getContext()
方法中进行获取的,其实现如下所示。
protected LoggerContext getContext() {
final Class<?> anchor = LogManager.getFactory().isClassLoaderDependent()
? StackLocatorUtil.getCallerClass(Log4jLoggerFactory.class, CALLER_PREDICATE)
: null;
LOGGER.trace("Log4jLoggerFactory.getContext() found anchor {}", anchor);
// 调用到AbstractLoggerAdapter的getContext()方法
return anchor == null
? LogManager.getContext(false)
: getContext(anchor);
}
protected LoggerContext getContext(final Class<?> callerClass) {
ClassLoader cl = null;
if (callerClass != null) {
// 拿到调用类的类加载器
cl = callerClass.getClassLoader();
}
if (cl == null) {
cl = LoaderUtil.getThreadContextClassLoader();
}
// 调用LogManager的getContext()方法来获取LoggerContext
return LogManager.getContext(cl, false);
}
在Log4jLoggerFactory
的getContext()
方法中会调用到父类AbstractLoggerAdapter
的getContext()
方法,然后最后会调用到LogManager
的getContext()
方法,跟进如下所示。
public static LoggerContext getContext(final ClassLoader loader, final boolean currentContext) {
try {
return factory.getContext(FQCN, loader, null, currentContext);
} catch (final IllegalStateException ex) {
LOGGER.warn(ex.getMessage() + " Using SimpleLogger");
return new SimpleLoggerContextFactory()
.getContext(FQCN, loader, null, currentContext);
}
}
上述方法中的factory字段是LogManager
持有的LoggerContextFactory
静态成员变量,在静态代码块中完成初始化,其实际类型是Log4jContextFactory
,用于加载LoggerContext
,现在继续跟进Log4jContextFactory
的getContext()
方法,如下所示。
public LoggerContext getContext(final String fqcn, final ClassLoader loader, final Object externalContext,
final boolean currentContext) {
// 使用selector选择得到一个LoggerContext
final LoggerContext ctx = selector.getContext(fqcn, loader, currentContext);
if (externalContext != null && ctx.getExternalContext() == null) {
ctx.setExternalContext(externalContext);
}
// 第一次获取到的LoggerContext的state为INITIALIZED
if (ctx.getState() == LifeCycle.State.INITIALIZED) {
// 调用LoggerContext的start()方法以启动LoggerContext
// LoggerContext启动完毕后,其状态会被设置为STARTED
ctx.start();
}
return ctx;
}
Log4jContextFactory
的getContext()
方法会首先使用其持有的selector来选择获取一个LoggerContext
,如果是第一次获取LoggerContext
,那么获取到的LoggerContext
的state为INITIALIZED,此时会调用LoggerContext
的start()
方法来启动LoggerContext
,启动完毕后LoggerContext
的state会被设置为STARTED。
public void start() {
......
if (configLock.tryLock()) {
try {
if (this.isInitialized() || this.isStopped()) {
this.setStarting();
// 为LogContext设置配置信息
reconfigure();
if (this.configuration.isShutdownHookEnabled()) {
setUpShutdownHook();
}
this.setStarted();
}
} finally {
configLock.unlock();
}
}
......
}
LoggerContext
的start()
方法中很重要的就是为LoggerContext
设置配置信息,跟进reconfigure()
方法,如下所示。
private void reconfigure(final URI configURI) {
Object externalContext = externalMap.get(EXTERNAL_CONTEXT_KEY);
final ClassLoader cl = ClassLoader.class.isInstance(externalContext) ? (ClassLoader) externalContext : null;
LOGGER.debug("Reconfiguration started for context[name={}] at URI {} ({}) with optional ClassLoader: {}",
contextName, configURI, this, cl);
// 找到配置文件并解析为Configuration对象
final Configuration instance = ConfigurationFactory.getInstance().getConfiguration(this, contextName, configURI, cl);
if (instance == null) {
LOGGER.error("Reconfiguration failed: No configuration found for '{}' at '{}' in '{}'", contextName, configURI, cl);
} else {
// 为LoggerContext设置Configuration,然后启动Configuration
setConfiguration(instance);
final String location = configuration == null ? "?" : String.valueOf(configuration.getConfigurationSource());
LOGGER.debug("Reconfiguration complete for context[name={}] at URI {} ({}) with optional ClassLoader: {}",
contextName, location, this, cl);
}
}
reconfigure()
方法中会先找到配置文件并解析为Configuration
对象,然后为LoggerContext
设置Configuration
,在设置的过程中,还会一并将Configuration
启动起来,而Configuration
的启动,实际就是将配置的Appender
,Logger(LoggerConfig)
,根Logger(LoggerConfig)
等都创建出来并启动。
LoggerContext
启动完成后,就可以返回了。至此,LoggerContext
获取的流程分析完毕。下面对本节内容进行小结。
LoggerContext
是Log4j2
中的锚点,其维护了应用请求的所有Logger
的列表和Log4j2
的配置信息Configuration
;- 首次获取得到的
LoggerContext
的状态为INITIALIZED,此时需要调用LoggerContext
的start()
方法来启动LoggerContext
; LoggerContext
启动实际就是找到Log4j2
配置文件并解析为Configuration
,然后将Configuration
设置给LoggerContext
,在设置过程中,还会将Configuration
启动,也就是将配置的Appender
,Logger
等创建出来,如果有异步的Appender
,Logger
等,还需要将这些异步组件的线程启动起来;LoggerContext
启动完毕后,就可以返回了。
三. Logger打印日志
本小节对Logger
打印一条日志的完整流程进行分析。正式分析前,需要进行一些准备工作。
首先给出下面一份Log4j2
的配置文件,如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<!-- status用于设置log4j2自身日志的信息输出级别,不配置时默认为OFF -->
<Configuration status="INFO">
<Properties>
<Property name="TEST_LOGGER">com.lee.learn.log4j2.Log4j2Test</Property>
</Properties>
<!-- 全局过滤器 -->
<ContextFilter/>
<!-- Appenders用于定义日志输出到哪里,其下常用三种子标签<Console>,<File>和<RollingFile> -->
<!-- 通过定义Appender,可以定义输出日志到哪里,输出日志级别,日志保存清理策略等 -->
<Appenders>
<!-- Console,表示控制台信息输出配置 -->
<Console name="MyConsole" target="SYSTEM_OUT">
<!-- ThresholdFilter,用于定义过滤机制 -->
<!-- level属性定义日志过滤级别 -->
<!-- onMatch="ACCEPT"表示保留level及以上级别的日志 -->
<!-- onMismatch="DENY"表示丢掉level以下级别的日志-->
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
<!-- PatternLayout,用于定义日志的输出格式 -->
<PatternLayout pattern="%msg%n"/>
</Console>
<!-- 输出WARN级别及以上的日志信息到日志文件 -->
<RollingFile name="MyFile" fileName="mylog.log"
filePattern="mylog.log.%i">
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="%msg%n"/>
<SizeBasedTriggeringPolicy size="20M"/>
</RollingFile>
</Appenders>
<Loggers>
<!-- 根日志打印器 -->
<Root level="INFO">
<Appender-ref ref="MyConsole"/>
<Appender-ref ref="MyFile"/>
</Root>
<!-- 自定义日志打印器 -->
<Logger name="${TEST_LOGGER}" level="INFO" additivity="false">
<LoggerFilter/>
<Appender-ref ref="MyConsole">
<AppenderRefFilter/>
</Appender-ref>
<Appender-ref ref="MyFile">
<AppenderRefFilter/>
</Appender-ref>
</Logger>
</Loggers>
</Configuration>
上述配置文件中,为Context
设置了一个全局的自定义过滤器ContextFilter
,为TEST_LOGGER设置了一个Logger
级别的自定义过滤器LoggerFilter
以及一个Appender-ref
级别的自定义过滤器AppenderRefFilter
,这些自定义过滤器的实现如下。
@Plugin(name = "ContextFilter", category = Node.CATEGORY,
elementType = Filter.ELEMENT_TYPE, printObject = true)
public class MyContextFilter extends AbstractFilter {
public MyContextFilter() {
super();
}
@PluginFactory
public static MyContextFilter createFilter() {
return new MyContextFilter();
}
@Override
public Result filter(LogEvent event) {
return Result.ACCEPT;
}
@Override
public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) {
return Result.ACCEPT;
}
@Override
public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) {
return Result.ACCEPT;
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String msg, Object... params) {
return Result.ACCEPT;
}
}
@Plugin(name = "LoggerFilter", category = Node.CATEGORY,
elementType = Filter.ELEMENT_TYPE, printObject = true)
public class MyLoggerFilter extends AbstractFilter {
public MyLoggerFilter() {
super();
}
@PluginFactory
public static MyLoggerFilter createFilter() {
return new MyLoggerFilter();
}
@Override
public Result filter(LogEvent event) {
return Result.ACCEPT;
}
@Override
public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) {
return Result.ACCEPT;
}
@Override
public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) {
return Result.ACCEPT;
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String msg, Object... params) {
return Result.ACCEPT;
}
}
@Plugin(name = "AppenderRefFilter", category = Node.CATEGORY,
elementType = Filter.ELEMENT_TYPE, printObject = true)
public class MyAppenderRefFilter extends AbstractFilter {
public MyAppenderRefFilter() {
super();
}
@PluginFactory
public static MyAppenderRefFilter createFilter() {
return new MyAppenderRefFilter();
}
@Override
public Result filter(LogEvent event) {
return Result.ACCEPT;
}
@Override
public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) {
return Result.ACCEPT;
}
@Override
public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) {
return Result.ACCEPT;
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String msg, Object... params) {
return Result.ACCEPT;
}
}
同时还为MyConsole和MyFile这两个自定义Appender
设置了Log4j2
预置的过滤器ThresholdFilter
,至此,准备工作完毕。
现在开始分析一条日志的打印流程。已知使用Log4j2
框架时,打印日志的日志打印器类型为Log4jLogger
,以打印INFO级别日志为例,看一下Log4jLogger
的info()
方法实现。
public void info(final String format) {
logger.logIfEnabled(FQCN, Level.INFO, null, format);
}
上述方法中的FQCN取值为打印日志的入口的日志打印器的全限定名,这里为org.apache.logging.slf4j.Log4jLogger
,表示打印日志的入口的日志打印器是Log4jLogger
,后续可以用于去堆栈中匹配打印日志的代码行号。已知Log4jLogger
持有一个类型为org.apache.logging.log4j.core.Logger
的日志打印器,打印日志会由Logger
来完成,继续看Logger
(实际是Logger
父类AbstractLogger
)的logIfEnabled()
方法,如下所示。
public void logIfEnabled(final String fqcn, final Level level,
final Marker marker, final String message) {
if (isEnabled(level, marker, message)) {
logMessage(fqcn, level, marker, message);
}
}
在上述方法中,首先会调用到Logger
的isEnabled()
方法来过滤日志,在这里会使用到为Context
配置的filter来过滤日志,比如本小节一开始时在配置文件中为Context
配置的ContextFilter
,而ContextFilter
则是通过Logger
中的PrivateConfig
中的Configuration
来拿到的。继续看Logger
(实际是Logger
父类AbstractLogger
)的logMessage()
方法,如下所示。
protected void logMessage(final String fqcn, final Level level, final Marker marker, final String message) {
// 将日志打印内容包装为Message的实现类对象
// 例如只是简单打印一条字符串日志内容,这里的Message为ReusableSimpleMessage
// 例如打印的内容使用了占位符,这里的Message为ReusableParameterizedMessage,用于处理占位符
final Message msg = messageFactory.newMessage(message);
logMessageSafely(fqcn, level, marker, msg, msg.getThrowable());
}
private void logMessageSafely(final String fqcn, final Level level, final Marker marker, final Message message,
final Throwable throwable) {
try {
logMessageTrackRecursion(fqcn, level, marker, message, throwable);
} finally {
ReusableMessageFactory.release(message);
}
}
private void logMessageTrackRecursion(final String fqcn,
final Level level,
final Marker marker,
final Message message,
final Throwable throwable) {
try {
incrementRecursionDepth();
tryLogMessage(fqcn, getLocation(fqcn), level, marker, message, throwable);
} finally {
decrementRecursionDepth();
}
}
private void tryLogMessage(final String fqcn,
final StackTraceElement location,
final Level level,
final Marker marker,
final Message message,
final Throwable throwable) {
try {
// 在这里调用到Logger实现的log()方法
log(level, marker, fqcn, location, message, throwable);
} catch (final Throwable t) {
handleLogMessageException(t, fqcn, message);
}
}
Logger
父类AbstractLogger
的logMessage()
方法的整体的一个调用如上所示,其中值得关注的是需要打印的日志被包装成了Message
接口的子类,例如打印的简单字符串内容会被包装成ReusableSimpleMessage
对象,使用了占位符的打印内容则会被包装成ReusableParameterizedMessage
对象。Logger
父类AbstractLogger
的logMessage()
方法一路调用下来,最后会调用到其子类Logger
实现的log()
方法,如下所示。
protected void log(final Level level, final Marker marker, final String fqcn, final StackTraceElement location,
final Message message, final Throwable throwable) {
// 从Logger中的PrivateConfig中的LoggerConfig中获取到ReliabilityStrategy,其实际类型为AwaitCompletionReliabilityStrategy
// ReliabilityStrategy会将待打印的Message传递给对应的Appender进行日志打印
final ReliabilityStrategy strategy = privateConfig.loggerConfig.getReliabilityStrategy();
if (strategy instanceof LocationAwareReliabilityStrategy) {
((LocationAwareReliabilityStrategy) strategy).log(this, getName(), fqcn, location, marker, level,
message, throwable);
} else {
strategy.log(this, getName(), fqcn, marker, level, message, throwable);
}
}
在Logger
实现的log()
方法中,会从Logger
中的PrivateConfig
中的LoggerConfig
中获取到AwaitCompletionReliabilityStrategy
,同时AwaitCompletionReliabilityStrategy
也持有LoggerConfig
的引用。继续看AwaitCompletionReliabilityStrategy
的log()
方法,如下所示。
public void log(final Supplier<LoggerConfig> reconfigured, final String loggerName, final String fqcn,
final StackTraceElement location, final Marker marker, final Level level, final Message data,
final Throwable t) {
// 拿到LoggerConfig
final LoggerConfig config = getActiveLoggerConfig(reconfigured);
try {
// 打印日志是交由LoggerConfig完成
config.log(loggerName, fqcn, location, marker, level, data, t);
} finally {
config.getReliabilityStrategy().afterLogEvent();
}
}
由上可知,打印日志是由LoggerConfig
来完成,继续跟进LoggerConfig
的log()
方法,如下所示。
public void log(final String loggerName, final String fqcn, final StackTraceElement location, final Marker marker,
final Level level, final Message data, final Throwable t) {
final List<Property> props = getProperties(loggerName, fqcn, marker, level, data, t);
// 将本次日志打印包装成一个打印事件LogEvent
final LogEvent logEvent = logEventFactory.createEvent(loggerName, marker, fqcn, location, level, data, props, t);
try {
// 这里第二个参数用于同步和异步打印的判断
log(logEvent, LoggerConfigPredicate.ALL);
} finally {
ReusableLogEventFactory.release(logEvent);
}
}
protected void log(final LogEvent event, final LoggerConfigPredicate predicate) {
// 在这里使用Logger级别过滤器进行过滤
if (!isFiltered(event)) {
processLogEvent(event, predicate);
}
}
private void processLogEvent(final LogEvent event, final LoggerConfigPredicate predicate) {
// 因为是同步打印日志,isIncludeLocation()方法会返回true,表示需要打印行号信息
// 如果是异步打印日志,则需要在<AsyncLogger>标签中配置includeLocation="true",isIncludeLocation()方法才会返回true
event.setIncludeLocation(isIncludeLocation());
// 同步和异步打印的判断
if (predicate.allow(this)) {
callAppenders(event);
}
// 如果当前日志打印器有父日志打印器,且additivity属性为true,则调用父日志打印器来打印日志内容
logParent(event, predicate);
}
protected void callAppenders(final LogEvent event) {
// 当前日志打印器的Appender会被封装成AppenderControl
final AppenderControl[] controls = appenders.get();
// 遍历当前日志打印器的Appender,并执行相应的打印逻辑
for (int i = 0; i < controls.length; i++) {
controls[i].callAppender(event);
}
}
通过上述源码可知,LoggerConfig
打印日志时,会首先将本地日志打印包装成一个打印事件LogEvent
,然后使用当前日志打印器的过滤器(如果有的话)进行过滤,再然后就会遍历当前日志打印器的Appender
来打印日志,最后,在所有Appender
打印完日志后,还会判断当前日志打印器是否有父日志打印器,如果有父日志打印器并且当前日志打印器的additivity属性为true,则还会调用父日志打印器打印一次日志,所以如果不想一条日志打印多次,则最好在自己的自定义日志打印器中将additivity属性设置为false。下面,继续跟进AppenderControl
的callAppender()
方法,看一下一条LogEvent
日志是如何被Appender
打印的。
public void callAppender(final LogEvent event) {
// 在这里会使用Appender-ref级别的日志过滤器进行过滤
if (shouldSkip(event)) {
return;
}
callAppenderPreventRecursion(event);
}
private void callAppenderPreventRecursion(final LogEvent event) {
try {
// recursive是一个ThreadLocal变量
// 这里将AppenderControl对象自身放到ThreadLocal中,是用于判断是否有对Appender递归的调用
recursive.set(this);
callAppender0(event);
} finally {
recursive.set(null);
}
}
private void callAppender0(final LogEvent event) {
ensureAppenderStarted();
// 在这里会使用Appender级别的日志过滤器进行过滤
if (!isFilteredByAppender(event)) {
tryCallAppender(event);
}
}
private void tryCallAppender(final LogEvent event) {
try {
// 让AppenderControl持有的Appender来进行日志打印
appender.append(event);
} catch (final RuntimeException error) {
handleAppenderError(event, error);
} catch (final Throwable throwable) {
handleAppenderError(event, new AppenderLoggingException(throwable));
}
}
由上可知,AppenderControl
会依次使用Appender-ref
级别和Appender
级别的过滤器进行过滤,然后调用到实际的Appender
来打印日志。在本文的示例中,这里实际的Appender
有ConsoleAppender
进行标准日志打印和RollingFileAppender
进行文件写入。下面简单的对ConsoleAppender
和RollingFileAppender
打印日志进行一个说明。
首先是ConsoleAppender
,其将日志内容进行标准输出主要是依赖于其父类AbstractOutputStreamAppender
,AbstractOutputStreamAppender
的append()
方法如下所示。
public void append(final LogEvent event) {
try {
tryAppend(event);
} catch (final AppenderLoggingException ex) {
error("Unable to write to stream " + manager.getName() + " for appender " + getName(), event, ex);
throw ex;
}
}
private void tryAppend(final LogEvent event) {
if (Constants.ENABLE_DIRECT_ENCODERS) {
directEncodeEvent(event);
} else {
writeByteArrayToManager(event);
}
}
protected void directEncodeEvent(final LogEvent event) {
// 使用Appender的Layout对打印内容进行格式化,然后转换为ByteBuffer并存放在manager中
// 这里的manager实际类型为OutputStreamManager
getLayout().encode(event, manager);
if (this.immediateFlush || event.isEndOfBatch()) {
// 调用OutputStreamManager的flush()方法完成最终日志内容的标准输出
manager.flush();
}
}
ConsoleAppender
对日志内容标准输出时,首先会调用ConsoleAppender
的Layout
(本示例中的Layout
为PatternLayout
)来完成打印内容的格式化,格式化后的打印内容会被转换为ByteBuffer
并存放在OutputStreamManager
中,最后会调用OutputStreamManager
的flush()
方法完成日志内容的标准输出。
现在再分析一下RollingFileAppender
打印日志。RollingFileAppender
的append()
方法如下所示。
public void append(final LogEvent event) {
// 这里先拿到RollingFileManager来根据配置的各种触发策略来判断日志文件是否需要翻转
getManager().checkRollover(event);
// 调用父类AbstractOutputStreamAppender的append()方法完成日志写入文件
super.append(event);
}
RollingFileAppender
将日志内容写入文件时,会先根据配置的各种翻转策略来判断当前是否已经达到翻转条件,如果达到则翻转日志文件,然后再调用到AbstractOutputStreamAppender
的append()
方法来完成日志内容的格式化以及写磁盘,AbstractOutputStreamAppender
的这部分逻辑与ConsoleAppender
中的逻辑是一致的,这里不再赘述。
至此,一条日志内容的打印流程分析完毕。这里进行一个小结,即一条日志的打印,会由一开始业务代码中的Logger
,依次传递给ReliabilityStrategy
,LoggerConfig
,Appender
和Layout
,最后由Manager
将日志进行刷盘打印。
总结
本篇文章主要是从使用Log4j2
日志框架时,日志打印器Logger
的获取,LoggerContext
的获取以及实际一条日志的打印进行的讨论。
对于日志打印器Logger
的获取,我们在应用程序中,获取到的日志打印器实际就是org.apache.logging.slf4j.Log4jLogger
,但是实际打印日志是由其持有的LoggerConfig
来完成,而LoggerConfig
是有一个父子层级的关系在里面,也就是说name为com.lee.learn
的日志打印器的LoggerConfig
会优先取name为com.lee.learn
,com.lee
和com
的日志打印器的LoggerConfig
,如果都没有在配置文件中为这些日志打印器配置LoggerConfig
,则最后会将根日志打印器的LoggerConfig
作为当前日志打印器的LoggerConfig
。所以假如在log4j2.xml文件中的<Loggers>标签的内容如下。
<Loggers>
<!-- 根日志打印器 -->
<Root level="INFO">
<Appender-ref ref="MyConsole"/>
<Appender-ref ref="MyFile"/>
</Root>
</Loggers>
那么相当于整个配置Configuration
中只有根日志打印器的LoggerConfig
,那么所有日志打印器都将使用根日志打印器的LoggerConfig
。同时每一个LoggerConfig
打印完日志后,还会判断其父LoggerConfig
是否需要也打印这条日志,判断的依据就是当前LoggerConfig
的additive属性为true还是false,这个属性值可以通过 <Logger additivity="false">
来设置。
LoggerContext
实际维护了应用请求的所有Logger
的列表和Log4j2
的配置信息Configuration
,LoggerContext
会在应用首次请求Logger
时完成初始化。
当在应用程序中使用Logger
打印一条日志时,这个打印的请求会由Logger
的LoggerConfig
来完成,LoggerConfig
中有一个appenders字段,其是AppenderControl
的集合,而AppenderControl
就是一个Appender
的包装类,所以LoggerConfig
打印日志时,会遍历AppenderControl
的集合,每个AppenderControl
会使用其包装的Appender
来打印日志,当Appender
打印一条日志时,会先使用Appender
的Layout
来完成打印内容的格式化,最后完成刷盘打印。
最后,本篇文章讨论的日志打印均为同步日志打印,即应用线程和打印日志的线程为同一线程,以一张流程示意图对Log4j2
同步打印一条日志进行概括。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。