slf4j中是如何运用SPI机制兼容各类日志框架的?

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: slf4j中是如何运用SPI机制兼容各类日志框架的?

image.png完成了接口的定义,接口的实现,以及接口实现方按照指定的格式提供配置文件之后,就可以通过ServiceLoader#load方法加载接口的实现类实例了。关于加载的具体示例,可参考findServiceProviders方法中的代码。值得留意的是ServiceLoader#load方法会加载类路径中所有的jar里面对应的SPI配置类。

其实,ServiceLoader调用load方式并没有进行加载操作,只有当遍历的时候才真正加载和初始化配置文件中的类。

SPI实现的关键代码

上面我们已经提到,在调用load方法时ServiceLoader并不会加载对应的配置项,而是在遍历的时候进行加载。先来看一下ServiceLoader的load方法源码:





public static <S> ServiceLoader<S> load(Class<S> service) {    ClassLoader cl = Thread.currentThread().getContextClassLoader();    return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);}

可以看到只是初始化了一个ServiceLoader,并赋值相应的属性值。而真正进行初始化或加载操作是在foreach方法执行时。在iterator方法中会初始化一个LazyClassPathLookupIterator类,该类为ServiceLoader的内部类,同样实现了Iterator接口,在实现的hasNext和next方法中完成了配置文件的加载和类的实例化。下面来看一下该类的核心内容:
























































private final class LazyClassPathLookupIterator<T> implements Iterator<Provider<T>>{    // 扫描路径的前缀    static final String PREFIX = "META-INF/services/";
    // 避免重复,采用Set存储    Set<String> providerNames = new HashSet<>();     // 扫描到的路径集合    Enumeration<URL> configs;    Iterator<String> pending;
    Provider<T> nextProvider;    ServiceConfigurationError nextError;
    LazyClassPathLookupIterator() { }
    private Class<?> nextProviderClass() {        if (configs == null) {            try {                // 拼接扫描路径+名称                String fullName = PREFIX + service.getName();                if (loader == null) {                    configs = ClassLoader.getSystemResources(fullName);                } else if (loader == ClassLoaders.platformClassLoader()) {                    // The platform classloader doesn't have a class path,                    // but the boot loader might.                    if (BootLoader.hasClassPath()) {                        configs = BootLoader.findResources(fullName);                    } else {                        configs = Collections.emptyEnumeration();                    }                } else {                    configs = loader.getResources(fullName);                }            } catch (IOException x) {                fail(service, "Error locating configuration files", x);            }        }        while ((pending == null) || !pending.hasNext()) {            if (!configs.hasMoreElements()) {                return null;            }            // 解析配置文件中的内容            pending = parse(configs.nextElement());        }        String cn = pending.next();        try {            // 根据配置进行类的初始化            return Class.forName(cn, false, loader);        } catch (ClassNotFoundException x) {            fail(service, "Provider " + cn + " not found");            return null;        }    }    // ...}

上述代码省略了细节操作,我们可以看到该内部类会根据默认的路径类名进行拼接扫描,这也就是为什么我们第二步要按照指定的格式进行定义。扫描到对应的文件之后,会对文件中配置的类的全限定名字符串进行加载,最后通过Class#forName进行初始化。

关于ServiceLoader的源代码就讲这么多,大家如果有兴趣,可以通过断点来跟踪一下,更方便理解细节。看完源代码其实就很容易明白SPI机制,本质上SPI机制就是定义一个约定,大家都按照这个约定来,然后使用者根据约定去解析使用即可。明白了这个底层原理,读者自己也可以实现一个简单的SPI。而dubbo对SPI的实现就是基于自身的功能场景进行了拓展,这就是为什么我们要活学活用的原因。

小结

通过本篇文章我们了解了LoggerFactory在获取SLF4JServiceProvider时所使用的SPI机制,不同的日志框架直接或间接实现sl4j定义的接口,并遵从SPI机制,sl4j便会对其进行加载和初始化处理。同时,也阅读了一下ServiceLoader的核心代码,更方便学习和了解SPI。

前言

在上篇文章中我们讲到了LoggerFactory获取SLF4JServiceProvider实例类时使用到了双重加锁的机制,同时拓展了单例模式的典型场景。在完成加锁及判断之后,调用了初始化方法performInitialization。在该方法中最重要的操作便是调用bind方法。

在bind方法中才真正进行了SLF4JServiceProvider实例的初始化及获取操作。我们将bind方法进行简化处理,核心代码如下:












private static void bind() {    List<SLF4JServiceProvider> providersList = findServiceProviders();    if(!providersList.isEmpty()) {        PROVIDER = providersList.get(0);        PROVIDER.initialize();        INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;    } else {        INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;    }    postBindCleanUp();}

这节课我们重点来了解findServiceProviders是如何加载和获取SLF4JServiceProvider实例的底层机制。

Java的SPI机制

findServiceProviders方法的代码如下:









private static List<SLF4JServiceProvider> findServiceProviders() {    ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);    List<SLF4JServiceProvider> providerList = new ArrayList<>();    for(SLF4JServiceProvider provider : serviceLoader) {        providerList.add(provider);    }    return providerList;}

通过调用ServiceLoader#load方法,加载了一个具有遍历(实现了Iterable)能力的ServiceLoader对象。然后遍历ServiceLoader中的SLF4JServiceProvider对象,存入List当中并返回List。看起来很简单的一个操作,便涉及到我们今天要讲的SPI机制。ServiceLoader#load方法的调用便是使用SPI的标志。

什么是SPI

SPI是Service Provider Interface的缩写,是Java内置的服务发现机制。

在开发的过程中,可以将一些通用的功能抽象成API,对API提供各种具体的实现,这是很正常的场景。但以日志框架为例,是否可以通过提供一套API,当切换底层日志框架时不用修改调用它们API的代码,只用替换成新的实现了该API的jar包即可?很明显slf4j就是用来做这件事的,而它底层便运用了SPI机制。

通过Java的SPI机制,可以实现框架的动态扩展,让第三方的实现能像插件一样嵌入到系统中,我们经常使用数据库API便是另外一个应用场景。

SPI的简单实现

实现SPI主要分三步:1、定义一个接口;2、提供方的“META-INF/services”目录下新建一个名称为接口全限定名的文本文件,内容为接口实现类的全限定名。3、调用方通过ServiceLoader#load方法加载接口的实现类实例。

在此我们就不单独写示例了,直接以slf4j中的实现为例,来进行讲解。

第一步,定义一个接口,这个很显然,接口便是SLF4JServiceProvider:













public interface SLF4JServiceProvider {
    ILoggerFactory getLoggerFactory();        IMarkerFactory getMarkerFactory();        MDCAdapter getMDCAdapter();        String getRequesteApiVersion();        void initialize();}

那么,这个接口是由谁来实现呢?理论说应该是有各个日志框架提供方来实现。比如Logback日志框架就默认实现了该接口,实现类为ch.qos.logback.classic.spi.LogbackServiceProvider。为什么说是理论上呢?这里Logback与slf4j是同一作者,可以在Logback中实现该接口,但像Logback的竞争对手log4j2,并不会主动实现该接口的。

因此,在slf4j的项目中出现了一些名称为:slf4j-log4j12、slf4j-jdk14、slf4j-simple的项目。这些项目存在的目的之一就是来间接实现SLF4JServiceProvider接口。同时,还会提供第二步的配置。

下面来看第二步:提供方的“META-INF/services”目录下新建一个名称为接口全限定名的文本文件,内容为接口实现类的全限定名。

在slf4j中默认提供了一个空的Logger,对应的也有一个NOPServiceProvider实现类。与sl4j的同级项目中单独有一个slf4j-nop项目,就是来提供空日志记录操作的。

首先NOPServiceProvider实现了接口SLF4JServiceProvider,其次在slf4j-nop的META-INF/services目录下有一个名称为org.slf4j.spi.SLF4JServiceProvider的文本文件,文件内的内容为:


org.slf4j.helpers.NOPServiceProvider
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
2月前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
419 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
|
3月前
|
XML JSON Java
Logback 与 log4j2 性能对比:谁才是日志框架的性能王者?
【10月更文挑战第5天】在Java开发中,日志框架是不可或缺的工具,它们帮助我们记录系统运行时的信息、警告和错误,对于开发人员来说至关重要。在众多日志框架中,Logback和log4j2以其卓越的性能和丰富的功能脱颖而出,成为开发者们的首选。本文将深入探讨Logback与log4j2在性能方面的对比,通过详细的分析和实例,帮助大家理解两者之间的性能差异,以便在实际项目中做出更明智的选择。
353 3
|
3月前
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
147 1
|
3月前
|
数据采集 监控 Java
SpringBoot日志全方位超详细手把手教程,零基础可学习 日志如何配置及SLF4J的使用......
本文是关于SpringBoot日志的详细教程,涵盖日志的定义、用途、SLF4J框架的使用、日志级别、持久化、文件分割及格式配置等内容。
230 0
SpringBoot日志全方位超详细手把手教程,零基础可学习 日志如何配置及SLF4J的使用......
|
4月前
|
设计模式 SQL 安全
PHP中的设计模式:单例模式的深入探索与实践在PHP的编程实践中,设计模式是解决常见软件设计问题的最佳实践。单例模式作为设计模式中的一种,确保一个类只有一个实例,并提供全局访问点,广泛应用于配置管理、日志记录和测试框架等场景。本文将深入探讨单例模式的原理、实现方式及其在PHP中的应用,帮助开发者更好地理解和运用这一设计模式。
在PHP开发中,单例模式通过确保类仅有一个实例并提供一个全局访问点,有效管理和访问共享资源。本文详细介绍了单例模式的概念、PHP实现方式及应用场景,并通过具体代码示例展示如何在PHP中实现单例模式以及如何在实际项目中正确使用它来优化代码结构和性能。
58 2
|
3月前
|
SQL XML 监控
SpringBoot框架日志详解
本文详细介绍了日志系统的重要性及其在不同环境下的配置方法。日志用于记录系统运行时的问题,确保服务的可靠性。文章解释了各种日志级别(如 info、warn、error 等)的作用,并介绍了常用的日志框架如 SLF4J 和 Logback。此外,还说明了如何在 SpringBoot 中配置日志输出路径及日志级别,包括控制台输出与文件输出的具体设置方法。通过这些配置,开发者能够更好地管理和调试应用程序。
|
4月前
|
Java
日志框架log4j打印异常堆栈信息携带traceId,方便接口异常排查
日常项目运行日志,异常栈打印是不带traceId,导致排查问题查找异常栈很麻烦。
|
4月前
|
运维 NoSQL Java
SpringBoot接入轻量级分布式日志框架GrayLog技术分享
在当今的软件开发环境中,日志管理扮演着至关重要的角色,尤其是在微服务架构下,分布式日志的统一收集、分析和展示成为了开发者和运维人员必须面对的问题。GrayLog作为一个轻量级的分布式日志框架,以其简洁、高效和易部署的特性,逐渐受到广大开发者的青睐。本文将详细介绍如何在SpringBoot项目中接入GrayLog,以实现日志的集中管理和分析。
303 1
|
5月前
|
存储 监控 Java
Java日志通关(三) - Slf4j 介绍
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第三篇。
|
5月前
|
人工智能 Java Spring
Spring框架下,如何让你的日志管理像‘AI’一样智能,提升开发效率的秘密武器!
【8月更文挑战第31天】日志管理在软件开发中至关重要,不仅能帮助开发者追踪问题和调试程序,还是系统监控和运维的重要工具。在Spring框架下,通过合理配置Logback等日志框架,可大幅提升日志管理效率。本文将介绍如何引入日志框架、配置日志级别、在代码中使用Logger,以及利用ELK等工具进行日志聚合和分析,帮助你构建高效、可靠的日志管理系统,为开发和运维提供支持。
80 0