关于slf4j:SLF4J门面日志框架源码探索-京东云技术团队

4次阅读

共计 8083 个字符,预计需要花费 21 分钟才能阅读完成。

1 SLF4J 介绍

SLF4J 即 Simple Logging Facade for Java,它提供了 Java 中所有日志框架的简略外观或形象。因而,它使用户可能应用单个依赖项解决任何日志框架,例如:Log4j,Logback 和 JUL(java.util.logging)。通过在类门路中插入适当的 jar 文件(绑定),能够在部署时插入所需的日志框架。如果要更换日志框架,仅仅替换依赖的 slf4j bindings。比方,从 java.util.logging 替换为 log4j,仅仅须要用 slf4j-log4j12-1.7.28.jar 替换 slf4j-jdk14-1.7.28.jar。

2 SLF4J 源码剖析

咱们通过代码动手,层层加码,直观感触 SLF4J 打印日志,并跟踪代码寻根究底。次要理解,SLF4J 是如何作为门面和其余日志框架进行解耦。

2.1 pom 只援用依赖 slf4j-api,版本是 1.7.30

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>

2.1.1 执行一个 Demo

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

2.1.2 日志提示信息

绑定 org.slf4j.impl.StaticLoggerBinder 失败。如果在类门路上没有找到绑定,那么 SL​​F4J 将默认为无操作实现

[]()

2.1.3 跟踪源码

点开办法 getLogger(),能够直观看到 LoggerFactory 应用动态工厂创立 Logger。通过以下办法,逐渐点击,报错也很容易找到,能够在 bind() 办法看到打印的异样日志信息。

org.slf4j.LoggerFactory#getLogger(java.lang.Class<?>)
org.slf4j.LoggerFactory#getLogger(java.lang.String)
org.slf4j.LoggerFactory#getILoggerFactory
org.slf4j.LoggerFactory#performInitialization
org.slf4j.LoggerFactory#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()) {staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            }
            // the next line does the binding
            StaticLoggerBinder.getSingleton();
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            reportActualBinding(staticLoggerBinderPathSet);
        } 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);
        } finally {postBindCleanUp();
        }
    }

进一步剖析绑定办法 findPossibleStaticLoggerBinderPathSet(),能够发现在以后 ClassPath 下查问了所有该门路的资源“org/slf4j/impl/StaticLoggerBinder.class”,这里可能没有加载到任何文件,也可能绑定多个,对没有绑定和绑定多个的场景进行了敌对提醒。这里通过门路加载资源的目标次要用来对加载的各种异样场景提醒。

再往下代码 StaticLoggerBinder.getSingleton() 才是理论的绑定,并且获取 StaticLoggerBinder 的实例。这里如果反编译,你会发现基本没有这个类 StaticLoggerBinder。

如果没有加载到文件,正如上边 demo 执行的后果一样,命中 NoSuchMethodError 异样,并打印没有绑定场景的提示信息。

办法 findPossibleStaticLoggerBinderPathSet() 的源码如下,能够发现类加载器通过门路获取 URL 资源。

            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            Enumeration<URL> paths;
            if (loggerFactoryClassLoader == null) {paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }

2.2 pom 援用依赖 logback-classic

        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>

2.2.1 执行 demo

能够看到失常的打印日志信息,并且没有任何异样

[]()

2.2.2 跟踪源码

这个时候如果再点击进入办法 StaticLoggerBinder.getSingleton(),发现类 StaticLoggerBinder 是由包 logback-classic 提供的,并且实现了 SLF4J 中的接口 LoggerFactoryBinder。StaticLoggerBinder 的创立用到了单例模式,该类次要目标返回一个创立 Logger 的工厂。这里理论返回了 ch.qos.logback.classic.LoggerContext 的实例,再由该实例创立 ch.qos.logback.classic.Logger。

UML 类图如下:

[]()

2.3 pom 再引入 log4j-slf4j-impl

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.9.1</version>
        </dependency>

2.3.1 执行 demo

打印日志如下,提醒绑定了两个 StaticLoggerBinder.class,但最终理论绑定的是 ch.qos.logback.classic.util.ContextSelectorStaticBinder。这里边也验证了一旦一个类被加载之后,全局限定名雷同的类就无奈被加载了。这里 Jar 包被加载的程序间接决定了类加载的程序。

SLF4J: Found binding in [jar:file:/D:/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/D:/.m2/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.9.1/log4j-slf4j-impl-2.9.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
18:19:43.521 [main] INFO com.cj.HelloSlf4j - Hello World info

2.4 log4j-slf4j-impl 和 logback-classic 的引入地位变换

如果 Pom 文件先引入 log4j-slf4j-impl,再引入 logback-classic

2.4.1 执行 demo

依据日志打印后果,能够看到理论绑定的是 org.apache.logging.slf4j.Log4jLoggerFactory;然而没有失常打印出日志,须要进行 log4j2 的日志配置。阐明理论绑定的是 og4j-slf4j-impl 包中的 org/slf4j/impl/StaticLoggerBinder.class 文件;这里也验证了如果有引入了多个桥接包,理论绑定的是先加载到的文件;

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/D:/.m2/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.9.1/log4j-slf4j-impl-2.9.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/D:/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]
ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console. Set system property 'log4j2.debug' to show Log4j2 internal initialization logging.

2.5 类加载形式的变动

2.5.1 slf4j-api-1.7.30 版本的打包技巧

反编译看 slf4j-api-1.7.30-sources.jar,发现压根没有这个类 org.slf4j.impl.StaticLoggerBinder,他怎么会编译胜利呢?猜测是不是打包的时候把这个类排除掉了呢?通过 git 下载源码发现 slf4j 源码其实是有这个文件的,org/slf4j/impl/StaticLoggerBinder.class;这里应用了一个小技巧,打包的时候把实现类排除掉了,尽管不太优雅,然而思路很奇妙。

2.5.2 slf4j-api-2.0.0 版本引入 SPI(Service Provider Interface)

该版本通过应用 SPI 形式进行实现类的加载,感觉比之前的实现形式优雅了很多。桥接包只须要在这个地位:META-INF/services/,定义一个文件 org.slf4j.spi.SLF4JServiceProvider(命名为 SLFJ4 提供的接口名),并且文件中指定实现类。只有引入这个桥接包,就能够适配到对应实现的日志框架。

以下是 SPI 形式加载的源码

private static List<SLF4JServiceProvider> findServiceProviders() {ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
        List<SLF4JServiceProvider> providerList = new ArrayList();
        Iterator var2 = serviceLoader.iterator();

        while(var2.hasNext()) {SLF4JServiceProvider provider = (SLF4JServiceProvider)var2.next();
            providerList.add(provider);
        }

        return providerList;
     }

2.5.3 类加载形式比照

[]()

2.6 SLF4J 官网曾经实现绑定的日志框架

slf4j 曾经提供了罕用日志框架的桥接包,以及具体的文档形容,应用起来非常简单。
下图是 SLF4J 官网中提供的,示意了各种日志实现框架和 SLF4J 的关系:

[]()

2.7 总结

  • SLF4J API 旨在一次绑定一个且仅一个底层日志框架。而且引入 SLF4J 后,不论是否能够加载到 StaticLoggerBinder,或者加载到多个 StaticLoggerBinder,都进行敌对提醒,用户体验上思考都很周到。如果类门路上存在多个绑定,SLF4J 将收回正告,列出这些绑定的地位。当类门路上有多个绑定可用时,应该抉择一个心愿应用的绑定,而后删除其余绑定。
  • 单纯看 SLF4J 源码,其实整体设计实现上都很简略明确,定位十分分明,就是做好门面。
  • 鉴于 SLF4J 接口及其部署模型的简略性,新日志框架的开发人员应该会发现编写 SLF4J 绑定非常容易。
  • 对于目前比拟支流的日志框架都通过实现适配进行兼容反对。只有用户抉择了 SLF4J,就能够确保当前变更日志框架的自在。

3 SLF4J 设计模式的应用

在 slf4j 中用到了一些经典的设计模式,比方门面模式、单例模式、动态工厂模式等,咱们来剖析以下几种设计模式。

3.1 门面模式(Facade Pattern)

1)解释

门面模式,也叫外观模式,要求一个子系统的内部与其外部的通信必须通过一个对立的对象进行。门面模式提供一个高层次的接口,使得子系统更易于应用。应用了门面模式,使客户端调用变得更加简略。

Slf4j 制订了 log 日志的应用规范,提供了高层次的接口,咱们编码过程只须要依赖接口 Logger 和工厂类 LoggerFactory 就能够实现日志的打印,齐全不必关怀日志外部的实现细节是 logback 实现的形式,还是 log4j 的实现形式。

2)图解

[]()

        Logger logger = LoggerFactory.getLogger(HelloSlf4j.class);
        logger.info("Hello World info");

3)长处

解耦,缩小零碎的相互依赖。所有的依赖都是对门面对象的依赖,与子系统无关,业务层的开发不须要关怀底层日志框架的实现及细节,在编码的时候也不须要思考日后更换框架所带来的老本。
接口和实现拆散,屏蔽了底层的实现细节,面向接口编程。

3.2 单例模式(Singleton Pattern)

1)解释

单例模式,确保一个类仅有一个实例,并提供一个拜访它的全局拜访点。
在 SLF4J 的适配包中都须要实现类 StaticLoggerBinder,而类 StaticLoggerBinder 的实现就用了单例模式,而且是最简略的实现办法,在动态初始化器中间接 new StaticLoggerBinder(),提供全局拜访办法获取该实例。

2)UML 图

[]()

3)长处

在单例模式中,流动的单例只有一个实例,对单例类的所有实例化失去的都是雷同的一个实例。这样就避免其它对象对本人的实例化,确保所有的对象都拜访一个实例
单例模式具备肯定的伸缩性,类本人来管制实例化过程,类就在扭转实例化过程上有相应的伸缩性。

提供了对惟一实例的受控拜访。

在内存里只有一个实例,缩小了内存的开销,进步零碎的性能。

4 启发

  • 只管 SLF4J 整体代码短小但很精炼,可见门面模式使用好的威力。门面模式也为咱们提供了对于多版本的实现如何对立定义接口以及兼容提供了参考。
  • SLF4J 定义和实现计划对用户都很敌对,同时又提供了各种桥接包,进行欠缺的文档领导应用。总之各项用户体验都很棒,这兴许也是 SLF4J 目前最受欢迎的起因之一吧。
  • 咱们要多思考面向接口编程的思维,升高代码耦合度,进步代码扩展性。
  • 应用 SPI 的形式,优雅的加载扩大实现。
  • 好产品是设计进去的,更是优化迭代进去的。

5 参考资料

  • slf4j 官网:https://www.slf4j.org/manual.html
    类加载:
  • https://docs.oracle.com/javase/7/docs/api/java/lang/ClassLoad…
  • https://docs.oracle.com/javase/7/docs/api/java/util/ServiceLo…
  • https://www.ibm.com/docs/en/sdk-java-technology/7.1?topic=cl-…

作者:京东物流 曹俊

起源:京东云开发者社区

正文完
 0