当前位置:首页 > 后端开发 > 正文内容

面试官:Java 的 SPI 都不了解?这很难让你经过啊!

邻居的猫1个月前 (12-09)后端开发2122

导言

今天和咱们共享一下一个在 Java 开发中非常重要的概念—— SPI(Service Provider Interface)SPI 直译叫做服务供给者接口,是一种用于动态加载服务的机制。它不仅能够协助咱们构建愈加灵敏和可扩展的运用程序,还能让咱们的代码愈加简练和易于保护。期望经过本文,咱们能够对 SPI 有一个全面而深化的了解,并能学会在实践项目中去运用它。

Java SPI 机制概述

界说与开展

SPI 是一种服务发现机制,它答应咱们的运用程序在运行时动态地发现和加载服务供给者。简略来说,SPI 便是经过一种标准化的办法来进行功用扩展,而无需修正中心代码。这种机制使得运用程序能够愈加灵敏地习惯不同的需求和环境。

SPIJava 的一个内置标准,JavaJDBC 便是运用 SPI 机制来加载不同的数据库驱动,如 MySQLPostgreSQL 等。跟着 Java 渠道的开展,SPI 机制也逐步被广泛运用于 Java 生态中的各种其他场景,如日志结构、音讯行列等。作为 Java 的标准扩展机制,SPI 极大地简化了插件化开发,使得运用更易于扩展。

SPI 机制的组成要素

SPI 机制首要由以下几个要害组件构成(以 JDBCMySQL 驱动程序为例):

  • 服务接口:界说服务的标准接口,一切服务供给者有必要完成此接口。

    java.sql.Driver

  • 服务供给者:完成了服务接口的详细完成类。

    com.mysql.cj.jdbc.Driver

  • 装备文件:坐落 META-INF/services 目录下的文件,文件名是服务接口的全约束名,文件内容是服务供给者完成类的全约束名列表。

  • 服务加载器ServiceLoader 类,担任读取装备文件并加载服务供给者。

    java.util.ServiceLoader

总结:经过上述这几个要害要素,咱们不难看出,其实 SPI 机制的中心思维便是:解耦合。它拟定了一套接口标准和一套服务发现机制,将服务的详细完成转移到运用之外,经过标准化装备的办法动态进行服务的加载,进步的运用的灵敏性和扩展性。

Java SPI 的作业原理及源码剖析

作业原理

Java SPI 机制经过 ServiceLoader 类来完成服务的动态加载。ServiceLoader 会查找 META-INF/services 目录下的装备文件,然后依据装备文件中的信息加载相应的服务供给者。

源码剖析

接下来,咱们经过阅览源码的办法,来看一下 ServiceLoader 的作业流程,搞清楚 ServiceLoader 怎么解析并加载服务的,咱们就把握了 SPI 的作业原理了。

先来看一下 ServiceLoader 类的成员变量:

public final class ServiceLoader<S> implements Iterable<S> {
    // 装备文件目录
    private static final String PREFIX = "META-INF/services/";

    // 需要被 SPI 加载的服务
    private final Class<S> service;

    // 用于加载和实例化 SPI 服务的类加载器
    private final ClassLoader loader;

    // 创立 ServiceLoader 时的拜访操控上下文
    private final AccessControlContext acc;

    // 按实例化次序缓存 SPI 服务供给者
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 当时的懒查找迭代器
    private LazyIterator lookupIterator;

}

下面,咱们以 JDBC 加载数据库驱动程序时的代码片段为例,看一下 SPI 是怎么运用的:

public class DriverManager {
    static {
        // 经过查看体系特点 jdbc.properties 加载初始 JDBC 驱动程序,然后运用 ServiceLoader 机制
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
}

private static void loadInitialDrivers() {
    // 加载 java.sql.Driver 类型的服务,回来 ServiceLoader 实例
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    // ServiceLoader 完成了 Iterable 接口偏重写了 iterator 办法,调用 iterator 办法回来一个迭代器
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    try{
        // 迭代器的遍历操作,获取一切可用的服务供给者实例
        while(driversIterator.hasNext()) {
            driversIterator.next();
        }
    } catch(Throwable t) {
        // Do nothing
    }
}

在这段代码中:

  • DriverManager 在静态代码块中调用了 loadInitialDrivers 办法。
  • ServiceLoader.load(Driver.class):创立了一个 ServiceLoader 实例,该实例担任查找并加载完成了 Driver 接口的一切服务供给者。
  • loadedDrivers.iterator():获取一个迭代器,用于遍历一切已加载的 Driver 实例。

能够看到,当运用 SPI 机制动态加载服务时,首要是经过 ServiceLoader.load 办法来完成的,这个办法会创立一个 ServiceLoader 实例。然后调用 iterator 办法,经过回来的迭代器获取一切可用的服务供给者实例。当调用 iterator 办法时,在办法内部 ServiceLoader 会先判别缓存 providers 中是否有数据:假如有,则直接回来缓存 providers 的迭代器;假如没有,则回来懒查找迭代器的迭代器。接下来,咱们来看下这部分的源码:

// service是需要被加载的 SPI 接口类型
public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    // 获取当时线程上下文的类加载器,用于加载 SPI 服务,然后调用重载结构办法。
    return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
    // 创立 ServiceLoader 实例
    return new ServiceLoader<>(service, loader);
}

// 私有结构办法
private ServiceLoader(Class<S> svc, ClassLoader cl) {
    // 非空校验
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    // 调用 reload 办法,从头加载 SPI 服务
    reload();
}

public void reload() {
    // 清空缓存中一切已实例化的 SPI 服务
    providers.clear();
    // 创立懒查找迭代器,用于推迟加载服务供给者。
    lookupIterator = new LazyIterator(service, loader);
}

// Iterable 接口完成,回来一个匿名内部类迭代器
public Iterator<S> iterator() {
    return new Iterator<S>() {

        // 已缓存的 SPI 服务供给者的迭代器
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        public boolean hasNext() {
            // 优先判别缓存中是否存在,有则回来
            if (knownProviders.hasNext())
                return true;
            // 没有,则回来懒查找迭代器的迭代器
            return lookupIterator.hasNext();
        }

        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}

从上述源码中能够看出,假如缓存中没有的话,那么会履行懒查找迭代器 lookupIterator 的办法,下面咱们看下 LazyIterator 类中的中心办法:hasNextServicehasNextService

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // 拼接 META-INF/services/ + SPI 接口的全约束名
            String fullName = PREFIX + service.getName();
            // 经过类加载器,加载 fullName 途径的资源文件,也便是 SPI 的装备文件
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        // 解析装备文件的内容,文件内容是服务供给者完成类的全约束名列表
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        // 依据从装备文件中解析到的 `SPI` 完成类的全约束名,经过反射获取其 Class 目标
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    // 类型查验,校验下供给的 SPI 完成是否为 SPI 服务接口类型
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
        // 创立 SPI 服务目标
        S p = service.cast(c.newInstance());
        // 加入到缓存傍边
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

好了,到这儿的话 Java SPI 的中心代码咱们根本现已剖析完了,经过上述对 ServiceLoader 的源码剖析,信任咱们对 Java SPI 机制的作业原理现已有了深化的了解,正所谓“实践出真知”,咱们能够去自界说 SPI 手动实践一下啦~

APISPI 的差异

API (Application Programming Interface)

API 是运用程序编程接口,界说了一组规矩和协议,用于不同软件组件之间的交互。API 一般由一组函数、办法、类、变量等组成,为开发者供给了拜访特定功用或数据的办法。API 的规划意图是为了封装复杂性,供给一个明晰、共同的接口,使得开发者能够更便利地运用底层功用。经过 API,开发者能够运用预界说的功用而无需了解其内部完成细节。

SPI (Service Provider Interface)

SPI 是一种服务供给者接口,它界说了一种服务的标准接口,答应不同的服务供给者完成这个接口。SPI 的首要意图是为了完成服务的动态发现和加载,然后进步体系的灵敏性和可扩展性。与 API 不同,SPI 着重的是服务供给者的发现和加载,而不是直接供给功用。

综上所述,APISPI 的本质差异在于

  • API 由服务供给方供给接口标准,界说了怎么运用其功用,并向外部露出这些接口。
  • SPI 由服务调用方供给接口标准,界说了一个标准接口,然后由不同的服务供给者完成这个接口,然后完成服务的动态发现和加载。

差异比照

为了便利了解,请看下图:

API 与 SPI 差异比照

SPI 机制的优劣势

优势

  • 解耦服务接口与完成:将服务接口和完成别离,使得服务接口无需重视服务完成类的详细完成,完成了服务接口与服务完成的解耦。
  • 便于扩展和保护:比如在新增服务供给者时,只需增加新的完成类和装备文件,无需修正现有代码。

缺乏

  • 强依靠类加载器SPI 强依靠于类加载器,它的完成类有必要放置在运用的类途径下才干被动态的发现和加载,这约束了服务发现的灵敏性。
  • 不能按需加载SPI 会对类途径下的完成进行悉数加载,在很多服务供给者的状况下,加载进程或许会有功用开支。

Spring 结构中的 SPI

Spring 结构并没有直接运用 JavaSPI 机制,而是采用了相似 SPI 的机制完成了自己的扩展点机制。以 Spring Boot 的主动安装为例:Spring Boot 的主动安装机制经过扫描 spring.factories 文件中的装备,加载相应的主动装备类,而这种约好装备的办法便是经过 SPI 机制完成的。

按需加载

Spring Boot 的主动装备机制可经过条件注解(如 @ConditionalOnClass@ConditionalOnMissingBean 等)来决议是否加载某个装备类。这种办法使得 Spring Boot 能够依据当时环境和依靠状况,按需加载装备类,避免了 Java SPI 中悉数加载形成的不必要的功用开支。

关于 Spring Boot 主动安装的原理,请看我的这篇文章 SpringBoot 主动安装原理

还有哪些 SPI 运用事例?

  • JDBCJDBC 运用 SPI 机制来加载不同的数据库驱动。例如,MySQLPostgreSQL 都有各自的 JDBC 驱动完成,但它们都完成了 java.sql.Driver 接口。经过 SPI 机制,JDBC 能够动态加载所需的数据库驱动,而无需硬编码。
  • DubboDubbo 是一个高功用的 Java RPC 结构,它运用 SPI 机制来扩展其功用。Dubbo 经过 META-INF/dubbo/ 目录下的装备文件来加载各种扩展点,如协议、过滤器、注册中心等。这使得 Dubbo 具有高度的可扩展性和灵敏性。
  • SLF4J:它运用 SPI 机制来发现和加载详细的日志完成。用户能够依据需要挑选或替换日志完成,而无需修正运用程序代码。

结语

到这儿,关于 JavaSPI 机制就介绍完了,感谢咱们的阅览!假如你有任何疑问或主张,欢迎在谈论区留言沟通。


更多精彩内容,请微信查找并重视【Java驿站】大众号。

扫描二维码推送至手机访问。

版权声明:本文由51Blog发布,如需转载请注明出处。

本文链接:https://www.51blog.vip/?id=166

分享给朋友:

“面试官:Java 的 SPI 都不了解?这很难让你经过啊!” 的相关文章

FPGA驱动adc128s052的几个问题

FPGA驱动adc128s052的几个问题

FPGA驱动adc128s052的若干细节问题 usbblaster最好是直接与电脑USB口衔接, 运用拓宽坞会呈现古怪驱动问题. adc数据手册阐明 附上adc128s052时序手册 ADC芯片cs引脚持续拉低,则每次采完16bit后持续新的16bit 留意 : adc128s052数据手册信号针...

Flutter/Dart第06天:Dart根底语法详解(变量)

Flutter/Dart第06天:Dart根底语法详解(变量)

Dart官网文档:https://dart.dev/language/variables 重要说明:本博客依据Dart官网文档,但并不是简略的对官网进行翻译,在掩盖中心功用情况下,我会依据个人研制经历,参加自己的一些扩展问题和场景验证。 Dart中的变量 变量是一个目标的引证,引证名便是变量的称号;...

php开源系统,优势、应用与未来趋势

php开源系统,优势、应用与未来趋势

PHP开源系统有很多种,涵盖了不同的应用砛n2. PbootCMS: 特点:全新内核,永久开源免费,适合企业网站开发建设。 用途:高效、简洁、强大的CMS系统。 3. ThinkSAAS: 特点:基于PHP MySQL,支持Apache和Nginx,支持php7版本。...

php中文乱码, PHP中文乱码的原因

php中文乱码, PHP中文乱码的原因

1. 设置字符编码: 在PHP文件的开头,使用 `` 来设置输出内容的字符编码为UTF8。 确保你的PHP文件本身也是保存为UTF8编码。2. 数据库连接: 如果你在使用数据库,确保数据库、数据库表和数据库列都使用UTF8编码。 在连接数据库时,设置字符集为UTF8,例如使用...

php开源商城,助力电商企业快速搭建线上平台

1. 萤火商城V2.0 轻量级、高性能、前后端分离的电商系统。 支持微信小程序、H5、公众号、APP。 前后端源码完全开源,支持二次开发。 允许个人学习研究使用,支持二次开发,允许商业用途(仅限自运营)。 2. ShopXO 企业级免费开源商城系统,基于Think...

rust是什么,什么是Rust?

rust是什么,什么是Rust?

Rust 是一种系统级编程语言,由 Mozilla 研究院开发。它设计用于安全、并发和实用的系统编程。Rust 旨在提供内存安全保证,同时保持高性能。Rust 的主要特点包括:1. 内存安全:Rust 通过所有权(ownership)和借用检查(borrow checking)机制来确保内存安全。这...