浅聊Java生态中的SPI技术

71 阅读4分钟

一、前言

Service Provider Interface 简称SPI,是一种服务发现机制。

本篇博客中,笔者将讲述Java中的SPI机制和Spring框架中的SPI机制、练手demo和其在源码中的应用、优缺点。

二、Java SPI

2.1 原理

Java SPI 是通过Java的类加载机制和反射机制来实现的,具体来说是通过java.util包下的ServiceLoader类中的静态方法<S> ServiceLoader<S> load(Class<S> service)来加载服务,核心逻辑如下:

(1)检查类路径META-INF/services目录下是否存在以接口全限定名命名的文件。

(2)若存在,使用流的方式读取并解析文件内容,获取实现该接口的类的全限定名,并通过Class.forName(cn, false, loader) 方法加载对应的类。

(3)在加载完类之后,会调用service.cast(c.newInstance())方法,通过反射机制创建对应类的实例,并将其缓存到名为LinkedHashMap<String,S> providers的map中。

【注意】当调用<S> ServiceLoader<S> load(Class<S> service)时,并不会立刻将所有实现了该接口的类都一次性加载进来,而是返回一个名为lookupIterator的懒迭代器,只有遍历迭代器时,才会按需加载对应的类及其实例。

2.1.1 源码解析

首先我们宏观了解下ServiceLoad类的结构,其实现了迭代器接口,声明了一些成员变量。

public final class ServiceLoader<S> implements Iterable<S> {

    // Java SPI 规定的接口存储位置,会从这个目录下读取内容
    private static final String PREFIX = "META-INF/services/";

    // The class or interface representing the service being loaded
    // 要被加载的服务类或接口
    private final Class<S> service;

    // The class loader used to locate, load, and instantiate providers
    // 类加载器
    private final ClassLoader loader;

    // The access control context taken when the ServiceLoader is created
    // 上下文访问控制器
    private final AccessControlContext acc;

    // Cached providers, in instantiation order
    // 缓存加载后的服务
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    // 懒加载器
    private LazyIterator lookupIterator;
}

接着,调用load()方法,会实列化ServiceLoad和该类的成员变量,最重要的是其调用reload()方法实例化了一个懒迭代器lookupIterator。

public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader)
{
    // 实例化
    return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    // 要加载的接口,判断不能为空
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // 获取类加载器
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    // 获取访问控制器
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    // 获取懒加载器,并清空服务缓存map
    reload();
}


public void reload() {
    providers.clear();
    // 实例化`懒`迭代器
    lookupIterator = new LazyIterator(service, loader);
}

load()方法执行完毕后,并没有加载服务。因为Java SPI实现了懒加载机制,当我们遍历load()方法返回后服务对象列表后,才会加载服务。【注意:因为ServiceLoader类实现了Iterable接口,当遍历时,首先会调用hasNext()方法,最终会调到hasNextService()方法,hasNextService()方法主要获取实现类的全限定名,nextService()方法通过类加载机制和反射机制来实例化获取到的实现类。】

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // fullName = /META-INF/services/全限定名
            String fullName = PREFIX + service.getName();
            // 转化为URL对象
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        // 读取并解析URL对象
        pending = parse(service, configs.nextElement());
    }
    // 拿到第一个实现类的全限定名
    nextName = pending.next();
    return true;
}

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        // 通过类加载器获取类的Class对象
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
        // 通过反射机制获取实例化实现类, 并放入map集合,返回实例化的对象
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

至此,Java SPI的核心源码已经解析完毕,接下来,笔者将举个例子使读者更直观的理解SPI

2.2 demo

首先demo的项目结构如下,共有5个module。

image.png

hello-spi-interface模块定义了一个接口SaveData,方法为save(String)。

image.png hello-spi-mysql模块实现了该方法,并在META/INF的service目录下创建了名为org.hello.interface.SavaData的文件,文件内容为SaveData接口的实现类的全限定名:org.hello.mysql.MysqlSave。hello-spi-redis和hello-spi-pgsl实现原理同hello-spi-mysql一样。 image.png hello-spi-app模块通过pom.xml文件来按需引入。例如,这里我们引入了mysql和pgsql模块,因此当服app服务启动时,其运行了mysql和pgsl的保存数据的执行逻辑。

image.png

image.png

2.3 源码中的应用

2.4 优缺点

三、Spring SPI

2.1 原理

2.2 demo

2.3 源码中的应用

2.4 优缺点

四、扩展

SPI在Dubbo框架中的应用```