SLF4J漫谈

SLF4J的全称是Simple Logging Facade for Java,是当前最流行的日志(包装)框架之一,它不是完成的一套日志框架实现,它主要用作各种日志框架(例如java.util.logging,logback,log4j)的简单外观或抽象,允许最终用户在部署时插入具体的日志框架。可以直接将SLF4J看做其他日志框架的统一API,用SLF4J的好处是,如果后期用户需要切换日志框架的实现,不需要改代码,只需要调整部分jar的依赖即可。

那么你一定很好奇,SLF4J是如何做到统一各个日志框架API的?本文试图给你揭露这一“秘密”。

部分已经流行的日志框架出现的比SLF4J早,比如java.util.logging、log4j等,所以为了制定一套统一的API而直接去这些日志实现框架做“手脚”已经不可能,即下面这一条路走不通:

3E21141D-273C-7D68-7825-D4267BFE53DE.png

于是乎,SLF4J想到了加一个适配层,加一个适配层的直接好处是使我们适配老的日志组件成为了可能,而且还能在适配层做些特殊化的处理,注意,这里的适配层并不是统一的一层,而是每个日志实现框架都有一个对应的适配层,这样管理起来更加方便:

A26CAE5C-F287-62AC-6113-05FD9B6F8C8C.png

如果这样做的话,会有多个适配的Jar出现,而且这些Jar的通用部分(比如统一的API)无法收拢来统一实现,为了抽离公共部分,于是有了SLF4J-API这一层:

6828C0ED-3CEC-EA63-E8F6-ECDFD0761278.png

这样的话,公共部分和定制化的实现都能更自由的处理和管理,那让我们看看真实的SLF4J的架构图:

A8C586B0-B0F6-B06C-9AF5-4969B05BE3E1.png

我们可以看到,SLF4J-API层的实现就是slf4j-api.jar,而适配层的jar都是slf4j开头,例如Log4j的适配层实现是slf4j-log4j12.jar(注意,上图少了个j)。slf4j-api.jar里面有统一个API,其中有两个重要接口,即日志对象接口org.slf4j.Logger、日志工厂接口org.slf4j.ILoggerFactory,当然,slf4j-api.jar里面不会仅有接口,它还有一些实现类,因为SFL4J还肩负一个重要任务,那就是绑定具体的日志框架实现。

我们接下来看看slf4j-api.jar是如何做到绑定具体的日志框架的,前面提到过,SLF4J-API层需要和适配层联动才能做到这一点,一想到动态加载和绑定,Java体系里比较有名的就是SPI了,SPI的全称是Service Provider Interface,即服务提供者接口,其目的是为了在Java体系中能通过动态绑定接口实现类来支持第三方框架或组件的扩展,JVM中还提供了一个定制化的ClassLoader(即java.util.ServiceLoader)来实现这一点(但其实不太好用,功能也不强大,所以像Dubbo等框架都自己实现),SLF4J(注意,SLF4J的老版本是没有使用SPI的)也使用了SPI。我们一般都如下使用SLF4J(代码片段来自SLF4J官网):

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {
  public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(HelloWorld.class);
    logger.info("Hello World");
  }
}

我们在使用SLF4J的时候,并没有显示的去调用他的初始化方法,所以在调用LoggerFactory.getLogger方法时,SLF4J内部会进行状态检查,如果没有初始化过,就会直接进行初始化,其中的日志框架的绑定就是初始化的最重要内容,我们直接通过IDEA的调用关系图来了解这一过程:

92306616-DBE1-0E12-CB2C-439918356298.png

上图中的LogFactory.getProvider中会去判断初始化状态,如果没有,那么将同步进行初始化,代码如下:

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

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

    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:
                // support re-entrant behavior.
                // See also http://jira.qos.ch/browse/SLF4J-97
                return SUBST_PROVIDER;
        }
        
        throw new IllegalStateException("Unreachable code");
    }

可见,同一时刻有且只有一个线程能去调用初始化方法,即performInitialization下面的bind方法:

private final static void performInitialization() {
        // 绑定过程
        bind();
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            versionSanityCheck();
        }
    }

    private final static void bind() {
        try {
            // 使用SPI的方式来加载日志框架实现(该方式出现于1.7.26及其之后的版本)
            List<SLF4JServiceProvider> providersList = findServiceProviders();
            reportMultipleBindingAmbiguity(providersList);
            if (providersList != null && !providersList.isEmpty()) {
                // 这里很关键,说明通过SPI机制,加载的第一个适配层将是SLF4J选定绑定的日志框架实现
                PROVIDER = providersList.get(0);
                PROVIDER.initialize();
                INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
                // 输出日志,当可选日志框架大于一时,告知我们实际绑定的日志框架
                reportActualBinding(providersList);
                fixSubstituteLoggers();
                replayEvents();
                // release all resources in SUBST_FACTORY
                SUBST_PROVIDER.getSubstituteLoggerFactory().clear();

            } else {
                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
                Util.report("No SLF4J providers were found.");
                Util.report("Defaulting to no-operation (NOP) logger implementation");
                Util.report("See " + NO_PROVIDERS_URL + " for further details.");

                // 通过指定类来实现加载,该适配方式比较原始和暴力,1.7.26版本之前只有该适配方式,由于这里是新版本的代码,所以如果SPI没有加载到,那么流转到这里将导致不打印任何日志也不会报错,但会打印日志提示
                Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportIgnoredStaticLoggerBinders(staticLoggerBinderPathSet);
            }

        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        }
    }

从上面的代码片段来看,我们发现了两个重要方法,即 findServiceProvidersfindPossibleStaticLoggerBinderPathSet,findServiceProviders是标准的通过SPI来加载日志实现框架的方法,而findPossibleStaticLoggerBinderPathSet方法是为了兜底老版本的实现,需要注意的是,老版本没有SPI的方式,如果将1.7.26版本之后的slf4j-api.jar和老版本的适配层jar一起使用,流程会走到了这里,将匹配到NOPLogger,导致不输出日志也不报错(但启动时会有日志提示)。这里还有一个关键点,即PROVIDER = providersList.get(0)的调用,说明了对于SPI机制,加载的第一个适配层将是SLF4J选定绑定的日志框架实现!我们看下findServiceProviders的实现:

private static List<SLF4JServiceProvider> findServiceProviders() {
        // SLF4JServiceProvider是SLF4J提供的SPI接口,如果适配层实现了SPI规范和该接口,那么可以在这里被发现和加载
        ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
        
        List<SLF4JServiceProvider> providerList = new ArrayList<SLF4JServiceProvider>();
        for (SLF4JServiceProvider provider : serviceLoader) {
            providerList.add(provider);
        }
        
        return providerList;
    }

这里的是标准的Java SPI的使用方式,适配层的jar只要实现了SLF4JServiceProvider接口和SPI规范的一些配置即可被slf4j-api.jar发现并加载,例如在Log4j的适配slf4j-log4j12.jar中是这样做的:

A1F256AF-BA13-54D1-E49C-466A2C412ACE.png

如果通过SPI没有发现任何日志框架的实现怎么办(比如1.7.26之前的老版本)?所以在这里仍然保留了老版本的加载方式,即findPossibleStaticLoggerBinderPathSet方法的实现:

static Set<URL> findPossibleStaticLoggerBinderPathSet() {
        Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
        try {
            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            Enumeration<URL> paths;
            
            // 尝试去加载 org/slf4j/impl/StaticLoggerBinder.class
            if (loggerFactoryClassLoader == null) {
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }
            
            while (paths.hasMoreElements()) {
                URL path = paths.nextElement();
                staticLoggerBinderPathSet.add(path);
            }
            
        } catch (IOException ioe) {
            Util.report("Error getting resources from path", ioe);
        }
        
        return staticLoggerBinderPathSet;
    }

这里可以看出,SFL4J尝试去发现所有StaticLoggerBinder类资源文件(注意,这里没有使用ClassLoader.loadClass方法是为了能发现所有的StaticLoggerBinder),StaticLoggerBinder是老版本SLF4J和适配层之间的连接契约,即适配层只要有StaticLoggerBinder这个类,就有机会被slf4j-api.jar来发现,我们看老版本的slf4j-log4j12.jar(例如1.7.25版本)中的StaticLoggerBinder实现(新版本已经不用这种方式,也没有这个类):

public class StaticLoggerBinder implements LoggerFactoryBinder {

    /**
     * The unique instance of this class.
     * 
     */
    private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

    /**
     * Return the singleton of this class.
     * 
     * @return the StaticLoggerBinder singleton
     */
    public static final StaticLoggerBinder getSingleton() {
        return SINGLETON;
    }

    /**
     * Declare the version of the SLF4J API this implementation is compiled against. 
     * The value of this field is modified with each major release. 
     */
    // to avoid constant folding by the compiler, this field must *not* be final
    public static String REQUESTED_API_VERSION = "1.6.99"; // !final

    private static final String loggerFactoryClassStr = Log4jLoggerFactory.class.getName();

    /**
     * The ILoggerFactory instance returned by the {@link #getLoggerFactory}
     * method should always be the same object
     */
    private final ILoggerFactory loggerFactory;

    private StaticLoggerBinder() {
        loggerFactory = new Log4jLoggerFactory();
        try {
            @SuppressWarnings("unused")
            Level level = Level.TRACE;
        } catch (NoSuchFieldError nsfe) {
            Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
        }
    }

    public ILoggerFactory getLoggerFactory() {
        return loggerFactory;
    }

    public String getLoggerFactoryClassStr() {
        return loggerFactoryClassStr;
    }
}

可以看出,StaticLoggerBinder是作为单例来使用的,最重要的方法就是getLoggerFactory,用来直接返回包装过Log4j的日志工厂接口ILoggerFactory(之前提到过的slf4j-api.jar的两个最重要的接口之一)的实现类。那么读者会有疑问,在老版本中,如果有多个适配实现,会使用哪一个呢?于是我们得看老版本的LoggerFacotry的bind方法实现方式,这里给出1.7.25版本的bind方法:

private final static void bind() {
        try {
            Set<URL> staticLoggerBinderPathSet = null;
            // skip check under android, see also
            // http://jira.qos.ch/browse/SLF4J-328
            if (!isAndroid()) {
                // 扫描所有的StaticLoggerBinder类文件
                staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            }
            // 显示的绑定,ClassLoader先加载谁就先绑定谁
            StaticLoggerBinder.getSingleton();
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            reportActualBinding(staticLoggerBinderPathSet);
            fixSubstituteLoggers();
            replayEvents();
            // release all resources in SUBST_FACTORY
            SUBST_FACTORY.clear();
        } catch (NoClassDefFoundError ncde) {
            String msg = ncde.getMessage();
            if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
                Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
                Util.report("Defaulting to no-operation (NOP) logger implementation");
                Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
            } else {
                failedBinding(ncde);
                throw ncde;
            }
        } catch (java.lang.NoSuchMethodError nsme) {
            String msg = nsme.getMessage();
            if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
                INITIALIZATION_STATE = FAILED_INITIALIZATION;
                Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
                Util.report("Your binding is version 1.5.5 or earlier.");
                Util.report("Upgrade your binding to version 1.6.x.");
            }
            throw nsme;
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        }
    }

其中我们发现了关键的StaticLoggerBinder.getSingleton()调用,所以在老版本中,如果存在多个适配层的实现,那么JVM先加载哪个适配层的StaticLoggerBinder类,那么就相当于SLF4J绑定了哪个适配层。

这里做一个总结,本文简单分析了SLF4J的架构设计,已经新老版本slf4j-api.jar是如何绑定底层的日志框架实现的,希望帮助同学了解内部的一些设计考量和实现。

收藏 (0)
评论列表
正在载入评论列表...
我是有底线的
为您推荐
    暂时没有数据