前语

前段时刻阅览到一篇文章,关于Service-Provider-Interface机制(SPI机制),在评论区看到一条评论:

spi完成类是不是只能是空结构函数?

后续我又回味了一下,这个问题能够引出许多风趣的内容,决定系统性的考虑并分享评论一番。

作者按:有时候考虑未必能取得令人振奋的完美答案,但这种考虑是触发突变的堆集

好玩系列 | 当SPI 和 规划形式、依靠注入发生磕碰,能够擦出怎样的火花

由于评论内容的scope比较广,而我的行文思路比较跳跃,为尽或许防止阅览时乏力,读者诸君可参阅以下导图:

好玩系列 | 当SPI 和 规划形式、依靠注入发生磕碰,能够擦出怎样的火花

文中触及的代码可于本仓库获取

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接口
  • 由供给服务的模块自行完成SPI接口,并在Meta info中注册
  • 供给服务的模块由某种机制被加载,例如编译时、运转时,一般运用编译时,运转时将触及插件化等
  • 发现并加载服务完成

Demo

界说以下module,依靠关系如下:

好玩系列 | 当SPI 和 规划形式、依靠注入发生磕碰,能够擦出怎样的火花

  • api 用于接口和模型类界说
  • host 为主工程
    • 编码时,依靠api
    • 编译时,依靠api 和 服务供给模块
  • module-a 一个服务供给模块,依靠api

便利起见,不再界说多个服务供给模块,完成类均置于module-a中,读者应当能够了解,host经过编译时确定服务供给模块,是一种”可变组件”的完成方法

  1. 在api中界说接口:
interface DemoApi {
    fun doSth(): String
}
  1. 在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版,并需求处理打包资源目录

好玩系列 | 当SPI 和 规划形式、依靠注入发生磕碰,能够擦出怎样的火花

内容如下:

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.