文章

一起学Java(13)-[日志篇]教你分析SLF4J和Log4j2源码,掌握SLF4J与Log4j2桥接集成原理

研究完SLF4JLogback这种无缝集成的方式(一起学Java(12)-[日志篇]教你分析SLF4J源码,掌握SLF4J如何与Logback无缝集成的原理),继续研究Log4j2SLF4J这种需要桥接集成的方式。

一、桥接包如何与SLF4J集成

我们已经知道SLF4J利用ServiceLoader机制,去寻找和加载SLF4JServiceProvider接口的实现类,而Log4j2原生是没有实现这个接口的,因此需要借助桥接机制,将Log4j2集成到SLF4J中,这个桥接包就是log4j2-slf4j2-provider

log4j2-slf4j2-provider

在桥接包中,找到META-INF/services目录,可以看到org.slf4j.spi.SLF4JServiceProvider文件,内容如下:

1
org.apache.logging.slf4j.Log4jLoggerFactory

这就是Log4j2SLF4JServiceProvider接口的实现。

在实现中,返回了Log4jLoggerFactory,这个类是Log4j2的工厂类,用于创建Logger对象。这个工厂类是桥接包中的类。

二、桥接包如何与Log4j2集成

SLF4J的的LoggerFactory类中,在bind函数中,当获取到SLF4JServiceProvider后,会调用Provider的initialize方法,进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    private final static void bind() {
        try {
            List<SLF4JServiceProvider> providersList = findServiceProviders();
            reportMultipleBindingAmbiguity(providersList);
            if (providersList != null && !providersList.isEmpty()) {
                PROVIDER = providersList.get(0);
                // SLF4JServiceProvider.initialize() is intended to be called here and nowhere else.
                PROVIDER.initialize(); // by OneCoder 调用桥接包中Provider的initialize方法初始化
                INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
                reportActualBinding(providersList);
            } else {
                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
                Reporter.warn("No SLF4J providers were found.");
                Reporter.warn("Defaulting to no-operation (NOP) logger implementation");
                Reporter.warn("See " + NO_PROVIDERS_URL + " for further details.");

                Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportIgnoredStaticLoggerBinders(staticLoggerBinderPathSet);
            }
            postBindCleanUp();
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        }
    }

Log42j桥接包中,初始化的代码如下:

1
2
3
4
5
    public void initialize() {
        markerFactory = new Log4jMarkerFactory();
        loggerFactory = new Log4jLoggerFactory(markerFactory);
        mdcAdapter = new Log4jMDCAdapter();
    }

即核心是构造了一个Log4jLoggerFactory对象返回给SLF4J,该类集成自log4j-api包中的AbstractLoggerAdapter抽象类,并实现了SLF4J中的ILoggerFactory接口,用于真正获取Logger实例。这样桥接包已经有呈上启下的关系了。

1
public class Log4jLoggerFactory extends AbstractLoggerAdapter<Logger> implements ILoggerFactory

获取到ILoggerFactory实例后,SLF4J会调用LoggerFactorygetLogger方法,获取到Logger实例。

1
2
3
    public Logger getLogger(String name) {
        return loggerFactory.getLogger(name);
    }

这时便会进入AbstractLoggerAdapter类中的getLogger方法:

1
2
3
4
5
6
7
8
9
10
11
    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;
        }
        // by OneCoder如果logger不存在,则创建一个新的logger,并将其添加到loggers中
        loggers.putIfAbsent(name, newLogger(name, context));
        return loggers.get(name);
    }

这里的LoggerContext是Log4j2中的概念,表示一个Logger的上下文,用于存储Logger的配置信息和获取Logger实例。桥接包中实现了getContext方法,返回Log4j2中(log4j-api包中)的LoggerContext对象。

1
2
3
4
5
6
7
    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);
    }

获取到LoggerContext后,便可以在LogContext中获取真正的Logger实例,如果是第一次创建logger,从上下文中获取不到logger对象,则会调用newLogger方法,创建一个新的logger,并将其添加到loggers中。newLogger方法的实现在桥接包中。

1
2
3
4
    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(markerFactory, validateContext(context).getLogger(key), name);
    }

LogContext中获取真正logger的代码:

1
2
3
4
5
6
7
8
9
10
11
12
    public Logger getLogger(final String name, final MessageFactory messageFactory) {
        // Note: This is the only method where we add entries to the 'loggerRegistry' ivar.
        Logger logger = loggerRegistry.getLogger(name, messageFactory);
        if (logger != null) {
            AbstractLogger.checkMessageFactory(logger, messageFactory);
            return logger;
        }

        logger = newInstance(this, name, messageFactory);
        loggerRegistry.putIfAbsent(name, messageFactory, logger);
        return loggerRegistry.getLogger(name, messageFactory);
    }

这里通过LoggerContext的getLogger方法,获取到Log4j2中(log4j-core包中)的Logger对象,然后通过Log4j2中的Logger对象,创建一个Log4jLogger对象,该类是在桥接包中定义的类,因为应用需要返回的是SLF4J中的Logger接口,Log4j2中没有提供原生实现,因此需要在桥接包中包装一层。

1
public class Log4jLogger implements LocationAwareLogger, Serializable
1
public interface LocationAwareLogger extends Logger
1
2
3
4
5
    public Log4jLogger(final Log4jMarkerFactory markerFactory, final ExtendedLogger logger, final String name) {
        this.markerFactory = markerFactory;
        this.logger = logger;
        this.name = name;
    }

至此,通过桥接层就实现了向上与SLF4J接口层对接,向下包装了Log4j2的Logger对象,用于真正的Log记录。

在Log4jLogger中,日志记录函数中都是使用真正的Logger实例,来执行日志记录。例如:

1
2
3
    public void info(final String format, final Object... args) {
        logger.logIfEnabled(FQCN, Level.INFO, null, format, args);
    }

一张图总结一下桥接包的关系原理:

log4j2-slf4j2-provider

三、Log4j2原生API用法

作为扩展和对比,试验一下原生使用Log4j2 API的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.coderli.one.log;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * 本类用于研究Log4j2原生日志接口使用方式
 * @author OneCoder
 * @Blog https://www.coderli.com
 * @source https://github.com/lihongzheshuai/java-all-in-one
 */
public class Log4jLogMain {

    public static void main(String[] args) {
        Logger logger = LogManager.getLogger(Log4jLogMain.class);
        logger.info("This is a log from log4j-api.");
    }

}

即在通过log4j-api包中的LogManager获取到Logger实例进行日志记录。

原生使用log4j-api的代码也已上传至github:https://github.com/lihongzheshuai/java-all-in-one,有兴趣的可以参考。考虑到未来可能使用Logback框架,因此这部分代码放在分支feature-log中。

本文由作者按照 CC BY 4.0 进行授权