前语
前段时刻阅览到一篇文章,关于Service-Provider-Interface机制(SPI机制),在评论区看到一条评论:
spi完成类是不是只能是空结构函数?
后续我又回味了一下,这个问题能够引出许多风趣的内容,决定系统性的考虑并分享评论一番。
作者按:有时候考虑未必能取得令人振奋的完美答案,但这种考虑是触发突变的堆集
由于评论内容的scope比较广,而我的行文思路比较跳跃,为尽或许防止阅览时乏力,读者诸君可参阅以下导图:
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.
Service provider interface 是能被第三方承继或许完成的API,能够用作结构扩张或许可变组件
不难了解,中心需求:
- 预先界说服务接口,即SPI接口
- 由供给服务的模块自行完成SPI接口,并在Meta info中注册
- 供给服务的模块由某种机制被加载,例如编译时、运转时,一般运用编译时,运转时将触及插件化等
- 发现并加载服务完成
Demo
界说以下module,依靠关系如下:
- api 用于接口和模型类界说
- host 为主工程
- 编码时,依靠api
- 编译时,依靠api 和 服务供给模块
- module-a 一个服务供给模块,依靠api
便利起见,不再界说多个服务供给模块,完成类均置于module-a中,读者应当能够了解,host经过编译时确定服务供给模块,是一种”可变组件”的完成方法
- 在api中界说接口:
interface DemoApi {
fun doSth(): String
}
- 在module-a中界说完成类
@AutoService(DemoApi::class)
class ModuleADemoApiImpl : DemoApi {
companion object {
const val NAME = "ModuleADemoApiImpl"
}
override fun doSth(): String {
return "the result by $NAME"
}
}
留意,还需求在Meta info中进行注册,手工操作比较费事,直接凭借Google的AutoService。
留意,Demo中图便利运用Kotlin,因而需运用kapt,假设日常习惯运用ksp,Zacsweers供给了AutoService的ksp版,并需求处理打包资源目录
内容如下:
osp.leobert.android.module.a.ModuleADemoApiImpl
模块加载即为声明dependency并编译,略去。
运用
fun directLoadDemo() {
val loader = ServiceLoader.load(DemoApi::class.java)
val iterator = loader.iterator()
var hasNext = false
do {
try {
hasNext = iterator.hasNext()
if (hasNext) {
iterator.next().let {
println("find a impl of DemoApi, doSth:")
println(it.doSth())
println()
}
}
} catch (e: Throwable) {
println("thr: " + e.message)
}
} while (hasNext)
println("finish directLoadDemorn")
}
运转将在控制台观测到:
find a impl of DemoApi, doSth:
the result by ModuleADemoApiImpl
面向问题
上文已废诸多笔墨,演示了SPI的运用,让咱们从头回到问题:
运用SPI时,SPI完成类是不是必需求无参结构函数?
不难了解,这需求从服务加载进程寻觅答案。接下来咱们分析下 ServiceLoader
中关于服务加载的中心代码。
原因分析-ServiceLoader中心代码
作者按:为什么要写这一段?
SPI是一种机制,既然是机制,就能够有多种完成手法,这里是Java中供给的一种手法!
阅览了解这一手法的完成能够帮助了解机制,而且能触类旁通地联想到其他手法.唯有自行阅览才能有最深地领会!
读者应当留意到,服务发现和服务加载的中心是ServiceLoader,答案也在其间。
作者按:或许大部分读者都是Android开发者,我挑选android.jar中的代码。留意,JDK中不同版本的源码不一致;Android发展历程中或许也发生过演变,未寻觅依据
咱们先重视两个中心方法:
static <S> ServiceLoader<S> load(Class<S> service)
Iterator<S> iterator()
class ServiceLoader {
private LazyIterator lookupIterator;
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
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();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}
很明显,服务发现和服务加载进程中需求利用 ClassLoader
,相关常识不再展开,此处可引发大量黑科技联想
knownProviders
是已加载实例的池,不是要点,要点是 lookupIterator
这部分代码略长,中心点在于:
- 根据
ClassLoader
加载指定的Resource,即META-INF/services/{接口类名}
,还记得AutoService生成的文件吗? - 解析内容取得类名
- 经过反射加载类
- 调用
Class#newInstance()
取得实例
Class#newInstance()
制约了服务完成类必需求有无参结构函数。
代码如下,泛读领会即可
private class LazyIterator implements Iterator<S> {
Class<S> service;
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 (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;
}
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", x);
}
if (!service.isAssignableFrom(c)) {
ClassCastException cce = new ClassCastException(service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
fail(service, "Provider " + cn + " not a subtype", cce);
}
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
}
public boolean hasNext() {
return hasNextService();
}
public S next() {
return nextService();
}
public void remove() {
throw new UnsupportedOperationException();
}
}
处理方案
严厉来说,假设项目中:
- 有谨慎且健壮且全面 的 方针生命周期办理,而且与方针实例化时刻无关联
- 经过其他途径,不依靠结构器做依靠注入
那么将可消除源头问题,即:没有运用有参结构器的必要。但实践比较骨感,这种假定过于理想化,而且会对编码习惯带来许多冲击。
假设非要运用含参结构器,有以下思路:
- 暗度陈仓,不直接供给服务完成,而是根据SPI机制和现有完成,供给一个新服务,该服务满足”创建、获取特定服务”的需求,将实例的创建进程与获取进程分离 。浅显地讲,界说的Interface为方针api-Interface的Factory或许Builder
- 力大砖飞,自完成SPI机制,浅显地讲,即自界说ServiceLoader
与规划形式磕碰
换个视点看待问题,运用 ServiceLoader
时,其一起完成了:
- 服务发现
- 服务加载(实例化)
问题在于,实例化方法不满足服务供给者期望,而服务运用者关心点在于发现服务并运用服务,此刻则不难想到规划形式。
如Factory模块,其创建方针时,无需对运用者露出创建的逻辑。
运用Factory形式
此刻,SPI接口不再是原服务接口,而是原服务接口的Factory
Demo
interface DemoApiFactory {
fun create(): DemoApi
}
模块供给Factory完成
class SomeOp {
fun execute(): String {
return "[result of SomeOp $this]"
}
}
//@AutoService(DemoApi::class)
class ModuleADemoApiImpl2(val someOp: SomeOp) : DemoApi {
companion object {
const val NAME = "ModuleADemoApiImpl2"
}
override fun doSth(): String {
return "${someOp.execute()} - the result by $NAME"
}
@AutoService(DemoApiFactory::class)
class Factory : DemoApiFactory {
override fun create(): DemoApi {
return ModuleADemoApiImpl2(SomeOp())
}
}
}
运用
fun useFactoryDemo() {
val loader = ServiceLoader.load(DemoApiFactory::class.java)
val iterator = loader.iterator()
var hasNext = false
do {
try {
hasNext = iterator.hasNext()
if (hasNext) {
iterator.next().create().let {
println("find a impl of DemoApi, doSth:")
println(it.doSth())
println()
}
}
} catch (e: Throwable) {
println("thr: " + e.message)
}
} while (hasNext)
println("finish useFactoryDemorn")
}
find a impl of DemoApi, doSth:
[result of SomeOp osp.leobert.android.module.a.SomeOp@26ba2a48]
- the result by ModuleADemoApiImpl2
finish useFactoryDemo
问题
读者诸君不难了解,Demo中的状况模仿的非常简单,而实践状况往往比较杂乱,例如:获取结构器所需的参数往往比较杂乱,或许来自不同模块
此刻可与Builder形式相结合,假设Builder已存在有参结构函数,在不修改的状况下,可继续套用Factory。
运用Builder形式
interface DemoApi {
fun doSth(): String
interface Builder {
var foo: Foo
fun build(): DemoApi
interface Factory {
fun create(): Builder
}
}
}
class Foo {
val createdAt = Throwable().stackTrace[1].toString()
}
模仿一个服务完成类,需求的参数分别由当前模块和宿主模块供给,因而运用Builder将进程分离
//@AutoService(DemoApi::class)
class ModuleADemoApiImpl3(val someOp: SomeOp, val needProvideByHost: Foo) : DemoApi {
companion object {
const val NAME = "ModuleADemoApiImpl3"
}
override fun doSth(): String {
return "${someOp.execute()} ,param2 create at${needProvideByHost.createdAt} - the result by $NAME"
}
class Builder(val someOp: SomeOp) : DemoApi.Builder {
override lateinit var foo: Foo
override fun build(): DemoApi {
return ModuleADemoApiImpl3(someOp, foo)
}
@AutoService(DemoApi.Builder.Factory::class)
class Factory : DemoApi.Builder.Factory {
override fun create(): DemoApi.Builder {
//the logic to get SomeOp instance,it may be complex
val someOp = SomeOp()
return Builder(someOp)
}
}
}
}
运用:
fun useBuilderDemo() {
val loader = ServiceLoader.load(DemoApi.Builder.Factory::class.java)
val iterator = loader.iterator()
var hasNext = false
do {
try {
hasNext = iterator.hasNext()
if (hasNext) {
iterator.next().create().let {
it.foo = Foo()
it.build()
}.let {
println("find a impl of DemoApi, doSth:")
println(it.doSth())
println()
}
}
} catch (e: Throwable) {
println("thr: " + e.message)
}
} while (hasNext)
println("finish useBuilderDemorn")
}
成果如下:
find a impl of DemoApi, doSth:
[result of SomeOp osp.leobert.android.module.a.SomeOp@180bc464] ,
param2 create at osp.leobert.android.host.MyClassKt.useBuilderDemo(MyClass.kt:75)
- the result by ModuleADemoApiImpl3
finish useBuilderDemo
然即使如此,实践项目中,依旧会有诸多费事。依靠获取或依靠注入,永久会面对极点杂乱的状况。咱们模仿的状况永久比不上实践状况杂乱。
读者诸君应当能够了解,当面对极点杂乱的状况时,例如参数来自3个甚至更多模块时,即使利用规划形式仍能处理问题,但其规划和了解成本已然极高!
与依靠注入磕碰
虽然从广义上看,SPI机制也是一种特定场景下的DI完成,本章节暂不无限扩展,仅在下个章节中留下初步。
当与依靠注入的手法相磕碰时,可考虑两个方向:
- 服务模块内部运用依靠注入,考量SPI(ServiceLoader)是否可无缝联接模块内DI
- 力大砖飞,自完成SPI机制,浅显地讲,即自界说ServiceLoader,并在其间无缝联接DI
与Dagger2兼容性探寻
首先能够清晰一点:
@AutoService
有必要注解于非抽象类上,所以,假设ServiceLoader能够和Dagger2生成的代码兼容,也需求手动注册 (或许自扩展Dagger2的编译,对生成类添加标记)其次,不难经过考虑得出结论:ServiceLoader 直接加载 Dagger2 的生成类,将会损坏Dagger2对依靠的生命周期办理。即使模仿
Anvil
(相似Hilt)进行一系列自界说扩展,也无法降低规划难度,不再展开评论。
因而,可靠的完成思路为:SPI接口完成类依靠 DaggerComponent
,并据此进入Dagger的国际获取到方针方针实例。伪代码如下:
class ApiFactoryImpl : ApiFactory {
fun create(foo: Foo): Api {
return DaggerComponent.create().provideApi(foo)
}
}
留意,伪代码仅暗示,实践编写时仍需求考虑Component的生命周期控制,以防止发生潜在BUG
很明显,由宿主模块供给的依靠运用 @Assisted
注解标记,其他依靠经过 Dagger2
进行办理
从头界说SPI接口:
interface DemoApiFactory2 {
fun create(foo: Foo): DemoApi
}
服务模块内部运用 DI:
class ModuleADemoApiImpl4
@AssistedInject constructor(
val someOp: SomeOp,
@Assisted val needProvideByHost: Foo
) : DemoApi {
companion object {
const val NAME = "ModuleADemoApiImpl4"
}
override fun doSth(): String {
return "${someOp.execute()} ,param2 create at${needProvideByHost.createdAt} - the result by $NAME"
}
@AutoService(DemoApiFactory2::class)
class SpiFactory : DemoApiFactory2 {
//留意,仅示例代码,实践运用时需严厉遵从Component的生命周期需求获取实例
private val factory by lazy {
DaggerAppComponent.create().provideFactory()
}
override fun create(foo: Foo): DemoApi {
return factory.create(foo)
}
}
@AssistedFactory
abstract class Factory {
abstract fun create(@Assisted needProvideByHost: Foo): ModuleADemoApiImpl4
}
}
DI部分的代码疏忽,详见仓库代码。
这个思路能够处理选用多种规划形式带来的编码杂乱度问题。
那么,是否得到了银弹呢?答案是否定的!SPI机制的规划是”轻量级”的,当依照这一思路,完美处理依靠注入和依靠办理难点时(明显它需求拥有一种聚合才能方可处理问题),简单推理即能够发现:
即使不运用SPI机制,也能够根据此刻的DI结构的聚合才能,完成:enable framework extension and replaceable components 的方针
价值是每个framework extension 都被一个特定的三方DI结构所绑缚,这明显不是好主意!
作者按:或许我这样表述很不便于了解,读者诸君能够从SpringBoot的相关常识进行横向对比了解:
在SpringBoot中有
spring.factories
,它能够在不侵入代码的状况下,运用第三方Jar包中的Bean,它的完成与JDK中SPI机制完成根本相似, 但SpringBoot本身就包含IOC容器,只需运用SpringBoot生态则意味着接受了它的DI,因而可无视DI结构绑缚
自界说ServiceLoader
读到此处的读者诸君,咱们将进行这次考虑中最要害的一步!咱们已经收集到诸多思路的弊端,假设自界说ServiceLoader完成SPI机制,最佳实践应当怎么?
正如我前文所言,这里只是只要初步,没有最终答案,只要一些思路供给参阅:
- 1.服务发现部分:
- 结合注册清单、反射手法等,应当取得是否有完成类
- 2.服务加载部分:
- 结合注册清单、反射手法等,应当取得完成类的构建途径和所需依靠
ServiceLoader中处理第一点时,将所有的注册类反射遍历,利用类型揣度完成方针。处理第二点时,直接反射无参结构器,致使存在必定限制。
因而,在规划时能够考虑:
- 注册清单中能够获悉服务完成类的实例化路径
- 注册清单中能够获悉服务完成类实例化时需求的依靠信息
- 将ServiceLoader规划为轻量级的IOC容器,撇去读写生命周期办理,仅供给有限的依靠获取途径
例如:注册清单内容能够规划为:
{Interface/abstract class}:{Implement Class}#construcor({Param Type1},{Param Type2})
osp.leobert.android.api.DemoApi:osp.leobert.android.module.a.ModuleADemoApiImpl4#ModuleADemoApiImpl4(osp.leobert.android.module.a.SomeOp,osp.leobert.android.api.Foo)
//便利阅览,进行换行:
osp.leobert.android.api.DemoApi:
osp.leobert.android.module.a.ModuleADemoApiImpl4#
ModuleADemoApiImpl4(
osp.leobert.android.module.a.SomeOp,
osp.leobert.android.api.Foo
)
结语
最近写文章时,有一些苦恼:对于单纯的常识点,不太乐意动笔;而容易发散的常识,发散出去又难以收束。
最近也在考虑,希冀寻觅到源于内心深处的无量力量,解开捆绑精力的桎梏,照亮前行的道路。
前段时刻读到一句话,分享给读者诸君:
there are three pillars in life: health, time, and money. At any given moment, most people have at most two. If you’re fortunate enough to have all three, you make the most of it while you can.