炸毛的猫炸毛的猫
归档
java
框架
中间件
数据库
杂谈
荧墨
归档
java
框架
中间件
数据库
杂谈
荧墨
  • 探究JavaSPI

探究 Java SPI 机制

阅读 vivo 互联网技术 《源码级深度理解 Java SPI》后记录

什么是 SPI

简介

SPI 全称 Service Provider Interface,是 Java 提供的,旨在由第三方实现或扩展的API,是一种动态加载服务的机制。

从字面意义上也能看出,服务提供者的接口,也就是服务提供者将依据此接口进行服务实现。

SPI 组成

Java SPI 四要素:

  • SPI 接口:为服务提供者实现类约束的接口或抽象类。
  • SPI 实现类:实际提供服务的实现类。
  • SPI 配置:Java SPI 机制约定的配置文件,提供查找服务实现类的逻辑。配置文件必须置于 META-INF/services 目录中,文件名应与服务提供者接口的完全限定名保持一致。文件中每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。
  • ServiceLoader:Java SPI 的核心类,用于加载 SPI 实现类。ServiceLoader 中有各种实用方法来获取特定实现、迭代它们或重新加载服务。

简单使用 SPI

https://github.com/yancy0109/code-examples/tree/main/spi

定义 SPI

package com.yancy.spi;

/**
 * SPI 接口
 * @author yancy0109
 */
public interface DataStorage {

    String search(String key);

}

提供实现类

package com.yancy.spi.Impl;

import com.yancy.spi.DataStorage;

/**
 * @author yancy0109
 * @date: 2024/2/6
 */
public class MysqlStorage implements DataStorage {
    @Override
    public String search(String key) {
        return "[Mysql] [Search] " + key + ", Result: No";
    }

}
package com.yancy.spi.Impl;

import com.yancy.spi.DataStorage;

/**
 * @author yancy0109
 * @date: 2024/2/6
 */
public class RedisStorage implements DataStorage {
    @Override
    public String search(String key) {
        return "[Redis] [Search] " + key + ", Result: No";
    }
    
}

声明实现类

META-INF/services/com.yancy.spi.DataStorage

com.yancy.spi.Impl.MysqlStorage
com.yancy.spi.Impl.RedisStorage

调用

通过 ServiceLoader 加载对应类的实现类,调用接口方法

public class SpiDemo {

    public static void main(String[] args) {
        ServiceLoader<DataStorage> serviceLoader = ServiceLoader.load(DataStorage.class);
        serviceLoader.forEach(dataStorage -> System.out.println(dataStorage.search("SPI_Test, Yes or NO?")));
    }
}

SPI 原理 - ServiceLoader

通过 ServiceLoader 即可解析获取实现服务对象,那么它是如何工作的呢?接下来继续走进 ServiceLoader 的实现。

ServiceLoader 成员变量

首先来看一看 ServiceLoader 会保存什么数据

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

    // 我们存放SPI实现类信息的目录 
    private static final String PREFIX = "META-INF/services/";
    
    // The class or interface representing the service being loaded
    // 表示要被加载的SPI服务的类或接口
    private final Class<S> service;

    // The class loader used to locate, load, and instantiate providers
    // 用于加载 SPI 服务的类加载器
    private final ClassLoader loader;

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

    // Cached providers, in instantiation order
    // SPI服务缓存,按照实例化顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    // 懒查询迭代器
    private LazyIterator lookupIterator;
    
    // .......
}

ServiceLoader 工作流程

Load 方法

通过此静态方法,对指定 Class 进行 SPI 服务加载初始化。

  1. 指定类加载 ClassLoader 和 访问控制上下文
  2. 重新加载 SPI 服务
    • 清空缓存中所有已实例化的 SPI 服务
    • 根据 ClassLoader 和 SPI 类型,创建懒加载迭代器

源码如下:

public final class ServiceLoader<S> implements Iterable<S>
{    
    // .......
    /**
    * service: SPI
    * loader:用于加载 SPI 服务的类加载器
    */
    public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
        return new ServiceLoader<>(service, loader);
    }    
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();  // 省略cl,则使用当前Thread#ClassLoader
        return ServiceLoader.load(service, cl);
    }
    
    public void reload() {
        providers.clear();	// 清除缓存中所有已实例化的SPI服务
        lookupIterator = new LazyIterator(service, loader);	// 根据 ClassLoader 与 SPI类型,创建懒加载迭代器
    }

    // 私有构造方法
    // 构建 ServiceLoader
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null"); // 检查 SPI Non Null
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;	// 指定类加载器 
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;  // AccessControlContext初始化
        reload();	// 重加载SPI服务
    }
    // ......
}

迭代方法

可以看到 ServiceLoader 实现了可迭代接口,他提供了一个方法获取迭代器进行对 SPI实现类 迭代。

public final class ServiceLoader<S> implements Iterable<S> {
    // .......
    
    public Iterator<S> iterator() {
    return new Iterator<S>() {

            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator(); // 从缓存中获取provider(LinkedList)迭代器,这个迭代器会在创建时被初始化,元素不会再被该变

            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();  // lookupIterator 即ServiceLoader内部LazyIterator
            }

            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }
}

可以看到,优先从 providers 获取迭代器,如果没有则通过懒加载器遍历。

从这里我们也可以猜到了,懒加载器将通过迭代实例化SPI服务实现类,并将其存储至 providers。

LazyIterator 工作流程

hasNextService
private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            String fullName = PREFIX + service.getName();	// 拼接全路径名
            // 获取SPI配置文件
            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;
        }
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();	// 标记 nextName,SPI下一个实现类
    return true;
}
ClassLoader.getSystemResources

针对上方代码,我还进行了额外的测试

public class getResourceDemo {
    public static void main(String[] args) throws IOException {
        Enumeration<URL> systemResources = ClassLoader.getSystemResources("META-INF/services/com.yancy.spi.DataStorage");
        while (systemResources.hasMoreElements()) {
            URL url = systemResources.nextElement();
            System.out.println(url.getFile());
        }
    }
}

输出如下:

image-20240207142614714

他把所有目标文件文资源全部进行了加载,我们可以通过遍历获取所有目标 SPI 配置文件,也就可以依据于此获取目标 SPI 实现类。

接下来则通过 parse 方法,来对 SPI实现类配置 进行逐行读取。

private Iterator<String> parse(Class<?> service, URL u)
    throws ServiceConfigurationError
{
    InputStream in = null;
    BufferedReader r = null;
    ArrayList<String> names = new ArrayList<>();
    try {
        in = u.openStream();
        r = new BufferedReader(new InputStreamReader(in, "utf-8"));
        int lc = 1;
        while ((lc = parseLine(service, u, r, lc, names)) >= 0);	// 从文件中读取行处理 直至读取结束
    } catch (IOException x) {
        fail(service, "Error reading configuration file", x);
    } finally {
        try {
            if (r != null) r.close();
            if (in != null) in.close();
        } catch (IOException y) {
            fail(service, "Error closing configuration file", y);
        }
    }
    return names.iterator();	// 返回当前资源已读取类名迭代器
}

// Parse a single line from the given configuration file, adding the name
// on the line to the names list.
//
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                      List<String> names)
    throws IOException, ServiceConfigurationError
{
    String ln = r.readLine();	// 读取一行
    if (ln == null) {
        return -1;
    }
    int ci = ln.indexOf('#');	// 获取 #
    if (ci >= 0) ln = ln.substring(0, ci); // c>=0 获取非注释内容
    ln = ln.trim();	// 移除空格
    int n = ln.length();	// 获取最终长度
    if (n != 0) {
        if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
            fail(service, u, lc, "Illegal configuration-file syntax");
        int cp = ln.codePointAt(0);
        if (!Character.isJavaIdentifierStart(cp))
            fail(service, u, lc, "Illegal provider-class name: " + ln);
        for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
            cp = ln.codePointAt(i);
            if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
        }
        if (!providers.containsKey(ln) && !names.contains(ln))	// 排除重复实现类
            names.add(ln);	// 将目标SPI实现类加入List
    }
    return lc + 1;
}
nextService
private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);	// 加载目标SPI实现类
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
        S p = service.cast(c.newInstance());	// 强转SPI
        providers.put(cn, p);	// providers 存放当前实例缓存
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

当一轮循环结束后,下次遍历,重新获取 Iterator,就能直接从缓存中获取已实例化的实现对象了。

SPI与类加载器

ServiceLoader 在加载服务时,可以指定传入类加载器 ClassLoader 来用于加载 SPI Config 指定类,为什么加载 SPI 服务时,要指定类加载器 ClassLoader 呢?ServiceLoader 默认采用 AppClassLoader。

双亲委派模型

概念

先来了解一个双亲委派模型:一个类加载器首先将类加载器请求传送到父类加载器,只有当父类加载器无法完成类加载请求时,才尝试加载。

JVM 三个重要的 ClassLoader:

  • BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++ 实现,通常表示为 null,没有父级,主要用于加载 JDK 内部(%JAVA_HOME%/lib)的核心类库。
  • ExtensionClassLoader(扩展类加载器):主要负责加载 %JAVA_HOME%/lib/ext 目录下的 Jar 包和类 以及被 java.ext.dirs 系统变量所指定路径下的所有类。
  • AppClasLoader(应用程序类加载器):面向用户的加载器,负责加载当前 classpath 下所有的 Jar包和类。

image-20240207164743339

好处

使得 Java类 伴随着它的类加载器,天然具备一种优先级的层次关系,从而使得类加载得到统一,不会出现重复加载问题。

  • 防止系统类内存中出现同样的字节码
  • 保证 Java 程序安全稳定运行
  • 防止恶意代码通过自定义的类加载器去替换 Java 核心类库中的关键类。

为什么需要指定类加载器

子类加载器可以使用父类加载器已经加载的类,而父类加载器无法使用子类加载器已加载的类。

  • SPI 接口若为 Java核心库 的一部分,将交由 BootstrapClassLoader 加载。
  • SPI 实现类则一般由 AppClassLoader 来加载。BootstrapClassLoader 无法找到其实现类,因为只加载了 Java核心库。所以,ServiceLoader 必须要指定一个 ClassLoader 才能读取到目标服务实现类。

Java 应用的线程的上下文类加载器就是 AppClassLoader。在核心类库使用 SPI 接口时,传递的类加载器使用线程上下文类加载器,就可以成功加载 SPI 实现类。线程上下文类加载器在很多 SPI 实现中都会用到。

Java SPI 的不足

  • 不能按需加载,需要遍历所有的实现,并进行实例化,才能在循环中找到我们需要的实现。

  • 获取实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。

  • 多个并发多线程使用 ServiceLoader 类的实例是不安全的。

Last Updated:
Contributors: yancy0109