什么是spi
SPI即 Service Provider Interface
,它是为接口提供实现服务类,类似IOC的思想
ServiceLoader
java中的spi
, ServiceLoader
尤为重要, 抛开注解生成代码和Transform
注册
-
load
可以使用map
实现,需要Class<T> baseClass
,String servicePath
构建返回约束和访问参数 -
register
项目经常需要一些全局唯一的Service
以及使用时创建Service
,注册参数有path 索引唯一路径
,interfaceClass 接口类
,obj 单实例/实例/惰性
假如我是一个库设计者,我希望把一个接口暴露给使用者实现具体的逻辑,那么我肯定不能够写死实现类对吧,不然我们怎么扩展嘛!比如我们有以下接口
package com.example.newtestproject
interface TestSpi {
fun getSpi();
}
如果我在使用的过程中,想不关心具体的实现类/又或者想兼容多个实现,那么怎么办呢?(比如日常开发的gradle,如果我想兼容多个agp版本在自己库的运行,一个个写死肯定是一个非常糟糕的实现,如果出了新版本,那么我们还要更改代码),我们能不能只定义一个规范,即上面的TestSpi就可以不关心以后的扩展呢?很简单,我们只需要在resource/META-INF/services目录下定义一个以该接口为名称的文件,文件内容是具体的接口实现类即可,如图
class TheTestSpi:TestSpi {
override fun getSpi() {
Log.i("hello","i am the interface implementation from TheTestSpi")
}
}
那么文件只需要写入com.example.newtestproject.TheTestSpi即可。
在我们想要用到TestSpi的功能的时候,就可以通过以下方式进行使用,从而不用关心具体的实现,达到了解耦合的目的
// spi test
val load = ServiceLoader.load(TestSpi::class.java)
load.forEach {
it.getSpi()
if(it is TheTestSpi){
Log.i("hello","theTestSpi")
}
}
ServiceLoader.load
从上面我们可以看到,最关键的是调用了ServiceLoader.load方法,这个就是Spi具体的实现了,本质是什么呢?相信都能够猜到了,其实就是反射,我们来跟一下源代码
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取了一个当前类的classloader,做好了准备
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
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;
}
我们可以看到,执行的时候有这么一句String fullName = PREFIX + service.getName();,很容易想到,如果我们要反射的话,是不是需要类全称,PREFIX 其实就是
private static final String PREFIX = "META-INF/services/";
可以看到,之所以我们需要在resource下定一个文件路径是META-INF/services,是因为在ServiceLoader中定义好了,所以ServiceLoader会按照约定的路径,去该文件夹下查找service.getName()(TestSpi)的实现类,最后通过
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,
// Android-changed: Let the ServiceConfigurationError have a cause.
"Provider " + cn + " not found", x);
// "Provider " + cn + " not found");
}
.....
Class.forName(cn, false, loader) 去进行了类查找操作,因为我们通过foreach去遍历,其实就是通过迭代器去获取了每个实例,所以才能够调用到了实现类TheTestSpi的getSpi() 方法。
在Android中的应用
SPI机制在很多库的设计上都有应用,比如Coroutine(kotlin协程库)就有用到,比如我们经常用到的 MainCoroutineDispatcher,如果我们希望一个任务运行在主线程,在Android就可以通过handler的方式去向主线程post一个消息,那如果在其他环境呢?kotlin不仅仅是想在android的世界立足,还有很多比如如果在native环境呢?在服务器环境呢?多平台环境呢(如KMM),那就不一定有Handler这个概念对不对!但是都有一个主线程的概念,所以Coroutine把这部分就通过SPI的方式去实现了,如
定义了这个接口 MainDispatcherFactory::class.java 用于给具体环境的主线程实现类进行实现,具体的实现类就是
internal class AndroidDispatcherFactory : MainDispatcherFactory {
override fun createDispatcher(allFactories: List<MainDispatcherFactory>) =
HandlerContext(Looper.getMainLooper().asHandler(async = true))
override fun hintOnError(): String? = "For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used"
override val loadPriority: Int
get() = Int.MAX_VALUE / 2
}
可以看到,在Android中就通过了Handler去实现,最后我们可以在源码中看到,SPI相关的注册信息
总结
通过SPI技术去实现的解耦合工作的出色工程还有很多很多,比如我们用的APT,还有didi开源的Booster,都有用到这方面的知识。