乐趣区

关于后端:Java-Spi是如何找到你的实现的-Java-SPI原理与实践

什么是 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.DBPlugin
com.example.springprovider.spi.impl.MqPlugin
com.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 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/ 接口文件中定义的实现类文件,而后将这个文件进行解析以求找到相应的实现类并加载

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 以及弄清楚了他的底层便捷的逻辑是什么。尽管因为内容较多可能为能把所有细节展现进去,然而整体上大家也可能有一个大抵的理解。如果还有问题,能够在评论区和我互动哦~

退出移动版