一文搞懂Log4j2的同步日志打印

1,706 阅读13分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情

前言

Log4j2诞生于2012年,是Apache推出用于对标Logback的日志框架,本篇文章将对Log4j2的同步日志打印的源码进行学习。

Log4j2版本:2.17.1

正文

一. Logger获取

通常,使用Slf4jLoggerFactory获取Log4j2Logger的代码如下所示。

private static final Logger logger_log4j2= LoggerFactory.getLogger(LearnLog4j2.class);

跟进LoggerFactorygetLogger() 方法,如下所示。

   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);
   }

已知LoggerFactorygetILoggerFactory() 方法会自动绑定实际使用的日志框架,所以这里getILoggerFactory() 方法会返回Log4jLoggerFactory,其是Log4j2提供的ILoggerFactory接口的实现类。Log4jLoggerFactory继承于AbstractLoggerAdapter,所以继续会调用到AbstractLoggerAdaptergetLogger() 方法,如下所示。

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会对应一个LoggermapAbstractLoggerAdaptergetLoggersInContext() 方法会将这个map初始化并获取出来,最后从map中根据name字段获取Logger

LoggerContextLog4j2中的锚点,其维护了应用请求的所有Logger的列表和Log4j2的配置信息ConfigurationLoggerContext的获取将在下一小节进行分析,现在继续跟进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);
}

Log4jLoggerFactorynewLogger() 方法中,创建出来的Logger实际为Log4jLogger,同时validateContext() 方法仅会校验一下LoggerContext 的全限定名是否为org.apache.logging.slf4j.SLF4JLoggerContext,如果不是则校验通过,否则报错,校验通过后会返回这个被校验的LoggerContext,所以继续看LoggerContextgetLogger() 方法的实现,如下所示。

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);
}

继续跟进LoggerContextnewInstance() 方法,如下所示。

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);
}

LoggerContextnewInstance() 方法中会调用Logger的构造函数,在Logger的构造函数中,会为Logger创建一个PrivateConfig对象,该对象的官方注释直译过来如下所示。

PrivateConfigLogger与其配置之间的绑定。

PrivateConfig内部持有一个Configuration对象,和一个LoggerConfig对象,其中Configuration对象从LoggerContext拿到,LoggerConfig对象从Configuration中拿到,Logger会将打印日志的任务委派给其持有的PrivateConfig中的LoggerConfig对象,所以真正打印日志的对象是LoggerConfig

下面继续跟进如何从Configuration对象中获取到LoggerConfig,由于是通过XML文件进行配置,所以Configuration对象的实际类型是XmlConfiguration,调用XmlConfigurationgetLoggerConfig() 方法时会实际调用到其父类AbstractConfigurationgetLoggerConfig() 方法,实现如下所示。

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对象持有的LoggerConfigmap中根据loggerNameLoggerConfig获取出来,如果获取不到,则依次以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如下所示。

Configuration对象持有的logggerConfigs

那么一开始以com.lee.learn.log4j2.Log4j2TestloggerConfigs获取LoggerConfig时会获取不到,然后再以com.lee.learn.log4j2去获取,还是获取不到,依次直到以com.learn去获取,此时会获取到。

至此,Logger的获取分析完毕,由于在整个过程中,出现了多个Logger,下面以一个类图进行概括示意,展示这些Logger之间的关联关系。

Log-Log4j2日志打印器结构类图

简单做一个小结。应用程序中,最终获取到的日志打印器Logger就是org.apache.logging.slf4j.Log4jLoggerLog4jLogger会将打印日志的任务委派给其持有的org.apache.logging.log4j.core.Logger中的PrivateConfig,而PrivateConfig又会将打印日志的任务委派给其持有的LoggerConfig,所以实际执行打印任务的是LoggerConfig。同时,每个日志打印器都有独属于自己的PrivateConfig,而Configuration则是全局共享的。最后LoggerConfig还有一个成员变量为parent,在LoggerConfig完成日志打印后,还会判断是否让其parent也打印日志,这个在第三节会详细进行说明。

二. LoggerContext获取

上一节中已知,LoggerContextLog4j2中的锚点,其维护了应用请求的所有Logger的列表和Log4j2的配置信息ConfigurationLoggerContext是在Log4jLoggerFactorygetContext() 方法中进行获取的,其实现如下所示。

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);
}

Log4jLoggerFactorygetContext() 方法中会调用到父类AbstractLoggerAdaptergetContext() 方法,然后最后会调用到LogManagergetContext() 方法,跟进如下所示。

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,现在继续跟进Log4jContextFactorygetContext() 方法,如下所示。

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;
}

Log4jContextFactorygetContext() 方法会首先使用其持有的selector来选择获取一个LoggerContext,如果是第一次获取LoggerContext,那么获取到的LoggerContextstateINITIALIZED,此时会调用LoggerContextstart() 方法来启动LoggerContext,启动完毕后LoggerContextstate会被设置为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();
        }
    }
    
    // 省略
    
}

LoggerContextstart() 方法中很重要的就是为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的启动,实际就是将配置的AppenderLogger(LoggerConfig),根Logger(LoggerConfig) 等都创建出来并启动。

LoggerContext启动完成后,就可以返回了。至此,LoggerContext获取的流程分析完毕。下面对本节内容进行小结。

  • LoggerContextLog4j2中的锚点,其维护了应用请求的所有Logger的列表和Log4j2的配置信息Configuration
  • 首次获取得到的LoggerContext的状态为INITIALIZED,此时需要调用LoggerContextstart() 方法来启动LoggerContext
  • LoggerContext启动实际就是找到Log4j2配置文件并解析为Configuration,然后将Configuration设置给LoggerContext,在设置过程中,还会将Configuration启动,也就是将配置的AppenderLogger等创建出来,如果有异步的AppenderLogger等,还需要将这些异步组件的线程启动起来;
  • 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;
    }

}

同时还为MyConsoleMyFile这两个自定义Appender设置了Log4j2预置的过滤器ThresholdFilter,至此,准备工作完毕。

现在开始分析一条日志的打印流程。已知使用Log4j2框架时,打印日志的日志打印器类型为Log4jLogger,以打印INFO级别日志为例,看一下Log4jLoggerinfo() 方法实现。

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);
    }
}

在上述方法中,首先会调用到LoggerisEnabled() 方法来过滤日志,在这里会使用到为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父类AbstractLoggerlogMessage() 方法的整体的一个调用如上所示,其中值得关注的是需要打印的日志被包装成了Message接口的子类,例如打印的简单字符串内容会被包装成ReusableSimpleMessage对象,使用了占位符的打印内容则会被包装成ReusableParameterizedMessage对象。Logger父类AbstractLoggerlogMessage() 方法一路调用下来,最后会调用到其子类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的引用。继续看AwaitCompletionReliabilityStrategylog() 方法,如下所示。

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来完成,继续跟进LoggerConfiglog() 方法,如下所示。

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。下面,继续跟进AppenderControlcallAppender() 方法,看一下一条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来打印日志。在本文的示例中,这里实际的AppenderConsoleAppender进行标准日志打印和RollingFileAppender进行文件写入。下面简单的对ConsoleAppenderRollingFileAppender打印日志进行一个说明。

首先是ConsoleAppender,其将日志内容进行标准输出主要是依赖于其父类AbstractOutputStreamAppenderAbstractOutputStreamAppenderappend() 方法如下所示。

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对日志内容标准输出时,首先会调用ConsoleAppenderLayout(本示例中的LayoutPatternLayout)来完成打印内容的格式化,格式化后的打印内容会被转换为ByteBuffer并存放在OutputStreamManager中,最后会调用OutputStreamManagerflush() 方法完成日志内容的标准输出。

现在再分析一下RollingFileAppender打印日志。RollingFileAppenderappend() 方法如下所示。

public void append(final LogEvent event) {
    // 这里先拿到RollingFileManager来根据配置的各种触发策略来判断日志文件是否需要翻转
    getManager().checkRollover(event);
    // 调用父类AbstractOutputStreamAppender的append()方法完成日志写入文件
    super.append(event);
}

RollingFileAppender将日志内容写入文件时,会先根据配置的各种翻转策略来判断当前是否已经达到翻转条件,如果达到则翻转日志文件,然后再调用到AbstractOutputStreamAppenderappend() 方法来完成日志内容的格式化以及写磁盘,AbstractOutputStreamAppender的这部分逻辑与ConsoleAppender中的逻辑是一致的,这里不再赘述。

至此,一条日志内容的打印流程分析完毕。这里进行一个小结,即一条日志的打印,会由一开始业务代码中的Logger,依次传递给ReliabilityStrategyLoggerConfigAppenderLayout,最后由Manager将日志进行刷盘打印。

总结

本篇文章主要是从使用Log4j2日志框架时,日志打印器Logger的获取,LoggerContext的获取以及实际一条日志的打印进行的讨论。

对于日志打印器Logger的获取,我们在应用程序中,获取到的日志打印器实际就是org.apache.logging.slf4j.Log4jLogger,但是实际打印日志是由其持有的LoggerConfig来完成,而LoggerConfig是有一个父子层级的关系在里面,也就是说namecom.lee.learn的日志打印器的LoggerConfig会优先取namecom.lee.learncom.leecom的日志打印器的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是否需要也打印这条日志,判断的依据就是当前LoggerConfigadditive属性为true还是false,这个属性值可以通过<Logger additivity="false">来设置。

LoggerContext实际维护了应用请求的所有Logger的列表和Log4j2的配置信息ConfigurationLoggerContext会在应用首次请求Logger时完成初始化。

当在应用程序中使用Logger打印一条日志时,这个打印的请求会由LoggerLoggerConfig来完成,LoggerConfig中有一个appenders字段,其是AppenderControl的集合,而AppenderControl就是一个Appender的包装类,所以LoggerConfig打印日志时,会遍历AppenderControl的集合,每个AppenderControl会使用其包装的Appender来打印日志,当Appender打印一条日志时,会先使用AppenderLayout来完成打印内容的格式化,最后完成刷盘打印。

最后,本篇文章讨论的日志打印均为同步日志打印,即应用线程和打印日志的线程为同一线程,以一张流程示意图对Log4j2同步打印一条日志进行概括。

同步日志打印流程


开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情