写点什么

Java Spi 是如何找到你的实现的? ——Java SPI 原理与实践

作者:骑牛上青山
  • 2024-02-18
    上海
  • 本文字数:3845 字

    阅读完需:约 13 分钟

什么是 SPI

SPI的全称是Service Provider Interface,顾名思义即服务提供者接口,相比API Application Programming Interface他们的不同之处在于API是应用提供给外部的功能,而SPI则更倾向于是规定好规范,具体实现由使用方自行实现。

为什么要使用 SPI

SPI提供方提供接口定义,使用方负责实现,这种方式更有利于解藕代码。在有统一标准,但是不确定使用场景的场合非常适用。

怎么使用 SPI

接下来我会用一个简单的例子来介绍如何使用SPI


首先我们在二方包中定义一个接口Plugin


public interface Plugin {    String getName();
void execute();}
复制代码


然后将二方包编译打包后在自己的应用项目中引入,之后实现二方包中的接口Plugin,下面我写了三个不同的实现:


public class DBPlugin implements Plugin {    @Override    public String getName() {        return "database";    }
@Override public void execute() { System.out.println("execute database plugin"); }}
复制代码


public class MqPlugin implements Plugin {    @Override    public String getName() {        return "mq";    }
@Override public void execute() { System.out.println("execute mq plugin"); }}
复制代码


public class RedisPlugin implements Plugin {    @Override    public String getName() {        return "redis";    }
@Override public void execute() { System.out.println("execute redis plugin"); }}
复制代码


之后在resources目录下的META-INF.services目录中添加以接口全限定名命名的文件。最后在这个文件中添加上述三个实现的全限定名就完成了配置。



com.example.springprovider.spi.impl.DBPlugincom.example.springprovider.spi.impl.MqPlugincom.example.springprovider.spi.impl.RedisPlugin
复制代码


然后我们编写一段代码来看下我们的几个SPI的实现是否已经装载成功了。


public void spiTest() {    ServiceLoader<Plugin> serviceLoader = ServiceLoader.load(Plugin.class);
for (Plugin plugin : serviceLoader) { System.out.println(plugin.getName()); plugin.execute(); }}
复制代码


运行代码,结果已经正常输出,上述配置成功!



SPI 的原理

上述的例子是成功的运行起来了,但是大家应该还是会有问题,为什么这么配置就可以运行了?文件名或者路径一定就需要按照上述的规定来配置吗?


要了解这些问题,我们就需要从源码的角度来深入的看一下。


此处使用 JDK8 的源码来进行讲解,JDK9 之后引入了 module 机制导致这部分代码为了兼容 module 也进行了大改,变得更为复杂不利于理解,因此如果有兴趣可以自行了解


要了解SPI的实现,最主要的就是ServiceLoader,这个类是SPI的主要实现。


private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loadedprivate final Class<S> service;
// The class loader used to locate, load, and instantiate providersprivate final ClassLoader loader;
// The access control context taken when the ServiceLoader is createdprivate final AccessControlContext acc;
// Cached providers, in instantiation orderprivate LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iteratorprivate LazyIterator lookupIterator;
复制代码


ServiceLoader定义了一系列成员变量,其中最重要的两个,providers是一个缓存搜索结果的 map,lookupIterator是用来搜索指定类的自定义迭代器。除此之外我们还可以看到定义了一个固定的PREFIXMETA-INF/services/,这个就是SPI默认的搜索路径。


在自定义迭代器LazyIterator中定义了nextServicehasNextService,这两个就是SPI搜索实现类的核心方法。


hasNextService逻辑很简单,主要是读取META-INF/services/接口文件中定义的实现类文件,然后将这个文件进行解析以求找到相应的实现类并加载


private boolean hasNextService() {    if (nextName != null) {        return true;    }    if (configs == null) {        try {            String fullName = PREFIX + service.getName();            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();    return true;}
复制代码


nextService主要是装载类,然后经过判断后放置入缓存的 map 中


private S nextService() {    if (!hasNextService())        throw new NoSuchElementException();    String cn = nextName;    nextName = null;    Class<?> c = null;    try {        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 {        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}
复制代码


接下来在parse函数中调用parseLine,在parseLine中解析最终的实现类并返回。至此完整的解析逻辑我们都已经清晰的看到了,回过头再来看开始的问题应该也都能够引刃而解了!

AutoService

很多人会觉得SPI的使用上会有一些麻烦,需要创建目录并且配置相关的文件,后续SPI产生变动还需要额外维护这个文件会很头疼。那么我在这里介绍一个SPI的便捷工具,由 Google 推出的AutoService工具。


使用方法很简单,在代码中引入依赖:


<dependency>    <groupId>com.google.auto.service</groupId>    <artifactId>auto-service</artifactId>    <version>1.0.1</version></dependency>
复制代码


之后直接在实现类上添加注解@AutoService(MyProvider.class)MyProvider配置为接口类即可。


那么这里就又有问题了,为什么AutoService一个注解就能够实现了而不用像 JDK 标准那样生成文件呢?想知道答案的话我们就又又又需要来看源码了。


找到AutoService关键的核心源码:


private void generateConfigFiles() {    Filer filer = processingEnv.getFiler();
for (String providerInterface : providers.keySet()) { String resourceFile = "META-INF/services/" + providerInterface; log("Working on resource file: " + resourceFile); try { SortedSet<String> allServices = Sets.newTreeSet(); try { FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "", resourceFile); log("Looking for existing resource file at " + existingFile.toUri()); Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream()); log("Existing service entries: " + oldServices); allServices.addAll(oldServices); } catch (IOException e) { log("Resource file did not already exist."); }
Set<String> newServices = new HashSet<>(providers.get(providerInterface)); if (!allServices.addAll(newServices)) { log("No new service entries being added."); continue; }
log("New service file contents: " + allServices); FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", resourceFile); try (OutputStream out = fileObject.openOutputStream()) { ServicesFiles.writeServiceFile(allServices, out); } log("Wrote to: " + fileObject.toUri()); } catch (IOException e) { fatalError("Unable to create " + resourceFile + ", " + e); return; } } }
复制代码


我们可以发现AutoService的核心思路其实很简单,就是通过注解的形式简化你的配置,然后将对应的文件夹以及文件内容由AutoService代码来自动生成。如此的话就不会有兼容性问题和后续的版本迭代的问题。

总结

SPI是一种便捷的可扩展方式,在实际的开源项目中也被广泛运用,在本文中我们深入源码了解了SPI的原理,弄清楚了SPI使用过程中的一些为什么。除此之外也找到了更加便捷的工具AutoService以及弄清楚了他的底层便捷的逻辑是什么。虽然因为内容较多可能为能把所有细节展示出来,但是整体上大家也能够有一个大致的了解。如果还有问题,可以在评论区和我互动哦~

发布于: 刚刚阅读数: 3
用户头像

还未添加个人签名 2021-05-18 加入

还未添加个人简介

评论

发布
暂无评论
Java Spi是如何找到你的实现的? ——Java SPI原理与实践_Java_骑牛上青山_InfoQ写作社区