前言
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) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
已知LoggerFactory的getILoggerFactory() 方法会自动绑定实际使用的日志框架,所以这里getILoggerFactory() 方法会返回Log4jLoggerFactory,其是Log4j2提供的ILoggerFactory接口的实现类。Log4jLoggerFactory继承于AbstractLoggerAdapter,所以继续会调用到AbstractLoggerAdapter的getLogger() 方法,如下所示。
public L getLogger(final String name) {
final LoggerContext context = getContext();
final ConcurrentMap<String, L> loggers = getLoggersInContext(context);
final L logger = loggers.get(name);
if (logger != null) {
return logger;
}
loggers.putIfAbsent(name, newLogger(name, context));
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;
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 = 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 = 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) {
LoggerConfig loggerConfig = loggerConfigs.get(loggerName);
if (loggerConfig != null) {
return loggerConfig;
}
String substr = loggerName;
while ((substr = NameUtil.getSubName(substr)) != null) {
loggerConfig = loggerConfigs.get(substr);
if (loggerConfig != null) {
return loggerConfig;
}
}
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);
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();
}
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) {
final LoggerContext ctx = selector.getContext(fqcn, loader, currentContext);
if (externalContext != null && ctx.getExternalContext() == null) {
ctx.setExternalContext(externalContext);
}
if (ctx.getState() == LifeCycle.State.INITIALIZED) {
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();
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);
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 {
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"?>
<Configuration status="INFO">
<Properties>
<Property name="TEST_LOGGER">com.lee.learn.log4j2.Log4j2Test</Property>
</Properties>
<ContextFilter/>
<Appenders>
<Console name="MyConsole" target="SYSTEM_OUT">
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="%msg%n"/>
</Console>
<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 logIfEnabled(final String fqcn, final Level level,
final Marker marker, final String message) {
if (isEnabled(level, marker, message)) {
logMessage(fqcn, level, marker, message);
}
}
上述方法中的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) {
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 {
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) {
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) {
final LoggerConfig config = getActiveLoggerConfig(reconfigured);
try {
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);
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) {
if (!isFiltered(event)) {
processLogEvent(event, predicate);
}
}
private void processLogEvent(final LogEvent event, final LoggerConfigPredicate predicate) {
event.setIncludeLocation(isIncludeLocation());
if (predicate.allow(this)) {
callAppenders(event);
}
logParent(event, predicate);
}
protected void callAppenders(final LogEvent event) {
final AppenderControl[] controls = appenders.get();
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) {
if (shouldSkip(event)) {
return;
}
callAppenderPreventRecursion(event);
}
private void callAppenderPreventRecursion(final LogEvent event) {
try {
recursive.set(this);
callAppender0(event);
} finally {
recursive.set(null);
}
}
private void callAppender0(final LogEvent event) {
ensureAppenderStarted();
if (!isFilteredByAppender(event)) {
tryCallAppender(event);
}
}
private void tryCallAppender(final LogEvent event) {
try {
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) {
getLayout().encode(event, manager);
if (this.immediateFlush || event.isEndOfBatch()) {
manager.flush();
}
}
ConsoleAppender对日志内容标准输出时,首先会调用ConsoleAppender的Layout(本示例中的Layout为PatternLayout)来完成打印内容的格式化,格式化后的打印内容会被转换为ByteBuffer并存放在OutputStreamManager中,最后会调用OutputStreamManager的flush() 方法完成日志内容的标准输出。
现在再分析一下RollingFileAppender
打印日志。RollingFileAppender的append() 方法如下所示。
public void append(final LogEvent event) {
getManager().checkRollover(event);
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同步打印一条日志进行概括。
原文地址:一文搞懂Log4j2的同步日志打印