SLF4J深入剖析(涵盖SLF4J 1.8)

1.SLF4J

SLF4J全称 Simple Logging Facade for Java,它为Java下的日志系统提供了一套统一的门面(接口)。通过引入SLF4J,我们可以使项目的logging与logging具体的实现分离,在提供了一致的接口的同时,提供了灵活选择logging实现的能力。

在SLF4J之前,Apache Common Logging(即Jakarta Commons Logging,简称JCL)也提供了类似的功能。它与SLF4J的区别在于:

  1. JCL即提供了统一的接口,也提供了一套默认的实现;SLF4J则只提供了接口层
  2. JCL采用运行时绑定,通过Classloader体系加载相应的logging实现;SLF4J采用了编译期绑定
  3. SLF4J在接口易用性上更有优势,大大减少了不必要的日志拼接:
    • JCL下,为了避免无效的字符串拼接,一般需要按照如下方式输出日志:
    if(log.isInfoEnabled()) {
        log.info("AnalyseOrderLogic.checkBusinessValid:" + channelCoopId
            + "," + JSON.toJSONString(entities));
    }
    
    • SLF4J则提供了占位符"{}",只在必要的情况下才会进行日志字符串处理和拼接:
    log.info("AnalyseOrderLogic.checkBusinessValid:{},{}", channelCoopId, JSON.toJSONString(entities));
    

2.SLF4J的使用

SLF4J的使用非常简单:

  1. 引入SLF4J依赖
  2. 引入一种logging的SLF4J实现,比如SLF4J LOG4J 12 Binding,或logback-classic

之后,就可以正常使用SLF4J打印日志了,demo如下:

package some.package; 

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

public MyClass {
    Logger logger = LoggerFactory.getLogger(MyClass.class);
    
    public void someMethod() {
        logger.info("Hello world");
    }
}

让我们从0开始搭建一个基于SLF4J的项目

2.1 引入SLF4J

【注】也可以从github上下载本节demo:

$ git clone git@github.com:jinluu/slf-demo.git
$ git checkout -b nop origin/nop
  1. 创建工程并引入slf4j-api依赖
$ mvn archetype:generate -DgroupId=cn.jinlu.slf.demo -DartifactId=slf-demo -Dversion=0.1-SNAPSHOT -DpackageName=cn.jinlu.slf.demo -DarchetypeArtifactId=maven-archetype-quickstart
  1. 引入slf4j-api依赖
    在pom.xml中添加依赖:
    
      org.slf4j
      slf4j-api
      1.7.25
    
  1. 使用logger
package cn.jinlu.slf.demo;

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

public class App {
    private static final Logger logger = LoggerFactory.getLogger(App.class);

    public static void main( String[] args )
    {
        logger.info( "Hello, {}!", App.class.getSimpleName());
    }
}

此时运行App.main(),会发现没有日志打印,但是有如下错误信息:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

出现该问题的原因是,SLF4J只提供了一个同一的日志接口/门面(Facade),如果找到任何实现,则绑定到默认的NOPLoggerFactory。此时日志系统不会生效,而是打印出上述错误信息并继续执行。

因此,为了打印日志,我们还需要引入日志得实现类。

看看此时的项目依赖关系:

$ mvn dependency:tree
...
[INFO] cn.jinlu.slf.demo:slf-demo:jar:0.1-SNAPSHOT
[INFO] +- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] \- junit:junit:jar:4.10:test
[INFO]    \- org.hamcrest:hamcrest-core:jar:1.1:test
...

2.2 引入Log4J作为SLF4J的实现

Logback是流行的log框架-Log4J的继任者。相比Log4J,logback做了大量的改进,比如提供了更高的性能,原生支持SLF4J等。

【注】也可以从github上下载本节demo:

$ git clone git@github.com:jinluu/slf-demo.git
$ git checkout -b log4j origin/log4j
  1. 引入log4j依赖
    
      org.slf4j
      slf4j-log4j12
      1.7.25
    
  1. src/main/resources下创建log4j.xml配置文件



    
        
            
        
    

    
    
        
        
    

    
    
        
        
    

再次运行App.main(),可以看到如下日志输出:

[29 17:18:45,209 INFO ] [main] demo.App - Hello, App!

最后,检查一下依赖关系:

$ mvn dependency:tree
[INFO] Scanning for projects...
...
[INFO] cn.jinlu.slf.demo:slf-demo:jar:0.1-SNAPSHOT
[INFO] +- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] +- org.slf4j:slf4j-log4j12:jar:1.7.25:compile
[INFO] |  \- log4j:log4j:jar:1.2.17:compile
[INFO] \- junit:junit:jar:4.10:test
[INFO]    \- org.hamcrest:hamcrest-core:jar:1.1:test

可见,slf4j-log4j12自动引入了log4j的实现log4j

2.2 将slf4j的实现修改为logback

【注】也可以从github上下载本节demo:

$ git clone git@github.com:jinluu/slf-demo.git
$ git checkout -b logback origin/logback

引入slf4j之后,对日志实现的改动变得更加灵活,比如如果我们希望从log4j迁移到性能更好的logback,那么我们可以:

  1. 修改依赖关系,将对slf4j-log4j12依赖修改为对logback-classic的依赖:

    ch.qos.logback
    logback-classic
    1.2.3

  1. src/main/resources下删除log4j.xml
    • 如果没有配置文件,logback会默认创建一个BasicConfigurator默认配置,将DEBUG级别及以上的日志输出到Console。

再次运行App.main(),可以看到如下日志输出:

[29 17:18:45,209 INFO ] [main] demo.App - Hello, App!

此时工程的依赖关系如下,可见logback-classic自动引入了logback-core的实现。

$ mvn dependency:tree
...
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ slf-demo ---
[INFO] cn.jinlu.slf.demo:slf-demo:jar:0.1-SNAPSHOT
[INFO] +- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] +- ch.qos.logback:logback-classic:jar:1.2.3:compile
[INFO] |  \- ch.qos.logback:logback-core:jar:1.2.3:compile
[INFO] \- junit:junit:jar:4.10:test
[INFO]    \- org.hamcrest:hamcrest-core:jar:1.1:test

3.SLF4J静态绑定源码解析

在第一章中,我们指出,SLF4J相比JCL的一大优势是采用了静态绑定,避免了在OSGI等场景中通过classloader动态绑定造成的困扰。现在我们看看SLF4J静态绑定的过程。

参考2.1节中App.java的代码,使用SLF4J时,

  1. 通过LoggerFactory.getLogger(Class)获取一个Logger
private static final Logger logger = LoggerFactory.getLogger(App.class)
  1. 在该类内部的任意处通过该logger打印不同级别的日志
logger.info( "Hello, {}!", App.class.getSimpleName());

首先看如何获取一个Logger

3.1 创建或获取一个Logger

一个典型的SLF4J类关系图如下所示。这里我们忽略slf4j-api中的辅助类(位于包org.slf4j.helpers内),以及不常用的MarkerMDC功能。

SLF4J深入剖析(涵盖SLF4J 1.8)_第1张图片
slf.plantuml.txt

就像demo代码那样,SLF4J非常简单。使用SLF4J只需要通过LoggerFactory.getLogger获取一个Logger对象,并通过该Logger对象进行日志记录即可。其他一切细节,都通过SLF4J及SLF4J-XXX-binder进行了屏蔽。这个binder用来将具体的logging实现与SLF4J进行绑定。

  • 在1.7及更早的版本中,该绑定都是通过继承org.slf4j.spi中的接口来实现,并且SLF4J对继承该接口的类的类名也进行了约定。因此图中org.slf4j.impl包含了这些类的实现,这些类都必须并且类名也必须遵照SLF4J的约定,且位于logging实现包中。比如以logback为例:
SLF4J深入剖析(涵盖SLF4J 1.8)_第2张图片
slf.plantuml.txt

在SLF4J的门面类中,会通过代码硬编码的方式获取指定类名的单例(StaticXxxBinder),并通过org.slf4j.spi中的接口获取相关的资源。

  • 在1.8版本中,引入了SPI自动服务发现。具体请参考第4章。

3.1.1 LoggerFactory.getLogger(Class)

调用LoggerFactory.getLogger(Class)的源码如下。getLogger会通过类的全限定名从LoggerFactory工厂中获取Logger。

public final class LoggerFactory {
    ...
    // 2.通过getILoggerFactory获取或创建一个可用的LoggerFactory,并通过该Factory获取或创建一个通过name指定的Logger。
    public static Logger getLogger(String name) {
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        return iLoggerFactory.getLogger(name);
    }
    // 1.将clazz的全限定名作为String,调用geteLogger(String)方法
    public static Logger getLogger(Class clazz) {
        Logger logger = getLogger(clazz.getName());
        if (DETECT_LOGGER_NAME_MISMATCH) {
            // 如果开启了检测命名错误,那么如果clazz不存在,则会打印错误信息。此处忽略相关处理
            ...
        }
        return logger;
    }
    
}

3.1.2 getILoggerFactory()创建Logger工厂

performInitialization()中,SLF4J调用bind()进行实现绑定,如果绑定成功,则会进行版本检查。SLF4J要求slf4j-api的版本必须与其实现的版本对应,否则可能会发生兼容性问题(SLF4J的1.8版与更早的版本,比如1.6和1.7存在兼容性问题)。

绑定成功后,每次调用getILoggerFactory(),则会通过StaticLoggerBinder.getSingletion().getLoggerFactory()获取一个ILoggerFactory接口派生的工厂对象,用来创建具体的Logger实例。

public final class LoggerFactory {
    // 使用volatile的INITIALIZATION_STATE确保只会发生一次绑定
    static volatile int INITIALIZATION_STATE = UNINITIALIZED;
    ...
    
    // 3.绑定SLF4J实现
    private final static void performInitialization() {
        // 4.bind()方法是SLF4J实现绑定的关键
        bind();
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            // 5.如果初始化成功,则检查SLF4J的实现支持的版本号是否与SLF4J匹配
            // SLF4J要求binder的版本与slf4j-api的版本匹配,否则打印一条警告信息,因为slf4j可能会不工作。
            versionSanityCheck();
        }
    }
    ...

    // 1.获取ILoggerFactory的实现
    public static ILoggerFactory getILoggerFactory() {
        // 2.通过volatile的INITIALIZATION_STATE确保只会发生一次绑定
        if (INITIALIZATION_STATE == UNINITIALIZED) {
            synchronized (LoggerFactory.class) {
                if (INITIALIZATION_STATE == UNINITIALIZED) {
                    INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                    performInitialization();
                }
            }
        }
        switch (INITIALIZATION_STATE) {
        case SUCCESSFUL_INITIALIZATION:
            // 6.如果初始化成功,则调用StaticLoggerBinder单例的getLoggerFactory()方法获得LoggerFactory工厂对象
            return StaticLoggerBinder.getSingleton().getLoggerFactory();
        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_FACTORY;
        }
        throw new IllegalStateException("Unreachable code");
    }
}

3.1.3 bind()方法绑定SLF4J实现(初始化)

最后,来看一下bind()方法的实现,注意代码中的注释。

在第2步中,我们可以看到SLF4J静态绑定的方式。它强制了StaticLoggerBinder的很多实现细节:

  • 必须是一个单例
  • 必须提供一个静态的getSingleton()方式创建/获取单例
  • 类的全限定名必须是"org.slf4j.impl.StaticLoggerBinder"
  • 必须提供一个static final String REQUESTED_API_VERSION对象指定支持的版本

对其他几个Binder:StaticMarkerBinder和StaticMDCBinder,SLF4J也有类似的强制实现要求。

public final class LoggerFactory {
    private final static void bind() {
        try {
            Set staticLoggerBinderPathSet = null;
            // skip check under android, see also
            // http://jira.qos.ch/browse/SLF4J-328
            if (!isAndroid()) {
                // 1.针对非android系统,通过ClassLoader寻址org/slf4j/impl/StaticLoggerBinder.class的可用实现。
                // 如果超过1个,则发出警告信息。最终SLF4J会选择其中的一个进行绑定。
                staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            }
            // the next line does the binding
            // 2.静态绑定,创建StaticLoggerBinder的单例
            StaticLoggerBinder.getSingleton();
            // 3.修改初始化状态
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            // 4.报告实际绑定的StaticLoggerBinder信息
            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);
        }
    }
}

4.SLF4J 1.8版的改进

【注】也可以从github上下载本节demo:

$ git clone git@github.com:jinluu/slf-demo.git
$ git checkout -b slf4j18 origin/slf4j18

SLF4J 1.8中最大的改进就是摒弃了hard code的代码绑定(参考3.1.3,注释2),而是使用了更加优雅、耦合更松的SPI方式进行服务发现。我们看看1.8版本中对日志绑定的改进:

  1. 提供了org.slf4j.spi.SLF4JServiceProvider服务接口用于SPI绑定
  2. 改进了org.slf4j.LoggerFactory.bind()的实现,采用SPI方式进行SLF4JServiceProvider服务发现和绑定
  3. 不再支持1.8版本以前的按照约定的类型StaticXxxBinder约定类名进行绑定的方式
  4. 去除了3.1.3节中对StaticLoggerBinder的所有强制约定

可见,1.8版本和之前的版本是完全不兼容的,且1.8版本明显更加优雅。

4.1 SLF4JServiceProvider

类图如下,只要将该接口的实现暴露成一个SPI服务,SLF4J就可以正常绑定到该logging实现上。

SLF4J深入剖析(涵盖SLF4J 1.8)_第3张图片
slf.plantuml.txt

4.2 绑定

通过代码及注释,可以发现:

  1. bind()只会通过SPI服务发现的方式寻找可用的日志服务。
    因此,如果采用了1.8版本的slf4j-api,则不支持1.8的日志实现不会被加载
  2. 如果发现了多于一个基于SPI的日志服务,则打印告警,并默认绑定第一个被发现的服务
  3. 如果没有发现基于SPI的日志服务,则默认绑定到SPI的NOP日志服务,并尝试通过指定全限定名的方式(org.slf4j.impl.StaticLoggerBinder)寻址旧版本的日志服务,如果找到了则发出版本mismatch告警,但是不会尝试加载老版本的日志服务。

因此,如果使用新版本的SLF4J(1.8及以上),务必使用对应的binder类,避免引起兼容性问题

除了采用了更优雅的服务发现机制,在其他方面,SLF4J 1.8与之前版本差别很小。

class LoggerFactory {
    private final static void performInitialization() {
        // 1.执行绑定
        bind();
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            // 2.成功,则进行版本检查
            // 注意:老版本不支持SPI,压根不会运行到这里
            versionSanityCheck();
        }
    }
    private final static void bind() {
        try {
            // 3.SPI服务发现
            List providersList = findServiceProviders();
            // 4.SPI发现多于一个日志实现则发出警告信息
            reportMultipleBindingAmbiguity(providersList);
            if (providersList != null && !providersList.isEmpty()) {
                // 5.绑定第一个被发现的logging服务
                PROVIDER = providersList.get(0);
                PROVIDER.initialize();
                INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
                // 6.报告真实绑定的logging信息
                reportActualBinding(providersList);
                fixSubstituteLoggers();
                replayEvents();
                // release all resources in SUBST_FACTORY
                SUBST_PROVIDER.getSubstituteLoggerFactory().clear();
            } else {
                // 7.如果通过SPI没有发现可用服务,则默认采用NOP日志
                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.");

                // 8.尝试寻找老版本的日志服务(通过寻址`org/slf4j/impl/StaticLoggerBinder.class`)
                // 找到则打印警告信息,但不会尝试绑定老日志服务
                Set staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportIgnoredStaticLoggerBinders(staticLoggerBinderPathSet);
            }
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        }
    }
}

4.3 logback对服务发现机制的改进

目前最新的SLF4J 1.8版处于slf4j-api:1.8.0-beta-2版本,对应的logback-classic版本为logback-classic:1.3.0-alpha4(官方对应1.8.0-beta-1,但是与beta-2兼容)。

为了兼容1.8的SLF4J,logback-classic提供了SPI服务配置文件,如下图。这样,在启动阶段,SLF4J就可以通过ServiceLoader找到logback-classic并进行注册了。

同时,最新版的logback也去掉了org.slf.impl包,彻底摒弃了老版本SLF4J的支持。

SLF4J深入剖析(涵盖SLF4J 1.8)_第4张图片
image

4.4 SPI

关于SPI服务的深度剖析,请参考笔者之前的博文『Service Provider Interface详解 (SPI)』


附录

  1. SLF4j官网
  2. logback官网
  3. demo

你可能感兴趣的