Logback source code analysis

In the daily development step is often performed by print logging program or troubleshoot the problem, a lot of code like the following, but it is how to implement it?

package chapters;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// 省略...
Logger logger = LoggerFactory.getLogger(LogbackTest.class);
logger.info(" {} is best player in world", "Greizmann");

This paper analyzes Logback logging framework to achieve the above code.

slf4j

Now commonly used in logging framework log4j, log4j2, jul (common-log) and logback. If used in the project is jul, and now would like to change with log4j, if a direct reference to the java.util.logging package Logger, you need to modify a lot of code, in order to solve this difficult task, Ceki Gülcü Great God developed slf4j (Simple Logging Facade for Java). slf4j many logging framework is the facade abstract interface implemented with slf4j wants to switch the log, the log just need a corresponding jar substitutions and additions corresponding adapter.

Source: A well-known logging system is how design out?

From the figure you can see why our code starts to lead slf4j package. In the development of a manual Ali

Forced: applications not directly use the logging system (log4j, logback) in the API, but rather rely on the use of SLF4J logging framework API. Logging framework using the facade pattern is conducive to maintaining unity and log processing methods for each class.

Logback realized SLF4J, less middle adaptation layer, Logback also Ceki Gülcü the god development.

Logger & Appender & Layouts

Logback three main categories logger, appender and layouts. These three components together to meet our role to develop in different places depending on the level and type of print log log messages.

Logger

ch.qos.logback.classic.Logger class structure:

Logger dependent on LoggerContext, LoggerContext responsible for the production Logger, to be managed by a tree-like hierarchy. Logger maintains log level and level value of the current node. logger by generational (level), have inherited the ability to log level, such as. "": If no name is chapters.LogbackTest log level, will inherit its parent chapters log level. All logs are ancestors ROOT name Logger, the default level DEBUG. Set the log level of the current node does not consider the log level of the parent class. Logger log level by enabling and disabling the control log. Log level the TRACE < DEBUG < INFO < WARN < ERROR .

Next we look at the combined configuration file Logger properties corresponding configuration label:

<configuration>
  <turboFilter class="ch.qos.logback.classic.turbo.MDCFilter">
    <MDCKey>username</MDCKey>
    <Value>sebastien</Value>
    <OnMatch>ACCEPT</OnMatch>
  </turboFilter>

  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>/Users/wolf/study/logback/logback-examples/myApp.log</file>
    <encoder>
      <pattern>%msg%n</pattern>
    </encoder>
  </appender>
 
  <logger name="chapters.LogbackTest" level="DEBUG"></logger>

  <root>
    <appender-ref ref="FILE"/>
  </root>
</configuration>

name: logger tag name attribute value.

level: the attribute value logger tag level.

parent: the parent class encapsulates "chapters", and "chapters" parent class "ROOT" in the logger object.

aai: appender-ref tag, and where the corresponding FileAppender implementation class objects. If the tag is not appender-ref is null.

loggerContext: maintains the filter, such as a turbo filters.

Appender

Appender role is to control the output destination of the log. Log output destination is diversified, you can log output to the console, file, remote socket server, MySQL, PostgreSQL, Oracle or other databases, JMS, remote UNIX Syslog daemons in. A log may be output to multiple destinations. Such as

<configuration>
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>/Users/wolf/study/logback/logback-examples/myApp.log</file>
    <encoder>
      <pattern>%msg%n</pattern>
    </encoder>
  </appender>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%msg%n</pattern>
    </encoder>
  </appender>

  <root>
    <appender-ref ref="STDOUT" />
    <appender-ref ref="FILE"/>
  </root>
</configuration>

The output xml configuration file and log myApp.log to the console.

Layouts/Encoder

There Logger and Appender above two components, the log has been output to the destination, but the log so that the print on our mortal unfriendly, hard to read. Mortal will be beautiful, beautify it with Layouts or log output format Encoder about it. Encoder introduced in logback 0.9.19 version. Previously, most of the appender dependent layout to convert log events to string, and then through java.io.Writerwrite. In previous versions, the user needs in FileAppenderthe built-in one PatternLayout. In the later versions 0.9.19, FileAppenderas well as the need for a sub-class encoder rather than layout.

Source

Logger create

Logger logger = LoggerFactory.getLogger(LogbackTest.class);

Next, we analyze the source code to initialize according to the logger. Or in accordance with the old rules interface call to a timing diagram, right before the analysis source.

第 步: org.slf4j.LoggerFactory # getLogger (java.lang.String)

public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}

Gets a ILoggerFactory, namely LoggerContext. Then the object from which to get to the Logger.

Step 3: org.slf4j.LoggerFactory # getILoggerFactory

public static ILoggerFactory getILoggerFactory() {
    return getProvider().getLoggerFactory();
}

Step 4: org.slf4j.LoggerFactory # getProvider

static SLF4JServiceProvider getProvider() {
    if (INITIALIZATION_STATE == UNINITIALIZED) {
        synchronized (LoggerFactory.class) {
            if (INITIALIZATION_STATE == UNINITIALIZED) {
                INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                performInitialization();
            }
        }
    }
    switch (INITIALIZATION_STATE) {
    case SUCCESSFUL_INITIALIZATION:
        return PROVIDER;
    case NOP_FALLBACK_INITIALIZATION:
        return NOP_FALLBACK_FACTORY;
    case FAILED_INITIALIZATION:
        throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
    case ONGOING_INITIALIZATION:
        return SUBST_PROVIDER;
    }
    throw new IllegalStateException("Unreachable code");
}

SLF4JServiceProvider for initialization, ie LogbackServiceProvider object. Then check the initialization state, if successful returns PROVIDER.

第5步:org.slf4j.LoggerFactory#performInitialization

private final static void performInitialization() {
    bind();
    if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
        versionSanityCheck();
    }
}

第6步:org.slf4j.LoggerFactory#bind

private final static void bind() {
    try {
        // 加载 SLF4JServiceProvider 
        List<SLF4JServiceProvider> providersList = findServiceProviders();
        reportMultipleBindingAmbiguity(providersList);
        if (providersList != null && !providersList.isEmpty()) {
           PROVIDER = providersList.get(0);
           // SLF4JServiceProvider.initialize()
           PROVIDER.initialize();
           INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            reportActualBinding(providersList);
            fixSubstituteLoggers();
            replayEvents();
            SUBST_PROVIDER.getSubstituteLoggerFactory().clear();
        } else {
            // 省略代码。。。
        }
    } catch (Exception e) {
        // 失败,设置状态值,上报
        failedBinding(e);
        throw new IllegalStateException("Unexpected initialization failure", e);
    }
}

通过ServiceLoader加载LogbackServiceProvider,然后进行初始化相关字段。初始化成功后把初始化状态设置成功状态值。

第7步:ch.qos.logback.classic.spi.LogbackServiceProvider#initialize

public void initialize() {
    // 初始化默认的loggerContext
    defaultLoggerContext = new LoggerContext();
    defaultLoggerContext.setName(CoreConstants.DEFAULT_CONTEXT_NAME);
    initializeLoggerContext();
    markerFactory = new BasicMarkerFactory();
    mdcAdapter = new LogbackMDCAdapter();
}

创建名字为default的LoggerContext对象,并初始化一些字段默认值。

ch.qos.logback.classic.LoggerContext#LoggerContext

public LoggerContext() {
    super();
    this.loggerCache = new ConcurrentHashMap<String, Logger>();
    this.loggerContextRemoteView = new LoggerContextVO(this);
    this.root = new Logger(Logger.ROOT_LOGGER_NAME, null, this);
    this.root.setLevel(Level.DEBUG);
    loggerCache.put(Logger.ROOT_LOGGER_NAME, root);
    initEvaluatorMap();
    size = 1;
    this.frameworkPackages = new ArrayList<String>();
}

初始化LoggerContext时设置了ROOT的Logger,日志级别为DEBUG。

第8步:ch.qos.logback.classic.spi.LogbackServiceProvider#initializeLoggerContext

private void initializeLoggerContext() {
    try {
        try {
            new ContextInitializer(defaultLoggerContext).autoConfig();
        } catch (JoranException je) {
            Util.report("Failed to auto configure default logger context", je);
        }
        // 省略代码。。。
    } catch (Exception t) { // see LOGBACK-1159
        Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
    }
}

把第7步初始化好的LoggerContext当做参数传入ContextInitializer,构建其对象。然后解析配置文件。

第9步:ch.qos.logback.classic.util.ContextInitializer#autoConfig

public void autoConfig() throws JoranException {
    StatusListenerConfigHelper.installIfAsked(loggerContext);
    // (1) 从指定路径获取
    URL url = findURLOfDefaultConfigurationFile(true);
    if (url != null) {
        configureByResource(url);
    } else {
        // (2) 从运行环境中获取
        Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);
        if (c != null) {
            // 省略代码。。。
        } else {
            // (3)设置默认的
            BasicConfigurator basicConfigurator = new BasicConfigurator();
            basicConfigurator.setContext(loggerContext);
            basicConfigurator.configure(loggerContext);
        }
    }
}

首先从指定的路径获取资源URL,如果存在就进行解析;如果不存在再从运行环境中获取配置;如果以上都没有最后会构建一个BasicConfigurator当作默认的。

ch.qos.logback.classic.util.ContextInitializer#findURLOfDefaultConfigurationFile

public URL findURLOfDefaultConfigurationFile(boolean updateStatus) {
    ClassLoader myClassLoader = Loader.getClassLoaderOfObject(this);
    // 启动参数中获取
    URL url = findConfigFileURLFromSystemProperties(myClassLoader, updateStatus);
    if (url != null) {
        return url;
    }
    // logback-test.xml
    url = getResource(TEST_AUTOCONFIG_FILE, myClassLoader, updateStatus);
    if (url != null) {
        return url;
    }
    //logback.groovy
    url = getResource(GROOVY_AUTOCONFIG_FILE, myClassLoader, updateStatus);
    if (url != null) {
        return url;
    }
    // logback.xml
    return getResource(AUTOCONFIG_FILE, myClassLoader, updateStatus);
}

先从启动参数中查找logback.configurationFile参数值,如果没有再从classpath中一次查找logback-test.xml -> logback.groovy -> logback.xml 。由此可知文件的优先级是 启动参数 -> logback-test.xml -> logback.groovy -> logback.xml

第10步:ch.qos.logback.classic.util.ContextInitializer#configureByResource

public void configureByResource(URL url) throws JoranException {
    if (url == null) {
        throw new IllegalArgumentException("URL argument cannot be null");
    }
    final String urlString = url.toString();
    if (urlString.endsWith("groovy")) {
         // 省略代码。。。
    } else if (urlString.endsWith("xml")) {
        JoranConfigurator configurator = new JoranConfigurator();
        configurator.setContext(loggerContext);
        configurator.doConfigure(url);
    } else {
        // 省略代码。。。
    }
}

根据文件后缀判断是 groovy或者xml,然后交给不同的配置解析器处理。这里也是把第7步中的LoggerContext传进去,继续封装它的字段值。

第12步:ch.qos.logback.core.joran.GenericConfigurator#doConfigure(org.xml.sax.InputSource)

public final void doConfigure(final InputSource inputSource) throws JoranException {

    long threshold = System.currentTimeMillis();
    SaxEventRecorder recorder = new SaxEventRecorder(context);
    recorder.recordEvents(inputSource);
    // 处理配置文件,封装到 LoggerContext 中
    playEventsAndProcessModel(recorder.saxEventList);
    StatusUtil statusUtil = new StatusUtil(context);
    if (statusUtil.noXMLParsingErrorsOccurred(threshold)) {
        registerSafeConfiguration(recorder.saxEventList);
    }
}

真正解析配置文件的逻辑在playEventsAndProcessModel方法中,这里就不展开分析了。到这一步LoggerContext基本初始化完成了。

第13步:ch.qos.logback.classic.LoggerContext#getLogger(java.lang.String)

@Override
public Logger getLogger(final String name) {
    // 省略代码。。。 
    if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
        return root;
    }

    int i = 0;
    Logger logger = root;
    // 从缓存中获取, 有直接返回
    Logger childLogger = (Logger) loggerCache.get(name);
    if (childLogger != null) {
        return childLogger;
    }

    // if the desired logger does not exist, them create all the loggers
    // in between as well (if they don't already exist)
    String childName;
    while (true) {
        int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
        if (h == -1) {
            childName = name;
        } else {
            childName = name.substring(0, h);
        }
        // move i left of the last point
        i = h + 1;
        synchronized (logger) {
            childLogger = logger.getChildByName(childName);
            if (childLogger == null) {
                childLogger = logger.createChildByName(childName);
                loggerCache.put(childName, childLogger);
                incSize();
            }
        }
        logger = childLogger;
        if (h == -1) {
            return childLogger;
        }
    }
}

经过前面漫长的对LoggerContext进行初始化工作,这一步就是从LoggerContext获取Logger对象。如果缓存中直接返回。否则通过“.”分代构建层次结构。

日志执行步骤

上一节Logger创建完成,接下来分析一下打日志的流程。

logger.info(" {} is best player in world", "Greizmann");

第1步:ch.qos.logback.classic.Logger#info(java.lang.String, java.lang.Object)

public void info(String format, Object arg) {
    filterAndLog_1(FQCN, null, Level.INFO, format, arg, null);
}

把接口的日志级别(Level.INFO)传到下一个方法。

第2步:ch.qos.logback.classic.Logger#filterAndLog_1

private void filterAndLog_1(final String localFQCN, final Marker marker, final Level level, final String msg, final Object param, final Throwable t) {
    // 先通过turboFilter过滤
    final FilterReply decision = loggerContext.getTurboFilterChainDecision_1(marker, this, level, msg, param, t);
    // 判断日志级别
    if (decision == FilterReply.NEUTRAL) {
        if (effectiveLevelInt > level.levelInt) {
            return;
        }
    } else if (decision == FilterReply.DENY) {
        return;
    }

    buildLoggingEventAndAppend(localFQCN, marker, level, msg, new Object[] { param }, t);
}

如果TurboFilter过滤器存在就会执行相关操作,并返回FilterReply。如果结果是FilterReply.DENY本条日志消息直接丢弃;如果是FilterReply.NEUTRAL会继续判断日志级别是否在该方法级别之上;如果是FilterReply.ACCEPT直接跳到下一步。

第3步:ch.qos.logback.classic.Logger#buildLoggingEventAndAppend

private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params, final Throwable t) {
    LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
    le.setMarker(marker);
    callAppenders(le);
}

创建了LoggingEvent对象,该对象包含日志请求所有相关的参数,请求的 logger,日志请求的级别,日志信息,与日志一同传递的异常信息,当前时间,当前线程,以及当前类的各种信息和 MDC。其实打印日志就是一个事件,所以这个对象是相关重要,下面全部是在操作该对象。

第4步:ch.qos.logback.classic.Logger#callAppenders

public void callAppenders(ILoggingEvent event) {
    int writes = 0;
    // 从自己往父辈查找满足
    for (Logger l = this; l != null; l = l.parent) {
        // 写文件
        writes += l.appendLoopOnAppenders(event);
        if (!l.additive) {
            break;
        }
    }
    // No appenders in hierarchy
    if (writes == 0) {
        loggerContext.noAppenderDefinedWarning(this);
    }
}

第5步:ch.qos.logback.classic.Logger#appendLoopOnAppenders

private int appendLoopOnAppenders(ILoggingEvent event) {
    if (aai != null) {
        return aai.appendLoopOnAppenders(event);
    } else {
        return 0;
    }
}

从当前Logger到父节点遍历,直到AppenderAttachableImpl不为空(有appender-ref 标签)。

第6步:ch.qos.logback.core.spi.AppenderAttachableImpl#appendLoopOnAppenders

public int appendLoopOnAppenders(E e) {
    int size = 0;
    final Appender<E>[] appenderArray = appenderList.asTypedArray();
    final int len = appenderArray.length;
    for (int i = 0; i < len; i++) {
        appenderArray[i].doAppend(e);
        size++;
    }
    return size;
}

如果设置了多个日志输出目的地,这里就是循环调用对应的Appender进行输出。

第7步:ch.qos.logback.core.UnsynchronizedAppenderBase#doAppend

public void doAppend(E eventObject) {
    if (Boolean.TRUE.equals(guard.get())) {
        return;
    }

    try {
        guard.set(Boolean.TRUE);
        if (!this.started) {
            if (statusRepeatCount++ < ALLOWED_REPEATS) {
                addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
            }
            return;
        }

        if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
            return;
        }

        this.append(eventObject);

    } catch (Exception e) {
        if (exceptionCount++ < ALLOWED_REPEATS) {
            addError("Appender [" + name + "] failed to append.", e);
        }
    } finally {
        guard.set(Boolean.FALSE);
    }
}

通过ThreadLocal控制递归导致的重复提交

第8步:ch.qos.logback.core.OutputStreamAppender#append

protected void append(E eventObject) {
    if (!isStarted()) {
        return;
    }

    subAppend(eventObject);
}

第9步:ch.qos.logback.core.OutputStreamAppender#subAppend

protected void subAppend(E event) {
    if (!isStarted()) {
        return;
    }
    try {
        if (event instanceof DeferredProcessingAware) {
            // 拼接日志信息(填充占位符),设置当前线程以及MDC等信息
            ((DeferredProcessingAware) event).prepareForDeferredProcessing();
        }
        byte[] byteArray = this.encoder.encode(event);
        writeBytes(byteArray);
    } catch (IOException ioe) {
        this.started = false;
        addStatus(new ErrorStatus("IO failure in appender", this, ioe));
    }
}

Encoder在这里惨淡登场,返回byte数组。

第10步:ch.qos.logback.core.encoder.LayoutWrappingEncoder#encode

public byte[] encode(E event) {
    String txt = layout.doLayout(event);
    return convertToBytes(txt);
}

Encoder先把LoggerEvent交给Layout,Layout组装日志信息,在每条信息后加上换行符。

第11步:ch.qos.logback.core.OutputStreamAppender#writeBytes

private void writeBytes(byte[] byteArray) throws IOException {
    if(byteArray == null || byteArray.length == 0)
        return;
    
    lock.lock();
    try {
        this.outputStream.write(byteArray);
        if (immediateFlush) {
            this.outputStream.flush();
        }
    } finally {
        lock.unlock();
    }
}

使用AQS锁控制并发问题。这也是Logback性能不如 Log4j2的原因。后面有时间分析一下Log4j2。

本文到此结束了,还有两天就要放假了,祝大家新年快乐。

Guess you like

Origin www.cnblogs.com/wolf-bin/p/12213058.html