正文
一. 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同步打印一条日志进行概括。