什么是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
是用来搜寻指定类的自定义迭代器。除此之外咱们还能够看到定义了一个固定的PREFIX
值META-INF/services/
,这个就是SPI
默认的搜寻门路。
在自定义迭代器LazyIterator
中定义了nextService
和hasNextService
,这两个就是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
以及弄清楚了他的底层便捷的逻辑是什么。尽管因为内容较多可能为能把所有细节展现进去,然而整体上大家也可能有一个大抵的理解。如果还有问题,能够在评论区和我互动哦~