SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制(为某个接口寻找服务实现的机制),可以用来启用框架扩展和替换组件,其核心思想就是 解耦。
模块之间基于接口编程,模块之间不对实现类进行硬编码,当代码里涉及具体的实现类,就违反了可拔插的原则,为了实现在模块装配的时候能不在程序里动态指明,就需要spi了。
这里我们要跟API区分开来,简单介绍一下API
API(Application Programming Interface)是一种应用程序编程接口,它定义了一组用于与特定软件组件或服务进行交互的函数、方法和数据结构。
其目的主要用于提供一种与特定软件组件或服务进行交互的抽象层。
比如我们常见的系统API,接入的各种三方API,这些API的特点是实现方式以及做好了,开发者调用这些API来做一些有预期的事情。
举个简单的例子,例如芯片公司定义了一个规范,需要第三方厂商去实现,那么对于芯片公司方来说,只需要集成对应厂商的插件,就可以完成对应规范的实现机制。 形成一种插拔式的扩展手段。
如JDBC、日志框架等都用到。
Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要(通常由服务提供者(如库或框架的开发者)实现,以提供特定功能的多种实现),我们看个图
将接口的实现类放在配置文件中,在程序运行过程中读取配置文件,通过反射加载实现类。
具体流程:
–> 读取META-INF/services/下的配置文件
–> 获得所有能被实例化的类的名称
–> 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化
–> 把实例化后的类缓存到providers对象中,(LinkedHashMap
JDK中查找服务的实现的工具类是:java.util.ServiceLoader。JDK标准的SPI会一次性加载实例化扩展点的所有实现。
我们举例,先看下完整的项目目录
我们现在需要进行股票交易,有多个券商可用。
1、先定义好接口,新建module-spi,
package com.test;
public interface ITrade {
void trade();
}
2、两个接口的实现
新建module-effecta module-effectb,表示不同的实现方。
这两个module分别要引用 接口的module
<dependencies>
<dependency>
<groupId>org.examplegroupId>
<artifactId>module-spiartifactId>
<version>1.0-SNAPSHOTversion>
<scope>compilescope>
dependency>
dependencies>
添加实现
package com.effectA;
import com.test.ITrade;
public class TradeEffectA implements ITrade {
@Override
public void trade() {
System.out.println("券商 A");
}
}
最后进行注册
java目录下 增加 resources/META-INF/services/ 目录,在该目录下创建文件 ,如下图所示:
module-effectb重复操作
再写一个测试方法,新建一个module-main,充当调用者,首先添加引用
<dependencies>
<dependency>
<groupId>org.examplegroupId>
<artifactId>module-spiartifactId>
<version>1.0-SNAPSHOTversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.examplegroupId>
<artifactId>module-effectAartifactId>
<version>1.0-SNAPSHOTversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.examplegroupId>
<artifactId>module-effectBartifactId>
<version>1.0-SNAPSHOTversion>
<scope>compilescope>
dependency>
dependencies>
package com.test;
import java.util.ServiceLoader;
public class Test {
public static void main(String[] args) {
ServiceLoader<ITrade> loader = ServiceLoader.load(ITrade.class);
for (ITrade itrade: loader) {
itrade.trade();
}
}
}
那为什么配置文件为什么要放在META-INF/services下面?
可以打开ServiceLoader的代码,里面定义了文件的PREFIX如下:
private static final String PREFIX = “META-INF/services/”
我们看下源码
public final class ServiceLoader<S> implements Iterable<S>{
//路径前缀(就是我们放置配置文件的目录)
private static final String PREFIX = "META-INF/services/";
// 代表被加载的类或者接口
private final Class<S> service;
// 用于定位,加载和实例化providers的类加载器
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文
private final AccessControlContext acc;
// 缓存providers,按实例化的顺序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒查找迭代器
private LazyIterator lookupIterator;
......
}
在这个例子中,每次都要手动去新建META-INF/services/的文件,是不是很麻烦,我们可以用Autoservice来简化代码,先上demo
新建module-effectc-autoservice,表示不同使用autoservice自动写入配置的的实现方。
引用 接口的module-spi 及 autoservice
<dependencies>
<dependency>
<groupId>org.examplegroupId>
<artifactId>module-spiartifactId>
<version>1.0-SNAPSHOTversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>com.google.auto.servicegroupId>
<artifactId>auto-serviceartifactId>
<version>1.0-rc4version>
<scope>compilescope>
dependency>
dependencies>
添加实现
@AutoService(ITrade.class)
@SupportedAnnotationTypes({"com.test.ITrade"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class TredeEffectC implements ITrade {
@Override
public void trade() {
System.out.println("券商 C");
}
}
到这里就结束了,是不是要简化了很多。这个机制同样适用Android,如安卓组件化,demo比较简单,就不贴代码了。
AutoService是Google开发一个自动生成SPI清单文件的框架。
自动往 resources/META-INF/services/ 写入文件。
https://github.com/google/auto
不用它也行,如果不使用它就需要手动去创建这个文件、手动往这个文件里添加服务(接口实现)。
其原理步骤:
我们看下autoservice的注解处理
private void processAnnotations(
Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//获取所有加了AutoService注解的类
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);
for (Element e : elements) {
//将Element转成TypeElement
TypeElement providerImplementer = MoreElements.asType(e);
//获取AutoServce注解指定的value
AnnotationMirror annotationMirror = getAnnotationMirror(e, AutoService.class).get();
//获取value集合
Set<DeclaredType> providerInterfaces = getValueFieldOfClasses(annotationMirror);
//如果没有指定value,报错
if (providerInterfaces.isEmpty()) {
error(MISSING_SERVICES_ERROR, e, annotationMirror);
continue;
}
//遍历所有的value,获取value的完整类名(例如javax.annotation.processing.Processor)
for (DeclaredType providerInterface : providerInterfaces) {
TypeElement providerType = MoreTypes.asTypeElement(providerInterface);
//判断是否是继承关系,是则放入providers缓存起来,否则报错
if (checkImplementer(providerImplementer, providerType, annotationMirror)) {
providers.put(getBinaryName(providerType), getBinaryName(providerImplementer));
} else {
//报错代码,略
}
}
}
}
注解处理完毕,就会生成SPI注册文件。如果SPI路径上文件已经存在,先要把已存在的SPI清单读进内存,再把新的provider加进去,然后全部写出,覆盖原来的文件。这部分逻辑如下:
private void generateConfigFiles() {
//获取文件工具类,processingEnv是AbstractProcessor的成员变量,直接拿来用。
Filer filer = processingEnv.getFiler();
//遍历之前解析的providers缓存
for (String providerInterface : providers.keySet()) {
//providerInterface就是value字段指定的接口,例如javax.annotation.processing.Processor
String resourceFile = "META-INF/services/" + providerInterface;
log("Working on resource file: " + resourceFile);
try {
SortedSet<String> allServices = Sets.newTreeSet();
try {
//已经存在的SPI文件
FileObject existingFile =
filer.getResource(StandardLocation.CLASS_OUTPUT, "", resourceFile);
//SPI文件中的service条目清单
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.");
}
//新的service条目清单
Set<String> newServices = new HashSet<>(providers.get(providerInterface));
//如果已经存在,则不处理
if (!allServices.addAll(newServices)) {
log("No new service entries being added.");
continue;
}
//以下是将缓存的services写入文件中。
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加到java项目中,其实就是引入了AutoServiceProcessor这个注解处理器,帮助我们处理@AutoService注解,将我们的服务(一般是APT类,也可以是其它的类,通过value指定)自动注册进SPI文件中。
javapoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件 这个框架功能非常实用。