SPI介绍
在面向对象的设计中,一般推荐模块之间基于接口编程,通常情况下调用方模块是不会感知到被调用方模块的内部实现。一旦代码里面涉及具体实现类,就违反了开闭原则。为了实现在模块装配的时候不用在程序里面动态指明,这就需要一种服务发现机制。Java SPI 就是提供了这样的一个机制:为某个接口寻找服务实现的机制。
SPI 全称 Service Provider Interface, 字面意思就是:服务提供者的接口。SPI 将服务接口和具体的服务实现分离开来,将服务调用和服务实现者解耦,能够提升程序的拓展性,可维护行。修改或替换服务实现并不需要修改调用方。
截屏2024-03-05 21.14.36
SPI 和API 的区别
从广义上来说,他们都是属于接口,而且非常容易混淆。
img
一般模块之间都是通过接口进行通信,在服务提供方和服务实现方之间引入一个接口。
当实现方提供了接口和实现,我们就可以调用实现方的接口从而拥有实现的能力,这种就是API, 接口和实现 都在实现方。
当接口在调用方这边时,就是SPI 了,调用方提供一个接口,然后交给不同的实现方去实现,从而进行提供服务。
基础使用
我们接下来用一个非常简单的实例来介绍一下SPI 的使用。
服务声明
截屏2024-03-05 21.28.20
项目的模块大概是这个结构,有一个基础 base module ,两个 业务组件 featureA 和 featureB 都依赖着base , app 壳工程引用着两个业务module。
首先我们在base中创建两个接口:
csharp
public interface IFeatureAInterface {
/*调用A组件*/
void getA();
/*调用B组件*/
void getB();
}
public interface IFeatureBInterface {
/*调用B组件*/
void getB();
/*调用A组件*/
void getA();
}
然后由两个业务组件分别对对应的接口去进行实现。
typescript
A组件实现
public class FeatureAImpl implements IFeatureAInterface {
@Override
public void getA() {
Log.e("wangyilei", "调用了A组件的A方法");
}
@Override
public void getB() {
}
}
B组件实现
public class FeatureBImpl implements IFeatureBInterface {
@Override
public void getB() {
Log.e("wangyilei", "调用了B组件的B方法");
}
@Override
public void getA() {
}
}
然后在对应业务对接口实现进行声明,创建文件结构如下:
csharp
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── com
│ │ │ └── xl
│ │ │ └── featurea
│ │ │ └── FeatureAImpl.java
│ │ └── resources
│ │ └── META-INF
│ │ └── services
│ │ └── com.xl.base.IFeatureAInterface
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── com
│ │ │ └── xl
│ │ │ └── featureb
│ │ │ └── FeatureBImpl.java
│ │ └── resources
│ │ └── META-INF
│ │ └── services
│ │ └── com.xl.base.IFeatureBInterface
在main下,创建与java 同级的 resources 目录,在然后再创建MATE-INF ,services 目录,然后在services目录中创建声明文件,文件名称以接口的全路径命令,文件内容为接口实现的全路径名。
csharp
com.xl.base.IFeatureAInterface : com.xl.featurea.FeatureAImpl
com.xl.base.IFeatureBInterface : com.xl.featureb.FeatureBImpl
这样我们的接口实现就声明好了,接下来我们就需要借助 ServiceLoader 来 获取这个服务进行调用。
服务调用
想要使用java 的SPI 机制,是需要依赖ServiceLoader来实现的,具体使用如下:
vbnet
//App壳工程
ServiceLoader<IFeatureAInterface> load = ServiceLoader.load(IFeatureAInterface.class);
Iterator<IFeatureAInterface> iterator = load.iterator();
while (iterator.hasNext()){
iterator.next().getA();
}
调用方式非常简单,这样我们就可以在App的壳工程中调用到featureA模块的方法。
配合AutoService使用
如果我们每次新建一个接口和实现的,都需要向META-INF.services中写一次声明的话,太过于繁琐了,而我们借助AutoService,可以让它来帮我们生成。
首先引入依赖
arduino
kapt 'com.google.auto.service:auto-service:1.0-rc4'
less
@AutoService(IFeatureBInterface.class)
public class FeatureBImpl implements IFeatureBInterface {
@Override
public void getB() {
......
}
@Override
public void getA() {
......
}
}
使用方式也非常简单,就是在实现类上加上AutoService注解,参数为对应接口的class即可。
经过编译之后在可以在对应module 的 build/intermediates/runtime_library_classes_dir/debug/META-INF/services/ 目录下 看到生成的声明文件。
实现原理
我们回过头来看下上文提到的ServiceLoader,因为我们的调用都是通过它来实现的,我们来看下它是如何完成服务发现的。
ServiceLoader 是一个被final修饰的类,不可被继承修改,同时它实现了Iterable接口,这是为了方便后续可以通过迭代循环的方式找到对应的服务实现类。
我们先从ServiceLoader#load方法看:
scss
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
reload();
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
这里主要是ServiceLoader对象的创建,最后的找到服务实现的工作是在一个内部实现类 LazyIterator 中完成的。
接下来我们看下ServiceLoader#iterator 方法做了些什么
typescript
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();// 调用 LazyIterator
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();// 调用 LazyIterator
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
这里主要是迭代循环查找实现类的时候先从 providers 缓存中查找,如果缓存没有命中 就在 LazyIterator 中查找。
在调用LazyIterator 迭代的时候主要如下:
typescript
public boolean hasNext() {
return hasNextService();
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//通过PREFIX(META-INF/services/)和类名 获取对应的配置文件,得到具体的实现类
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;
}
typescript
public S next() {
return nextService();
}
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) {
....
}
....
try {
//创建实现类的对象
S p = service.cast(c.newInstance());
//放入到缓存集合中
providers.put(cn, p);
return p;
} catch (Throwable x) {
.....
}
throw new Error(); // This cannot happen
}
简单来说,就是通过声明文件的名称来加载这个服务的声明文件,通过这个声明文件来获取到实现服务的完整名称,再通过反射来创建实现服务的实例,然后存到缓存中供调用者使用。
AutoService原理
AutoService的原理主要分为三步:
1.在开发阶段,使用注解对接口实现类修饰修饰。
2.在编译期阶段,注解处理器去遍历被注解修饰的类的信息,然后收集到写入到META-INF/services文件夹下的文件中。
3.在使用阶段,通过ServiceLoader去MATE-INF/services文件加下查找指定文件,然后解析获取到需要加载的类信息。
这里AutoService 使用的是APT技术,APT全称 Annotation Processing Tool
,翻译为注解处理器,是一种处理注解的工具,它对源码文件进行检测找出其中注解,并进行额外的处理。
APT能够在编译期阶段,帮助我们自动生成代码,简化使用。
AutoService中的 AutoServiceProcessor 继承了抽象类 AbstractProcessor :
typescript
public class AutoServiceProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
try {
return processImpl(annotations, roundEnv);
} catch (Exception e) {
....
return true;
}
}
private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//注解是否处理完成
if (roundEnv.processingOver()) {
//生成声明配置文件
generateConfigFiles();
} else {
//解析注解信息
processAnnotations(annotations, roundEnv);
}
return true;
}
}
所有的注解操作都是在 process 方法中完成的。
scss
private void processAnnotations(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//获取被注解修饰的类的集合
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);
for (Element e : elements) {
TypeElement providerImplementer = (TypeElement) e;
//获取注解信息
AnnotationMirror annotationMirror = getAnnotationMirror(e, AutoService.class).get();
//获取注解中的值
Set<DeclaredType> providerInterfaces = getValueFieldOfClasses(annotationMirror);
//注解中没有值 抛出异常
if (providerInterfaces.isEmpty()) {
error(MISSING_SERVICES_ERROR, e, annotationMirror);
continue;
}
//遍历注解上的值
for (DeclaredType providerInterface : providerInterfaces) {
TypeElement providerType = MoreTypes.asTypeElement(providerInterface);
//判断该子类的类型与AutoService.value中的值是否一致,如果一致添加到providers缓存中。
if (checkImplementer(providerImplementer, providerType)) {
providers.put(getBinaryName(providerType), getBinaryName(providerImplementer));
} else {
........
}
}
}
}
流程小结:
1.获取被注解修饰的类的集合
2.遍历被AutoService注解修饰的类
3.获取注解的value值
4.遍历获取到的value值。
5.如果该类与注解中的value值一致的话存入缓存中。
ini
private void generateConfigFiles() {
Filer filer = processingEnv.getFiler();
//遍历缓存中的信息
for (String providerInterface : providers.keySet()) {
//根据缓存信息 拼接文件名,一个接口对应一个文件。
String resourceFile = "META-INF/services/" + providerInterface;
try {
SortedSet<String> allServices = Sets.newTreeSet();
try {
//META-INF/services/下 已经存在的声明配置文件
FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "", resourceFile);
//声明配置文件中 声明的服务集合
Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
allServices.addAll(oldServices);
} catch (IOException e) {
......
}
Set<String> newServices = new HashSet<String>(providers.get(providerInterface));
//如果要创建的服务已经存在就略过
if (allServices.containsAll(newServices)) {
return;
}
allServices.addAll(newServices);
//创建对应的声明配置文件
FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",resourceFile);
OutputStream out = fileObject.openOutputStream();
ServicesFiles.writeServiceFile(allServices, out);
out.close();
} catch (IOException e) {
....
return;
}
}
}
流程小结:
1.遍历缓存的信息
2.根据缓存的信息拼接文件名,一个接口对应一个文件
3.读取 META-INF/services/下 已经存在的声明配置文件,如果对应的声明文件已经存在就直接返回。
4.创建对应的声明文件
至此,AutoService的原理就分析完了,简答来说就是利用APT技术来帮助我们生成声明文件,大大简化了我们的使用。
总结
其实不难发现,SPI的本质是通过反射来完成的,我们按照规定将暴露对外使用的接口和实现类在META-INF.service下进行声明,这些操作由AutoService利用APT技术来帮助我们生成了。
通过SPI技术能够大大地提高接口灵活性。