SPI机制是JAVA原生提供一套能力,在很多场景下都有使用到,本篇文章重点会通过示例和源码的方式来解释什么是SPI;为什么要使用SPI;如何使用SPI;JAVA是如何实现SPI; SPI思想说开去。
什么是SPI
先看下维基百科的定义:
Service Provider Interface (SPI) is an API intended to be implemented or extended by a third party. It can be used to enable framework extension and replaceable components.
这里面重点描述了两点:
- 由第三方实现或扩展的API
- 可用于框架扩展或者实现可替换组件
由这个描述我们大概了解了SPI的使用场景,一句话描述:用于框架或者库提供一个口子(Java中通常以接口实现)供第三方或者调用方去实现,从而将该实现融合到框架或者库中,最终实现框架可扩展或者组件可替换的目的。
为什么要使用SPI
根据上一节的概念描述,可以看出SPI的基本使用场景:框架扩展和实现可替换组件。
根据定义,SPI只是一种特殊的API。我们细想一下,这个“特殊”是体现再什么地方?理解了这个特殊也就能更明白为什么要使用SPI。
(图片来自于设计原则:小议 SPI 和 API)
上图将SPI和API进行了对比,可以看出SPI和API的特殊主要体现再调用方和提供服务方,对于SPI,由client去实现,由Service去加载客户的实现。API体现的是Service提供服务,client去调用。
所以我们可以得到一个基本的使用场景:当需要暴露一个口子让client去实现,然后服务区执行这个实现的时候可以考虑使用SPI。
如何使用SPI
通过上文的分析可以看出,SPI使用需要有两方:调用方(Service),实现方(Client):
// 框架调用方
package com.fdconsole.SPI.service;
/**
* 定义API接口供第三方实现,再调用方也可以提供一个默认实现
*/
public interface Search {
default void search(String searchStr) {
System.out.println("print from interface Search...");
}
}
// 实现方1(第三方)
package com.fdconsole.SPI.client;
import com.fdconsole.SPI.service.Search;
/**
* 第三方实现调用方定义的API接口
*/
public class GithubSearch implements Search {
@Override
public void search(String searchStr) {
System.out.println("print from class GithubSearch...");
}
}
// 实现方2(第三方)
package com.fdconsole.SPI.client;
import com.fdconsole.SPI.service.Search;
/**
* 第三方实现调用方定义的API接口
*/
public class GitlabSearch implements Search {
@Override
public void search(String searchStr) {
System.out.println("print from class GitlabSearch...");
}
}
上文的三段代码片段提供了SPI需要的实现方和调用方,现在只需要在调用方里通过某种方式能拿到实现方提供的具体实现即可。
因为在调用方我们目前能拿到的信息只有定义的API接口,我们需要拿到具体实现,就需要在实现方想办法告诉调用方该调用哪个,一般我们会通过某种约定来实现这个功能,比如放在某个具体的目录底下,SPI用的也是这种思想。SPI要求放在classpath:/META-INF/services/
底下,并且要求将实现类全限定名写在以调用方接口全限定名命名的文件中。
// com.fdconsole.SPI.service.Search 文件
com.fdconsole.SPI.client.GitlabSearch
com.fdconsole.SPI.client.GithubSearch
上述文件需要位于 classpath:/META-INF/services/
中,现在只需要在调用方去找这个文件类,然后利用反射根据类全限定名加载对应的类即可找到client希望框架加载的具体的实现
// 框架调用方
package com.fdconsole.SPI.service;
import java.util.ServiceLoader;
/**
* 根据API定义去加载具体实现
*/
public class Main {
public static void main(String[] args) {
ServiceLoader<Search> searches = ServiceLoader.load(Search.class);
for (Search search : searches) {
search.search("xxx");
}
}
}
/**
* 输出:
* print from class GitlabSearch...
* print from class GithubSearch...
*/
PS:调用方都是框架或者库,实现方是我们自己
JAVA是如何实现SPI
上文在实现例子的时候,我们应该能猜到JAVA实现SPI的核心逻辑,这部分实现主要在于如何让调用方知道确切的实现方,也就是这段代码
// 到指定的目录文件(META-INF/services/)加载具体的实现类
ServiceLoader<Search> searches = ServiceLoader.load(Search.class);
// 遍历实现类,执行逻辑
for (Search search : searches) {
search.search("xxx");
}
在开始分析源码之前,我们先想下可能的疑问,带着问题去看源码会更容易理解:
- 目录是如何指定的? => PREFIX 默认约定
- 文件名为什么要是接口名?=> 通过传入的class直接获取的全限定名,唯一
- ServiceLoader是怎么去找对应的文件的?=> 通过
ClassLoader.getResources
方式到对应的classpath中查找配置文件 - 通过这种配置文件的方式,可以再文件中写任意的方法吗?=> 不可以,在遍历的时候会通过
service.isAssignableFrom(c)
进行判断保证必须是API接口的子类
ServiceLoader源码如下,再其中我加了核心代码的中文注释,看完之后应该很容易解答上面的问题:
// 实现了 Iterable, 说明该类是可迭代的,通过源码可以看到 load返回的就是 ServiceLoader实例供遍历执行的
public final class ServiceLoader<S> implements Iterable<S>
{
// 指定了目前前缀,所以必须在这个目录下
private static final String PREFIX = "META-INF/services/";
// 定义的API接口的class
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// 传入的API接口的class => Service.class
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 加载配置文件用的classloader, 默认是当前线程的类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
// 解析出来的实现类的全限定名
//
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
// ...
}
// 返回配置文件中的实现类全限定名的迭代器
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError
{
// ...
}
// 实现一种懒加载的方式,实现调用方当遍历到对应的实现的时候才加载的能力
//
//
private class LazyIterator implements Iterator<S>
{
Class<S> service; // 传入接口的class实例
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (configs == null) {
// 在这里拼接约定的 前缀(META-INF/services/)+类名(com.fdconsole.SPI.service.Search)
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 通过类加载器的获取资源的方式到对应的classpath中查找配置文件
configs = loader.getResources(fullName);
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
String cn = nextName;
nextName = null;
Class<?> c = null;
// 根据全限定名加载实现类
c = Class.forName(cn, false, loader);
// 保证配置文件中的实现是按照API接口定义的,否则抛异常
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
// 将实现类转换成父API接口
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
}
public boolean hasNext() {
return hasNextService();
}
public S next() {
return nextService();
}
}
// 可迭代类的迭代方法,通过foreach来遍历调用next方式获取实例,然后桥接到懒迭代器上
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
return lookupIterator.hasNext();
}
public S next() {
return lookupIterator.next();
}
};
}
/**
* Creates a new service loader for the given service type and class
* loader.
*
* @param <S> the class of the service type
*
* @param service
* The interface or abstract class representing the service
* 调用方提供的API接口供第三方实现
*
* @param loader
* The class loader to be used to load provider-configuration files
* and provider classes, or <tt>null</tt> if the system class
* loader (or, failing that, the bootstrap class loader) is to be
* used
*/
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// 返回一个可迭代的loader对象
return new ServiceLoader<>(service, loader);
}
/**
* Creates a new service loader for the given service type, using the
* current thread's {@linkplain java.lang.Thread#getContextClassLoader
* context class loader}.
*
* @param service
* The interface or abstract class representing the service
* 调用方提供的API接口供第三方实现
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
}
通过源码分析发现SPI整体的实现思路还是很简明清晰的,也算是对现在流行的约定思想的一种实现,大家约定好实现类放置的位置,如何放即可。
SPI思想说开去
再看回维基百科的定义,SPI整体还是很清晰的,而它更体现一种普遍的设计理念:就是通过暴露口子给第三方实现,然第三方给我们某个功能以可扩展(替换)的思想增加功能。
下面我们看一些常见的这种思想的运用:
插件系统
常用的IDE,比如IJ、eclipse、vscode等等,都提供了强大的插件机制,让第三方可以基于他们提供的API接口来开发自定义插件,然后放到特定的地方,然后IDE再某个时机去找(调用)这些实现
框架提供的各种定制点
任何框架或者库都会提供很多定制点或者Hook供开发者去定制,比如Spring框架的自定义属性编辑器,自定义标签等等,CSE对负载均衡的定制,可以供定义选择server从而让用户定制如何从注册中心选取server instance
…
总结
在上文通过概念、实例再到源码分析了SPI的实现机制和SPI的思想,最终可以看到SPI也是我们很常见的一种思想的实现