SPI 机制详解

  1. SPI 的作用
  2. 基于 Interface 的 SPI 实现
  3. JDK 中的 SPI 实现

SPI, Service Provider Interface.

There are three essential components of a service provider framework:
a service interface, which providers implement;
a provider registration API, which the system uses to register implementations, giving clients access to them;
and a service access API, which clients use to obtain an instance of the service.

service provider framework 有三个重要的组件,

service interface, 提供实现
供应者的注册接口,可以用来注册接口实现,这样就可以访问到实现类。
获取 service 的api,可以用来获取 service 的实例。

SPI 的作用

SPI 主要是被框架的开发人员使用,比如 java.sql.Driver 接口,其他不同厂商可以针对同一接口做出不同的实现,mysql 和 postgresql 都有不同的实现提供给用户。Java 的 SPI 机制可以为某个接口注册服务实现。

Java 的 SPI 实现是由 java.util.ServiceLoader 类实现。当服务的提供者提供了一种接口的实现之后,需要在 classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的 META-INF/services/ 中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。

这是一个 service provider framework 大致结构

// Service provider framework sketch


// Service 接口,我们要使用的功能在这里
// Service interface
public interface Service {
    ... // Service-specific methods go here
}

// Provider 用来获取 Service 实例
// Service provider interface
public interface Provider {
    Service newService();
}

// Noninstantiable class for service registration and access
public class Services {
    private Services() { }  // Prevents instantiation (Item 4)

    // 用来维持 Provider 实例的 Map
    // Maps service names to services
    private static final Map<String, Provider> providers =
        new ConcurrentHashMap<String, Provider>();
    public static final String DEFAULT_PROVIDER_NAME = "<def>";

    // 注册 Provider 到 Map
    // Provider registration API
    public static void registerDefaultProvider(Provider p) {
        registerProvider(DEFAULT_PROVIDER_NAME, p);
    }
    public static void registerProvider(String name, Provider p){
        providers.put(name, p);
    }

    // Service 获取接口
    // Service access API
    public static Service newInstance() {
        return newInstance(DEFAULT_PROVIDER_NAME);
    }
    // 生成 Service 实例
    public static Service newInstance(String name) {
        Provider p = providers.get(name);
        if (p == null)
            throw new IllegalArgumentException(
                "No provider registered with name: " + name);
        return p.newService();
    }
}

上面的一段代码复现了上一节中提到的三个重要的组件。理解这段代码,对 SPI 机制原理的理解非常重要。

基于 Interface 的 SPI 实现

至于为什么要基于 Interface 去做 SPI,那是因为读入的类你是不知道他的具体类的,也并不知道他有哪些方法,因为这是运行时加载的,编译期都没法获取。具体见下面伪代码:

// 如果不使用 interface
// 比如现在要导入一个 Student 类
class Student {
    public void say () { println("hello, i'm a student"); }
}

// 平常使用
main () {
    // 如果是 基于 SPI 运行时注册的类,你是不知道他的类名的,就像你并不知道他叫 Student
    // 即使你知道他类名叫 Student,也没有用,因为这里的 Student 类需要 import 进来,而编译时并没有这个类,就没办法 import
    Student stu = new Student();
    stu.say();
}

// 基于 SPI
interface Person {
    public void say();
}
class Student implements Person {
    public void say () { println("hello, i'm a student"); }
}
main () {
    // 动态加载类,使用 cast 就可以得到一个 Person接口的实例,就可以调用 say() 方法了
    // 这里是直接获取 Service,可以看到能得到的 Service 是受限的,所以可以使用一个 Provider 并提供一个 API 来获取 Service,以期更完美的实现。(参看上面的三个重要组件。)
    Person stu = (Person) Class.forName("com.xx.Student").newInstance();
    stu.say();
}

JDK 中的 SPI 实现

JDK 的 SPI 实现是由 java.util.ServiceLoader 类实现。

以下是 ServiceLoader 类的成员变量:

public final class ServiceLoader<S>
    implements Iterable<S>
{   // 可以看到默认的寻找配置的地址是 META-INF/services/
    private static final String PREFIX = "META-INF/services/";
    // 使用 Class.forName 加载到配置文件中的对象,使用 service.cast(newInstance) 强转类型
    // The class or interface representing the service being loaded
    private Class<S> service;
    // 类加载器,如果为null,默认使用 systemClassLoader
    // The class loader used to locate, load, and instantiate providers
    private ClassLoader loader;
    // 存储 provider 的集合,存的是 service 的实例[service.cast(newInstance)]
    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 迭代器,迭代过程中实例化 service并存到 map中
    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;
    ...
}

其实原理也很简单,用目标对象 Interface 作为泛型,这样就能利用 Interface 的全限定名查找 META-INF/services/ 下的文件,然后一行一行读取文件,加载 providers 到 map 中。所以 ServiceLoader 类是加载配置文件中全部的类的实例的,而且是一次性加载完成。

由上可知,整个类的其他部分就是在实现查找文件 => 获取类加载器 => 加载 class 对象 => cast 到指定 Interface 并存入 map,而整个过程都是在迭代器中完成的(iterator.next() 方法)。

哦,对了,重新加载直接调用 reload() 方法就好了,方法实现就是新建一个 LazyIterator,然后重复上面的动作。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 nickchenyx@gmail.com

Title:SPI 机制详解

Count:1.3k

Author:nickChen

Created At:2018-04-17, 16:04:24

Updated At:2023-05-08, 23:27:10

Url:http://nickchenyx.github.io/2018/04/17/spi-essentials-md/

Copyright: 'Attribution-non-commercial-shared in the same way 4.0' Reprint please keep the original link and author.