SPI 是一种用于动态加载服务的机制。它的核心思想就是解耦,属于典型的微内核架构模式。SPI 在 Java 世界应用非常广泛,如:Dubbo、Spring Boot 等框架。本文从源码入手分析,深入探讨 Java SPI 的特性、原理,以及在一些比较经典领域的应用。
SPI 全称 Service Provider Interface,是 Java 提供的,旨在由第三方实现或扩展的 API,它是一种用于动态加载服务的机制。Java 中 SPI 机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。
Java SPI 有四个要素:
正所谓,实践出真知,我们不妨通过一个具体的示例来看一下,如何使用 Java SPI。
首先,需要定义一个 SPI 接口,和普通接口并没有什么差别。
1 2 3 4 5 |
package io.github.dunwu.javacore.spi; public interface DataStorage { String search(String key); } |
假设,我们需要在程序中使用两种不同的数据存储——MySQL 和 Redis。因此,我们需要两个不同的实现类去分别完成相应工作。
MySQL查询 MOCK 类
1 2 3 4 5 6 7 8 |
package io.github.dunwu.javacore.spi; public class MysqlStorage implements DataStorage { @Override public String search(String key) { return "【Mysql】搜索" + key + ",结果:No" ; } } |
Redis 查询 MOCK 类
1 2 3 4 5 6 7 8 |
package io.github.dunwu.javacore.spi; public class RedisStorage implements DataStorage { @Override public String search(String key) { return "【Redis】搜索" + key + ",结果:Yes" ; } } |
service 传入的是期望加载的 SPI 接口类型 到目前为止,定义接口,并实现接口和普通的 Java 接口实现没有任何不同。
如果想通过 Java SPI 机制来发现服务,就需要在 SPI 配置中约定好发现服务的逻辑。配置文件必须置于 META-INF/services 目录中,并且,文件名应与服务提供者接口的完全限定名保持一致。文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。以本示例代码为例,其文件名应该为io.github.dunwu.javacore.spi.DataStorage,
文件中的内容如下:
1 2 |
io.github.dunwu.javacore.spi.MysqlStorage io.github.dunwu.javacore.spi.RedisStorage |
完成了上面的步骤,就可以通过 ServiceLoader 来加载服务。示例如下:
1 2 3 4 5 6 7 8 9 10 11 |
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" ))); } } |
输出:
1 2 3 |
============ Java SPI 测试============ 【Mysql】搜索Yes Or No,结果:No 【Redis】搜索Yes Or No,结果:Yes |
上文中,我们已经了解 Java SPI 的要素以及使用 Java SPI 的方法。你有没有想过,Java SPI 和普通 Java 接口有何不同,Java SPI 是如何工作的。实际上,Java SPI 机制依赖于 ServiceLoader 类去解析、加载服务。因此,掌握了 ServiceLoader 的工作流程,就掌握了 SPI 的原理。ServiceLoader 的代码本身很精练,接下来,让我们通过走读源码的方式,逐一理解 ServiceLoader 的工作流程。
先看一下 ServiceLoader 类的成员变量,大致有个印象,后面的源码中都会使用到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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; // ... } |
(1)ServiceLoader.load 静态方法
应用程序加载 Java SPI 服务,都是先调用 ServiceLoader.load 静态方法。
ServiceLoader.load 静态方法的作用是:
① 指定类加载 ClassLoader 和访问控制上下文;
② 然后,重新加载 SPI 服务
清空缓存中所有已实例化的 SPI 服务
根据 ClassLoader 和 SPI 类型,创建懒加载迭代器
这里,摘录 ServiceLoader.load 相关源码,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// service 传入的是期望加载的 SPI 接口类型 // loader 是用于加载 SPI 服务的类加载器 public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { return new ServiceLoader<>(service, loader); } public void reload() { // 清空缓存中所有已实例化的 SPI 服务 providers.clear(); // 根据 ClassLoader 和 SPI 类型,创建懒加载迭代器 lookupIterator = new LazyIterator(service, loader); } // 私有构造方法 // 重新加载 SPI 服务 private ServiceLoader(Class<S> svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null" ); // 指定类加载 ClassLoader 和访问控制上下文 loader = (cl == null ) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null ) ? AccessController.getContext() : null ; // 然后,重新加载 SPI 服务 reload(); } |
(2)应用程序通过 ServiceLoader 的 iterator 方法遍历 SPI 实例
ServiceLoader 的类定义,明确了 ServiceLoader 类实现了 Iterable<T> 接口,所以,它是可以迭代遍历的。实际上,ServiceLoader 类维护了一个缓存 providers( LinkedHashMap 对象),缓存 providers 中保存了已经被成功加载的 SPI 实例,这个 Map 的 key 是 SPI 接口实现类的全限定名,value 是该实现类的一个实例对象。
当应用程序调用 ServiceLoader 的 iterator 方法时,ServiceLoader 会先判断缓存 providers 中是否有数据:如果有,则直接返回缓存 providers 的迭代器;如果没有,则返回懒加载迭代器的迭代器。