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

news2024/11/16 21:20:57

前言

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) {
       
       ILoggerFactory iLoggerFactory = getILoggerFactory();
       
       return iLoggerFactory.getLogger(name);
   }

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

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会对应一个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;
    
    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 = 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 = 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) {
    
    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对象持有的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如下所示。

图片

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

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

图片

简单做一个小结。应用程序中,最终获取到的日志打印器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);
    
    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);
}

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

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

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);
    
    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的启动,实际就是将配置的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"?>


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

}

同时还为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) {
    
    
    
    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父类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) {
    
    
    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) {
    
    final LoggerConfig config = getActiveLoggerConfig(reconfigured);
    try {
        
        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);
    
    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。下面,继续跟进AppenderControlcallAppender() 方法,看一下一条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来打印日志。在本文的示例中,这里实际的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) {
    
    
    getLayout().encode(event, manager);
    if (this.immediateFlush || event.isEndOfBatch()) {
        
        manager.flush();
    }
}

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

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

public void append(final LogEvent event) {
    
    getManager().checkRollover(event);
    
    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同步打印一条日志进行概括。

图片

原文地址:一文搞懂Log4j2的同步日志打印 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1538464.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

无插件网页视频播放器,支持图像绘制(包含方格子、方框等),支持音视频播放、支持录像截图,提供源码下载

前言 本播放器内部采用jessibuca插件接口&#xff0c;支持录像、截图、音视频播放等功能。播放器播放基于ws流&#xff0c;图像绘制操作&#xff1a;1&#xff09;支持绘制方格子&#xff0c;用于监控移动检测画框&#xff1b;2&#xff09;支持绘制不透明方框&#xff0c;用于…

ExoPlayer架构详解与源码分析(11)——DataSource

系列文章目录 ExoPlayer架构详解与源码分析&#xff08;1&#xff09;——前言 ExoPlayer架构详解与源码分析&#xff08;2&#xff09;——Player ExoPlayer架构详解与源码分析&#xff08;3&#xff09;——Timeline ExoPlayer架构详解与源码分析&#xff08;4&#xff09;—…

探索SDK技术架构:构建高效稳定的开发工具

随着移动应用和软件开发的不断发展&#xff0c;SDK&#xff08;Software Development Kit&#xff09;已经成为开发者们日常工作中不可或缺的利器。SDK作为一种开发工具包&#xff0c;能够提供丰富的功能和接口&#xff0c;帮助开发者加快应用开发的速度&#xff0c;提升软件质…

【Web应用技术基础】HTML(6)——案例2:填写简历信息

样式&#xff1a; 代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>填写简历信息&…

探索uni-app项目的架构与开发实践:快速开发的项目模板参考

摘要&#xff1a;本文将深入探讨uni-app项目架构的模板设计&#xff0c;以及如何通过使用该模板实现快速开发。我们将重点介绍模板中的组件示例、SDK示例和模板页面&#xff0c;并阐述它们在提高开发效率和优化用户体验方面的作用。 一、引言 随着移动互联网的迅猛发展&#…

环信新版单群聊UIKit集成指南——Android篇

前言 环信新版UIKit已重磅发布&#xff01;目前包含单群聊UIKit、聊天室ChatroomUIKit&#xff0c;本文详细讲解Android端单群聊UIKit的集成教程。 环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 UI 组件库&#xff0c;提供各种组件实现会话列表、聊天界…

Jetson AGX ORIN 配置 FGVC-PIM 神经网络

Jetson AGX ORIN 配置 FGVC-PIM 神经网络 文章目录 Jetson AGX ORIN 配置 FGVC-PIM 神经网络配置 ORIN 环境创建 FGVC-PIM 虚拟环境安装 PyTorch安装 torchvision安装其他依赖包 配置 ORIN 环境 首先先配置 ORIN 的环境&#xff0c;可以参考这个链接&#xff1a; Jetson AGX …

UE5制作推箱子动作时获取物体与角色朝向的角度及跨蓝图修改变量

就是脑残死磕&#xff0c;你们如果有更好的方法一定要留言啊~~独乐乐不如众乐乐。 做推箱子的时候需要考虑脸是不是面对着箱子&#xff0c;不是必须90度&#xff0c;可以有一个-45~45度的范围。 摸索了一下&#xff0c;有几种做法和几个小白坑&#xff0c;这里列出来。 一、准…

Etcd Raft 协议(进阶篇)

前言 在正式开始介绍 Raft 协议之间&#xff0c;我们有必要简单介绍一下其相关概念。在分布式系统中&#xff0c;一致性是比较常见的概念&#xff0c;所谓一致性指的是集群中的多个节点在状态上达成一致。在程序和操作系统不会崩溃、硬件不会损坏、服务器不会掉电、网络绝对可靠…

电商系统秒杀二 秒杀场景下如何进行限流

本章学习内容 1、在秒杀页面&#xff0c;客户点击秒杀后&#xff0c;在前台弹出一个验证码&#xff0c;需要用户输入验证码才能往后端发送请求&#xff0c;这样能够错开秒杀下单的时间。 2、通过验证码&#xff0c;对后台下单请求进行保护&#xff0c;防止刷单&#xff0c;即防…

产品推荐 | 基于 Xilinx ZU19/ZU17/ZU11-Zyng Ultrascale+SOM 板卡

一、产品描述 iWare推出基于Xilinx FFVC1760封装的Zynq Ultrascale MPSoC系列SOM板卡&#xff0c;完美兼容ZU19/ZU17/ZU11 EG设备&#xff0c;具备卓越性能&#xff0c;最大内存带宽达64位&#xff0c;搭载8GB PS DDR4 RAM并支持ECC&#xff0c;满足高端应用需求。 二、产品参数…

MySQL主键冲突问题分析处理

目录 背景问题分析分析数据分析代码验证分析结果 原因分析验证MySQL参数解决办法修改MySQL配置参数修改代码 背景 因公司业务及预算调整&#xff0c;系统部署从原有云服务提供商迁移到另外一家云服务提供商&#xff0c;在测试新服务能力的时候&#xff0c;发现应用系统某个功能…

总结: HQL语句

总结: HQL语句 Part1 数据库的操作Part2 数据表的操作1. 创建普通表2. 内外部表3. 内外部表转换 Part1 数据库的操作 查看数据库: show databases; 创建数据库: create database if not exists 数据库名 使用数据库: use 数据库名; 查看数据库详细信息: desc database 数据库名…

通过Anaconda安装Python会得到的重要文件夹

E:\Anaconda\路径下 Scripts 文件夹&#xff1a;该文件夹包含了可执行的Python脚本文件&#xff0c;例如pip和conda等命令行工具。【pip3.exe和django-admin.exe等】Lib 文件夹&#xff1a;该文件夹包含了Python的标准库和其他第三方库的源代码文件。【Lib下面的site-packages…

JVM—内存可见性

什么是可见性 可见性&#xff1a;一个线程对共享变量值的修改,能够及时地被其他线程看到共享变量&#xff1a;如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量 Java内存模型(JMM) Java内存模型(Java Memory Model)描述了Java程序中各种…

【滑动窗口】长度最小的子数组|无重复字符的最长子串|最大连续1的个数 III|将 x 减到 0 的最小操作数

1. 长度最小的子数组 - 力扣&#xff08;LeetCode&#xff09; 1.题目解析&#xff1a; 2.算法原理 &#xff08;1&#xff09;方法一&#xff1a;暴力列举出所有的子数组的和 时间复杂度&#xff1a;O&#xff08;n**2&#xff09;&#xff1a;枚举所有子数组O&#xff08;…

Linux信号处理

Linux信号处理 什么是linux信号 本质是一种通知机制&#xff0c;用户 or 操作系统通过发送一定的信号&#xff0c;通知进程&#xff0c;某些事情已经发生&#xff0c;你可以在后续进行处理。 信号产生是随机的&#xff0c;进程可能正在忙自己的事情&#xff0c;所以&#xf…

【LVGL-按钮按钮矩阵部件】

LVGL-按钮&按钮矩阵部件 ■ LVGL-按钮部件■ 按钮部件&#xff1a; 点击三个按钮一个回调函数修改label值。 ■ LVGL-按钮矩阵部件■ 示例一&#xff1a;按钮换行&#xff0c;和宽度设置。■ 示例二&#xff1a;设置按钮宽度为2倍■ 示例三&#xff1a;获取点击的按钮下标&…

Deep_Learning_readme

本文目录 1.环境安装2. A1-1实验&#xff1a;3. A1-2实验&#xff1a;4. A1-3实验 &#xff1a;**5. A2-CNN实验****6. A3-RNN实验** &#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;提示&#xff1a;全程路径和环境名字不…

Linux安装Nacos

安装前必要准备 准备Java环境 &#xff0c;8以上的版本&#xff0c;mysql&#xff08;集群相关信息&#xff09;&#xff0c;nginx&#xff08;进行代理&#xff09; 安装Nacos 首先我们要有一个nacos的包&#xff0c;我们可以在线下载&#xff0c;也可以提前下载好&#xf…