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

打印 上一主题 下一主题

主题 883|帖子 883|积分 2653

引言

今天和大家分享一下一个在 Java 开辟中非常重要的概念—— SPI(Service Provider Interface)。SPI 直译叫做服务提供者接口,是一种用于动态加载服务的机制。它不仅能够资助我们构建更加灵活和可扩展的应用步伐,还能让我们的代码更加简洁和易于维护。盼望通过本文,大家能够对 SPI 有一个全面而深刻的理解,并能学会在实际项目中去运用它。
Java SPI 机制概述

界说与发展

SPI 是一种服务发现机制,它允许我们的应用步伐在运行时动态地发现和加载服务提供者。简朴来说,SPI 就是通过一种尺度化的方式来进行功能扩展,而无需修改核心代码。这种机制使得应用步伐可以更加灵活地顺应差别的需求和环境。
SPI 是 Java 的一个内置尺度,Java 中 JDBC 就是利用 SPI 机制来加载差别的数据库驱动,如 MySQL、PostgreSQL 等。随着 Java 平台的发展,SPI 机制也逐渐被广泛应用于 Java 生态中的各种其他场景,如日志框架、消息队列等。作为 Java 的尺度扩展机制,SPI 极大地简化了插件化开辟,使得应用更易于扩展。
SPI 机制的组成要素

SPI 机制主要由以下几个关键组件构成(以 JDBC 中 MySQL 驱动步伐为例):

  • 服务接口:界说服务的尺度接口,所有服务提供者必须实现此接口。
    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 类的成员变量:
  1. public final class ServiceLoader<S> implements Iterable<S> {
  2.     // 配置文件目录
  3.     private static final String PREFIX = "META-INF/services/";
  4.     // 需要被 SPI 加载的服务
  5.     private final Class<S> service;
  6.     // 用于加载和实例化 SPI 服务的类加载器
  7.     private final ClassLoader loader;
  8.     // 创建 ServiceLoader 时的访问控制上下文
  9.     private final AccessControlContext acc;
  10.     // 按实例化顺序缓存 SPI 服务提供者
  11.     private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
  12.     // 当前的懒查找迭代器
  13.     private LazyIterator lookupIterator;
  14. }
复制代码
下面,我们以 JDBC 加载数据库驱动步伐时的代码片段为例,看一下 SPI 是如何利用的:
  1. public class DriverManager {
  2.     static {
  3.         // 通过检查系统属性 jdbc.properties 加载初始 JDBC 驱动程序,然后使用 ServiceLoader 机制
  4.         loadInitialDrivers();
  5.         println("JDBC DriverManager initialized");
  6.     }
  7. }
  8. private static void loadInitialDrivers() {
  9.     // 加载 java.sql.Driver 类型的服务,返回 ServiceLoader 实例
  10.     ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
  11.     // ServiceLoader 实现了 Iterable 接口并重写了 iterator 方法,调用 iterator 方法返回一个迭代器
  12.     Iterator<Driver> driversIterator = loadedDrivers.iterator();
  13.     try{
  14.         // 迭代器的遍历操作,获取所有可用的服务提供者实例
  15.         while(driversIterator.hasNext()) {
  16.             driversIterator.next();
  17.         }
  18.     } catch(Throwable t) {
  19.         // Do nothing
  20.     }
  21. }
复制代码
在这段代码中:

  • DriverManager 在静态代码块中调用了 loadInitialDrivers 方法。
  • ServiceLoader.load(Driver.class):创建了一个 ServiceLoader 实例,该实例负责查找并加载实现了 Driver 接口的所有服务提供者。
  • loadedDrivers.iterator():获取一个迭代器,用于遍历所有已加载的 Driver 实例。
可以看到,当利用 SPI 机制动态加载服务时,主要是通过 ServiceLoader.load 方法来实现的,这个方法会创建一个 ServiceLoader 实例。然后调用 iterator 方法,通过返回的迭代器获取所有可用的服务提供者实例。当调用 iterator 方法时,在方法内部 ServiceLoader 会先判断缓存 providers 中是否有数据:如果有,则直接返回缓存 providers 的迭代器;如果没有,则返回懒查找迭代器的迭代器。接下来,我们来看下这部分的源码:
  1. // service是需要被加载的 SPI 接口类型
  2. public static <S> ServiceLoader<S> load(Class<S> service) {
  3.     ClassLoader cl = Thread.currentThread().getContextClassLoader();
  4.     // 获取当前线程上下文的类加载器,用于加载 SPI 服务,然后调用重载构造方法。
  5.     return ServiceLoader.load(service, cl);
  6. }
  7. public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
  8.     // 创建 ServiceLoader 实例
  9.     return new ServiceLoader<>(service, loader);
  10. }
  11. // 私有构造方法
  12. private ServiceLoader(Class<S> svc, ClassLoader cl) {
  13.     // 非空校验
  14.     service = Objects.requireNonNull(svc, "Service interface cannot be null");
  15.     loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
  16.     acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
  17.     // 调用 reload 方法,重新加载 SPI 服务
  18.     reload();
  19. }
  20. public void reload() {
  21.     // 清空缓存中所有已实例化的 SPI 服务
  22.     providers.clear();
  23.     // 创建懒查找迭代器,用于延迟加载服务提供者。
  24.     lookupIterator = new LazyIterator(service, loader);
  25. }
  26. // Iterable 接口实现,返回一个匿名内部类迭代器
  27. public Iterator<S> iterator() {
  28.     return new Iterator<S>() {
  29.         // 已缓存的 SPI 服务提供者的迭代器
  30.         Iterator<Map.Entry<String,S>> knownProviders
  31.             = providers.entrySet().iterator();
  32.         public boolean hasNext() {
  33.             // 优先判断缓存中是否存在,有则返回
  34.             if (knownProviders.hasNext())
  35.                 return true;
  36.             // 没有,则返回懒查找迭代器的迭代器
  37.             return lookupIterator.hasNext();
  38.         }
  39.         public S next() {
  40.             if (knownProviders.hasNext())
  41.                 return knownProviders.next().getValue();
  42.             return lookupIterator.next();
  43.         }
  44.         public void remove() {
  45.             throw new UnsupportedOperationException();
  46.         }
  47.     };
  48. }
复制代码
从上述源码中可以看出,如果缓存中没有的话,那么会实行懒查找迭代器 lookupIterator 的方法,下面我们看下 LazyIterator 类中的核心方法:hasNextService 与 hasNextService
  1. private boolean hasNextService() {
  2.     if (nextName != null) {
  3.         return true;
  4.     }
  5.     if (configs == null) {
  6.         try {
  7.             // 拼接 META-INF/services/ + SPI 接口的全限定名
  8.             String fullName = PREFIX + service.getName();
  9.             // 通过类加载器,加载 fullName 路径的资源文件,也就是 SPI 的配置文件
  10.             if (loader == null)
  11.                 configs = ClassLoader.getSystemResources(fullName);
  12.             else
  13.                 configs = loader.getResources(fullName);
  14.         } catch (IOException x) {
  15.             fail(service, "Error locating configuration files", x);
  16.         }
  17.     }
  18.     while ((pending == null) || !pending.hasNext()) {
  19.         if (!configs.hasMoreElements()) {
  20.             return false;
  21.         }
  22.         // 解析配置文件的内容,文件内容是服务提供者实现类的全限定名列表
  23.         pending = parse(service, configs.nextElement());
  24.     }
  25.     nextName = pending.next();
  26.     return true;
  27. }
  28. private S nextService() {
  29.     if (!hasNextService())
  30.         throw new NoSuchElementException();
  31.     String cn = nextName;
  32.     nextName = null;
  33.     Class<?> c = null;
  34.     try {
  35.         // 根据从配置文件中解析到的 `SPI` 实现类的全限定名,通过反射获取其 Class 对象
  36.         c = Class.forName(cn, false, loader);
  37.     } catch (ClassNotFoundException x) {
  38.         fail(service,
  39.              "Provider " + cn + " not found");
  40.     }
  41.     // 类型检验,校验下提供的 SPI 实现是否为 SPI 服务接口类型
  42.     if (!service.isAssignableFrom(c)) {
  43.         fail(service,
  44.              "Provider " + cn  + " not a subtype");
  45.     }
  46.     try {
  47.         // 创建 SPI 服务对象
  48.         S p = service.cast(c.newInstance());
  49.         // 加入到缓存当中
  50.         providers.put(cn, p);
  51.         return p;
  52.     } catch (Throwable x) {
  53.         fail(service,
  54.              "Provider " + cn + " could not be instantiated",
  55.              x);
  56.     }
  57.     throw new Error();          // This cannot happen
  58. }
复制代码
好了,到这里的话 Java SPI 的核心代码我们根本已经分析完了,通过上述对 ServiceLoader 的源码分析,相信大家对 Java SPI 机制的工作原理已经有了深入的理解,正所谓“实践出真知”,大家可以去自界说 SPI 手动实践一下啦~
API 与 SPI 的区别

API (Application Programming Interface)

API 是应用步伐编程接口,界说了一组规则和协议,用于差别软件组件之间的交互。API 通常由一组函数、方法、类、变量等组成,为开辟者提供了访问特定功能或数据的方式。API 的设计目的是为了封装复杂性,提供一个清晰、一致的接口,使得开辟者可以更方便地利用底层功能。通过 API,开辟者可以利用预界说的功能而无需了解其内部实现细节。
SPI (Service Provider Interface)

SPI 是一种服务提供者接口,它界说了一种服务的尺度接口,允许差别的服务提供者实现这个接口。SPI 的主要目的是为了实现服务的动态发现和加载,从而进步体系的灵活性和可扩展性。与 API 差别,SPI 强调的是服务提供者的发现和加载,而不是直接提供功能。
综上所述,API 与 SPI 的本质区别在于

  • API 由服务提供方提供接口规范,界说了如何利用其功能,并向外部暴露这些接口。
  • SPI 由服务调用方提供接口规范,界说了一个尺度接口,然后由差别的服务提供者实现这个接口,从而实现服务的动态发现和加载。
区别对比

为了方便理解,请看下图:

SPI 机制的优劣势

上风


  • 解耦服务接口与实现:将服务接口和实现分离,使得服务接口无需关注服务实现类的具体实现,实现了服务接口与服务实现的解耦。
  • 便于扩展和维护:好比在新增服务提供者时,只需添加新的实现类和配置文件,无需修改现有代码。
不足


  • 强依赖类加载器:SPI 强依赖于类加载器,它的实现类必须放置在应用的类路径下才能被动态的发现和加载,这限制了服务发现的灵活性。
  • 不能按需加载:SPI 会对类路径下的实现进行全部加载,在大量服务提供者的情况下,加载过程可能会有性能开销。
Spring 框架中的 SPI

Spring 框架并没有直接利用 Java 的 SPI 机制,而是采用了雷同 SPI 的机制实现了自己的扩展点机制。以 Spring Boot 的自动装配为例:Spring Boot 的自动装配机制通过扫描 spring.factories 文件中的配置,加载相应的自动配置类,而这种约定配置的方式就是通过 SPI 机制实现的。
按需加载

Spring Boot 的自动配置机制可通过条件注解(如 @ConditionalOnClass、@ConditionalOnMissingBean 等)来决定是否加载某个配置类。这种方式使得 Spring Boot 可以根据当前环境和依赖情况,按需加载配置类,避免了 Java SPI 中全部加载造成的不须要的性能开销。
关于 Spring Boot 自动装配的原理,请看我的这篇文章 SpringBoot 自动装配原理
还有哪些 SPI 应用案例?


  • JDBC:JDBC 利用 SPI 机制来加载差别的数据库驱动。例如,MySQL 和 PostgreSQL 都有各自的 JDBC 驱动实现,但它们都实现了 java.sql.Driver 接口。通过 SPI 机制,JDBC 可以动态加载所需的数据库驱动,而无需硬编码。
  • Dubbo:Dubbo 是一个高性能的 Java RPC 框架,它利用 SPI 机制来扩展其功能。Dubbo 通过 META-INF/dubbo/ 目录下的配置文件来加载各种扩展点,如协议、过滤器、注册中心等。这使得 Dubbo 具有高度的可扩展性和灵活性。
  • SLF4J:它利用 SPI 机制来发现和加载具体的日志实现。用户可以根据需要选择或更换日志实现,而无需修改应用步伐代码。
结语

到这里,关于 Java 的 SPI 机制就介绍完了,感谢大家的阅读!如果你有任何疑问或建议,欢迎在评论区留言交流。
更多精彩内容,请微信搜刮并关注【Java驿站】公众号。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

刘俊凯

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表