概述
- 什么是spi
SPI (Service Provider Interface)属于动态加载接口实现类
的的一项技术,是JDK内置的一种服务提供发现机制,使用ServiceLoader去加载接口对应的实现,这样我们就不用关注实现类,ServiceLoader会告诉我们。官方文档描述为:为某个接口寻找服务的机制,类似IOC思想,将装配的控制权交给ServiceLoader。
- 解决问题
只提供服务接口,具体服务由其他组件实现,接口和具体实现分离(类似桥接),同时能够通过系统的ServiceLoader
拿到这些实现类的集合,统一处理,这样在组件化中往往会带来很多便利,SPI机制可以实现不同模块之间方便的面向接口编程,拒绝了硬编码的方式,解耦效果很好
即相当于制定标准,然后不同实现方用不同的方式实现标准供使用方使用,并且可以动态加载
在Android中如何使用
上面说的可能比较抽象,下面将结合例子说明下在Android中的运用。
这种机制在使用起来也比较简单,使用步骤如下:
定义接口和接口的实现类
创建resources/META-INF/services目录
在上述Service目录下,创建一个以接口名(类的全名) 命名的文件, 其内容是实现类的类名 (类的全名)。
在services目录下创建的文件是com.binglumeng.spidemo.IService 文件中的内容为Animal接口的实现类, 可能是com.binglumeng.spidemo.AService
- 在java代码中使用ServcieLoader来动态加载并调用内部方法.
主工程和组件之间一些“服务”的配置
定义接口
1 |
|
在主工程和bdisplay 模块中的实现该接口
创建spi描述文件
在工程的main目录下新建目录resources/META-INF/services,以服务接口名为文件名新建spi描述文件,内容为具体的服务实现类权限定名,可以有多个
文件结构如下
加载不同服务
通过ServiceLoader来加载接口的不同实现类,然后会得到迭代器,在迭代器中可以拿到不同实现类全限定名,然后通过反射动态加载实例就可以调用display方法了。
1 | ServiceLoader<Display> loader = ServiceLoader.load(IDisplay.class); |
源码分析
感觉有点很神奇
ServiceLoader
loader = ServiceLoader.load(Display.class);
就可以拿到Display.class
接口的所有实现类了, amazing!(感觉这里跟Retrift使用有点类似)下面来分析一下这个背后到底隐藏了什么
核心类 ServiceLoader.java
先看下几个重要的成员变量
- PREFIX就是配置文件所在的包目录路径;
- service就是接口名称,在我们这个例子中就是Display;
- loader就是类加载器,其实最终都是通过反射加载实例;
- providers就是不同实现类的缓存,key就是实现类的全限定名,value就是实现类的实例
- lookupIterator就是内部类LazyIterator的实例。
1 | private static final String PREFIX = "META-INF/services/"; |
之前spi加载的三个关键步骤
- ServiceLoader
loader = ServiceLoader.load(IDisplay.class); - mIterator =loader.iterator();
- while(mIterator.hasNext()){
mIterator.next().display();
}
获取实现接口集合
ServiceLoader提供了两个静态的load方法,如果我们没有传入类加载器,ServiceLoader会自动为我们获得一个当前线程的类加载器,最终都是调用构造函数。
1 |
|
构造函数中有一个重要的函数reload
1 |
|
所以看到当我们load class之后并没有得到什么实现类,那么在何时加载的呢?
懒加载
那么service provider在什么地方进行加载?我们接着看第二个步骤loader.iterator(),
- 首先会到providers中去查找有没有存在的实例,有就直接返回,没有再到LazyIterator中查找
1 |
|
其实就是返回一个迭代器。我们看下官方文档的解释,这个就是懒加载实现的地方,
焦点聚焦在LazyIterator
上
- hasNext()
- 首先拿到配置文件名fullName,我们这个例子中是com.example.Display
- 通过类加载器获得所有模块的配置文件Enumeration
configs configs - 依次扫描每个配置文件的内容,返回配置文件内容Iterator
pending,每个配置文件中可能有多个实现类的全限定名,所以pending也是个迭代器。
1 |
|
Tips
关于 ClassLoader.getSystemResources(fullName)可以查阅
- next()
在上面hasNext()方法中拿到的nextName就是实现类的全限定名,接下来我们去看看具体实例化工作的地方next():
- 1.首先根据nextName,Class.forName加载拿到具体实现类的class对象
- 2.Class.newInstance()实例化拿到具体实现类的实例对象
- 3.将实例对象转换service.cast为接口
- 4.将实例对象放到缓存中,providers.put(cn, p),key就是实现类的全限定名,value是实例对象。
- 5.返回实例对象
1 |
|
总结
Spi的优缺点
- 优点
只提供服务接口,具体服务由其他组件实现,接口和具体实现分离,同时能够通过系统的ServiceLoader拿到这些实现类的集合,统一处理。
- 缺点
- Java中SPI是随jar发布的,每个不同的jar都可以包含一系列的SPI配置,而Android平台上,应用在构建的时候最终会将所有的jar合并,这样很容易造成相同的SPI冲突,常见的问题是DuplicatedZipEntryException异常
- 读取SPI配置信息是在运行时从jar包中读取,由于apk是签过名的,在从jar中读取的时候,签名校验的耗时问题会造成性能损失
后续可以改进的点
Java中使用ServiceLoader去读取SPI配置信息是在程序运行时,我们可以将这个读取配置信息提前,在编译时候就搞定,通过gradle插件,去扫描class文件,找到具体的服务类(可以通过标注来确定),然后生成新的java文件,这个文件中包含了具体的实现类。这样程序在运行时,就已经知道了所有的具体服务类,缺点就是编译时间会加长,自己需要重新写一套读取SPI信息、生成java文件等逻辑。
经过优化后,SPI已经偏离了原本的初衷,但是可以做更多的事,可以将业务服务分离,通过SPI找到业务服务入口,业务组件化,抽成单独的aar,独立成工程。