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中添加依赖:
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.25</version>
    </dependency>
  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依赖
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.7.25</version>
    </dependency>
  1. src/main/resources下创建log4j.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration>
    <appender name="myConsole" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="[%d{dd HH:mm:ss,SSS\} %-5p] [%t] %c{2\} - %m%n" />
        </layout>
    </appender>

    <!-- 指定logger的设置,additivity指示是否遵循缺省的继承机制-->
    <logger name="cn.jinlu.slf.demo" additivity="false">
        <level value ="info"/>
        <appender-ref ref="myConsole" />
    </logger>

    <!-- 根logger的设置-->
    <root>
        <level value ="warn"/>
        <appender-ref ref="myConsole"/>
    </root>
</log4j:configuration>

再次运行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的依赖:
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
  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功能。

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为例:
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<URL> 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实现上。

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<SLF4JServiceProvider> 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<URL> 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的支持。

image

4.4 SPI

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


附录

  1. SLF4j官网
  2. logback官网
  3. demo
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343