源码级深度理解 Java SPI

2022/11/9 1:24:04

本文主要是介绍源码级深度理解 Java SPI,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

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 接口,和普通接口并没有什么差别。

1
2
3
4
5
package io.github.dunwu.javacore.spi;
 
public interface DataStorage {
    String search(String key);
}

2.2 SPI 实现类

假设,我们需要在程序中使用两种不同的数据存储——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 接口实现没有任何不同。

2.3 SPI 配置

如果想通过 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

2.4 ServiceLoader

完成了上面的步骤,就可以通过 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

三、SPI 原理

上文中,我们已经了解 Java SPI 的要素以及使用 Java SPI 的方法。你有没有想过,Java SPI 和普通 Java 接口有何不同,Java SPI 是如何工作的。实际上,Java SPI 机制依赖于 ServiceLoader 类去解析、加载服务。因此,掌握了 ServiceLoader 的工作流程,就掌握了 SPI 的原理。ServiceLoader 的代码本身很精练,接下来,让我们通过走读源码的方式,逐一理解 ServiceLoader 的工作流程。

3.1 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;
 
    // ...
}

3.2 ServiceLoader 的工作流程

(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 的迭代器;如果没有,则返回懒加载迭代器的迭代器。



这篇关于源码级深度理解 Java SPI的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程