`
manzhizhen
  • 浏览: 293399 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

SLF4J漫谈

    博客分类:
  • Java
阅读更多

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

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

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

 

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

 

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

 

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

 

我们可以看到,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的调用关系图来了解这一过程:

 

上图中的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中是这样做的:

 

如果通过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是如何绑定底层的日志框架实现的,希望帮助同学了解内部的一些设计考量和实现。

分享到:
评论

相关推荐

    log4j + slf4j-api + slf4j-log4j12

    在给定的标题和描述中,我们看到了两个关键的日志框架——`log4j`和`SLF4J(Simple Logging Facade for Java)`,以及它们之间的桥接器`slf4j-log4j12`。这些组件是Java日志处理的常用工具,让我们详细了解一下它们...

    slf4j jar包

    org.slf4j.ILoggerFactory.class org.slf4j.IMarkerFactory.class org.slf4j.Logger.class org.slf4j.LoggerFactory.class org.slf4j.MDC.class org.slf4j.Marker.class org.slf4j.MarkerFactory.class org.slf4j....

    最新slf4j-1.7.21.zip源码和jar包

    1、jcl-over-slf4j-1.7.21.jar 2、jcl-over-slf4j-1.7.21-sources.jar 3、jul-to-slf4j-1.7.21.jar 4、jul-to-slf4j-1.7.21-sources.jar 5、log4j-over-slf4j-1.7.21.jar 6、log4j-over-slf4j-1.7.21-sources....

    slf4j-1.7.21所有相关jar包

    该压缩包中包含以下内容: 1、jcl-over-slf4j-1.7.21.jar 2、jcl-over-slf4j-1.7.21-sources.jar 3、jul-to-slf4j-1.7.21.jar 4、jul-to-slf4j-1.7.21-sources.jar 5、log4j-over-slf4j-1.7.21.jar 6、log4j-over-...

    slf4j-api.jar和slf4j-nop.jar打包下载

    SLF4J(Simple Logging Facade for Java)是Java中一个重要的日志抽象层,它为各种日志框架,如Log4j、java.util.logging、Logback等提供了一个统一的接口。通过SLF4J,开发者可以在不修改代码的情况下更换底层的...

    最新slf4j-1.7.25.zip源码和jar包

    该压缩包中包含以下内容: 1、jcl-over-slf4j-1.7.21.jar 2、jcl-over-slf4j-1.7.21-sources.jar 3、jul-to-slf4j-1.7.21.jar 4、jul-to-slf4j-1.7.21-sources.jar 5、log4j-over-slf4j-1.7.21.jar 6、log4j-over-...

    slf4j-log4j12-1.7.12.jar、slf4j-api-1.7.12.jar

    SLF4J(Simple Logging Facade for Java)是Java中的一种日志抽象层,它提供了一个API,允许开发者在运行时插入所需的日志框架。SLF4J的主要目的是为各种日志框架,如log4j、logback等,提供一个统一的接口,使得...

    slf4j最新jar包下载和jar包

    SLF4J(Simple Logging Facade for Java)是Java中的一种日志抽象层,它提供了一个接口,允许用户在运行时动态地绑定到各种具体的日志框架,如Log4j、Java内置的日志或者Logback等。这个设计使得开发者可以在不修改...

    slf4j-api-1.7.12.jar slf4j-log4j12-1.7.12.jar

    SLF4J(Simple Logging Facade for...`slf4j-api-1.7.12.jar`和`slf4j-log4j12-1.7.12.jar`分别是SLF4J API和SLF4J到Log4j的绑定,它们共同工作,使开发者能够利用Log4j的强大功能,同时保持代码与具体日志系统的分离。

    slf4j-1.6.0全套jar包资源,slf4j-api-1.6.0.jar,slf4j-jdk14-1.6.0.jar...

    slf4j-api-1.6.0.jar,slf4j-jdk14-1.6.0.jar,slf4j-log4j12-1.6.0-rc0.jar,slf4j-nop-1.6.0.jar,slf4j-simple-1.6.0.jar

    Java Slf4j依赖包

    Java Slf4j,全称为Simple Logging Facade for Java,是一个为各种日志框架提供一个简单统一的接口,使得最终用户能够在部署时配置他们希望的日志框架。Slf4j允许开发者在部署应用时插入所需的日志实现,无需修改...

    slf4j-api-1.7.26.zip

    描述中提到的"slf4j-api-1.7.26.jar"是SLF4J API的核心库,它包含所有开发者需要使用的接口和类,例如`org.slf4j.Logger`和`org.slf4j.LoggerFactory`。`Logger`接口提供了不同级别的日志记录方法,如`trace()`, `...

    SLF4j中文使用手册

    SLF4J(Simple Logging Facade for Java)是一个用于Java编程语言的日志门面框架,它提供了一个简单的抽象层,用以使用各种日志框架,例如java.util.logging、logback和log4j。门面模式的好处在于,开发者可以在不同...

    slf4j-api-1.7.7,slf4j-log4j12-1.7.7

    SLF4J(Simple Logging Facade for Java)是Java中一个重要的日志接口框架,它为各种日志实现提供了一个抽象层,使得开发者能够在运行时动态地选择具体使用的日志实现,例如Log4j、Java Util Logging (JUL)、Logback...

    slf4j所需要的所有jar文件集合

    1. **slf4j.api-1.6.1.jar**:这是SLF4J的核心API库,包含了所有SLF4J的接口和注解,使得开发者可以在代码中使用SLF4J的API进行日志记录。例如,`org.slf4j.Logger`和`org.slf4j.LoggerFactory`是SLF4J中最常用的类...

    slf4j-log4j12-1.7.7.jar下载

    在使用SLF4J和Log4j12时,你需要注意的一点是,由于Log4j1.2相比Log4j2在某些方面可能较旧,例如性能和功能更新,因此在新项目中,你可能会考虑使用更新的SLF4J绑定器,如slf4j-log4j2,以便利用Log4j2的改进特性。...

    slf4j-api-1.6.1-slf4j-nop-1.6.1.rar

    标题中的"slf4j-api-1.6.1-slf4j-nop-1.6.1.rar"表明这是一个包含SLF4J API和NOP绑定的日志库的压缩包。描述中提到,解压后会得到两个JAR文件:slf4j-api-1.6.1.jar和slf4j-nop-1.6.1.jar。 **SLF4J API** 1. **...

    log4j-over-slf4j-1.7.33-API文档-中文版.zip

    赠送jar包:log4j-over-slf4j-1.7.33.jar; 赠送原API文档:log4j-over-slf4j-1.7.33-javadoc.jar; 赠送源代码:log4j-over-slf4j-1.7.33-sources.jar; 赠送Maven依赖信息文件:log4j-over-slf4j-1.7.33.pom; ...

    log4j-to-slf4j-2.17.1-API文档-中文版.zip

    赠送jar包:log4j-to-slf4j-2.17.1.jar; 赠送原API文档:log4j-to-slf4j-2.17.1-javadoc.jar; 赠送源代码:log4j-to-slf4j-2.17.1-sources.jar; 赠送Maven依赖信息文件:log4j-to-slf4j-2.17.1.pom; 包含翻译后...

Global site tag (gtag.js) - Google Analytics