源码级深度理解 Java SPI

作者:vivo 互联网服务器团队- Zhang Peng
SPI 是一种用于动态加载服务的机制 。它的核心思想就是解耦,属于典型的微内核架构模式 。SPI 在 Java 世界应用非常广泛,如:Dubbo、Spring Boot 等框架 。本文从源码入手分析,深入探讨 Java SPI 的特性、原理 , 以及在一些比较经典领域的应用 。
一、SPI 简介SPI 全称 Service Provider Interface,是 Java 提供的,旨在由第三方实现或扩展的 API , 它是一种用于动态加载服务的机制 。Java 中 SPI 机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦 。
Java SPI 有四个要素:
  • SPI 接口:为服务提供者实现类约定的的接口或抽象类 。
  • SPI 实现类:实际提供服务的实现类 。
  • SPI 配置:Java SPI 机制约定的配置文件 , 提供查找服务实现类的逻辑 。配置文件必须置于 META-INF/services 目录中,并且,文件名应与服务提供者接口的完全限定名保持一致 。文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称 。
  • ServiceLoader:Java SPI 的核心类,用于加载 SPI 实现类 。ServiceLoader 中有各种实用方法来获取特定实现、迭代它们或重新加载服务 。
二、SPI 示例正所谓,实践出真知,我们不妨通过一个具体的示例来看一下,如何使用 Java SPI 。
2.1 SPI 接口首先,需要定义一个 SPI 接口 , 和普通接口并没有什么差别 。
package io.github.dunwu.javacore.spi;public interface DataStorage {String search(String key);}2.2 SPI 实现类假设,我们需要在程序中使用两种不同的数据存储——MySQL 和 Redis 。因此 , 我们需要两个不同的实现类去分别完成相应工作 。
MySQL查询 MOCK 类
package io.github.dunwu.javacore.spi;public class MysqlStorage implements DataStorage {@Overridepublic String search(String key) {return "【Mysql】搜索" + key + ",结果:No";}}Redis 查询 MOCK 类
package io.github.dunwu.javacore.spi;public class RedisStorage implements DataStorage {@Overridepublic String search(String key) {return "【Redis】搜索" + key + ",结果:Yes";}}service 传入的是期望加载的 SPI 接口类型 到目前为止,定义接口,并实现接口和普通的 Java 接口实现没有任何不同 。
2.3 SPI 配置如果想通过 Java SPI 机制来发现服务,就需要在 SPI 配置中约定好发现服务的逻辑 。配置文件必须置于 META-INF/services 目录中,并且 , 文件名应与服务提供者接口的完全限定名保持一致 。文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称 。以本示例代码为例,其文件名应该为io.github.dunwu.javacore.spi.DataStorage , 
文件中的内容如下:
io.github.dunwu.javacore.spi.MysqlStorageio.github.dunwu.javacore.spi.RedisStorage2.4 ServiceLoader完成了上面的步骤,就可以通过 ServiceLoader 来加载服务 。示例如下:
import java.util.ServiceLoader;public class SpiDemo {public static void main(String[] args) {ServiceLoader<DataStorage> serviceLoader = ServiceLoader.load(DataStorage.class);System.out.println("============ Java SPI 测试============");serviceLoader.forEach(loader -> System.out.println(loader.search("Yes Or No")));}}输出:
============ Java SPI 测试============【Mysql】搜索Yes Or No , 结果:No【Redis】搜索Yes Or No,结果:Yes三、SPI 原理上文中,我们已经了解 Java SPI 的要素以及使用 Java SPI 的方法 。你有没有想过,Java SPI 和普通 Java 接口有何不同 , Java SPI 是如何工作的 。实际上,Java SPI 机制依赖于 ServiceLoader 类去解析、加载服务 。因此,掌握了 ServiceLoader 的工作流程,就掌握了 SPI 的原理 。ServiceLoader 的代码本身很精练 , 接下来,让我们通过走读源码的方式,逐一理解 ServiceLoader 的工作流程 。
3.1 ServiceLoader 的成员变量先看一下 ServiceLoader 类的成员变量 , 大致有个印象,后面的源码中都会使用到 。
public final class ServiceLoader<S> implements Iterable<S> {// SPI 配置文件目录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;// ...}

推荐阅读