好玩系列 | 当SPI 和 设计模式、依赖注入发生碰撞,可以擦出怎样的火花
前言
前段时间阅读到一篇文章,关于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 directLoadDemo\r\n")
}
运行将在控制台观测到:
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 useFactoryDemo\r\n")
}
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 useBuilderDemo\r\n")
}
结果如下:
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.