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

什么是SPI

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

为什么要使用SPI

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

怎么使用SPI

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

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

java 复制代码
public interface Plugin {
    String getName();

    void execute();
}

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

java 复制代码
public class DBPlugin implements Plugin {
    @Override
    public String getName() {
        return "database";
    }

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

    @Override
    public void execute() {
        System.out.println("execute mq plugin");
    }
}
java 复制代码
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目录中添加以接口全限定名命名的文件。最后在这个文件中添加上述三个实现的全限定名就完成了配置。

rust 复制代码
com.example.springprovider.spi.impl.DBPlugin
com.example.springprovider.spi.impl.MqPlugin
com.example.springprovider.spi.impl.RedisPlugin

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

java 复制代码
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的主要实现。

java 复制代码
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;

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

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

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

java 复制代码
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中

java 复制代码
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工具。

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

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

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

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

找到AutoService关键的核心源码:

java 复制代码
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以及弄清楚了他的底层便捷的逻辑是什么。虽然因为内容较多可能为能把所有细节展示出来,但是整体上大家也能够有一个大致的了解。如果还有问题,可以在评论区和我互动哦~

相关推荐
信徒_2 分钟前
Go 语言中的协程
开发语言·后端·golang
m0_7482365814 分钟前
跟据spring boot版本,查看对应的tomcat,并查看可支持的tomcat的版本范围
spring boot·后端·tomcat
web1511736022320 分钟前
Spring Boot项目中解决跨域问题(四种方式)
spring boot·后端·dubbo
我就是我35235 分钟前
记录一次SpringMVC的406错误
java·后端·springmvc
向哆哆37 分钟前
Java应用程序的跨平台性能优化研究
java·开发语言·性能优化
ekkcole1 小时前
windows使用命令解压jar包,替换里面的文件。并重新打包成jar包,解决Failed to get nested archive for entry
java·windows·jar
handsomestWei2 小时前
java实现多图合成mp4和视频附件下载
java·开发语言·音视频·wutool·图片合成视频·视频附件下载
全栈若城2 小时前
03 Python字符串与基础操作详解
java·开发语言·python
伯牙碎琴2 小时前
二、Spring Framework基础:IoC(控制反转)和DI(依赖注入)
java·spring·log4j
菲力蒲LY2 小时前
输入搜索、分组展示选项、下拉选取,全局跳转页,el-select 实现 —— 后端数据处理代码,抛砖引玉展思路
java·前端·mybatis