概述
什么是 spi
SPI (Service Provider Interface) 属于
动态加载接口实现类
的的一项技术, 是 JDK 内置的一种服务提供发现机制, 使用 ServiceLoader 去加载接口对应的实现, 这样我们就不用关注实现类, ServiceLoader 会告诉我们. 官方文档 https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html 描述为:
为某个接口寻找服务的机制, 类似 IOC 思想, 将装配的控制权交给 ServiceLoader.
解决问题
只提供服务接口, 具体服务由其他组件实现, 接口和具体实现分离 (类似桥接), 同时能够
通过系统的 ServiceLoader
拿到这些实现类的集合, 统一处理, 这样在组件化中往往会带来很多便利, SPI 机制可以实现不同模块之间方便的面向接口编程, 拒绝了硬编码的方式, 解耦效果很好
即相当于制定标准, 然后不同实现方用不同的方式实现标准供使用方使用, 并且可以动态加载
在 Android 中如何使用
上面说的可能比较抽象, 下面将结合例子说明下在 Android 中的运用.
这种机制在使用起来也比较简单, 使用步骤如下:
定义接口和接口的实现类
创建 resources/META-INF/services 目录
在上述 Service 目录下, 创建一个以接口名 (类的全名) 命名的文件, 其内容是实现类的类名 (类的全名).
在 services 目录下创建的文件是 com.binglumeng.spidemo.IService 文件中的内容为 Animal 接口的实现类, 可能是 com.binglumeng.spidemo.AService
在 java 代码中使用 ServcieLoader 来动态加载并调用内部方法.
主工程和组件之间一些 "服务" 的配置
定义接口
- package com.example;
- public interface IDisplay {
- String display();
- }
在主工程和 bdisplay 模块中的实现该接口
创建 spi 描述文件
在工程的 main 目录下新建目录 resources/META-INF/services,
以服务接口名为文件名新建 spi 描述文件, 内容为具体的服务实现类权限定名, 可以有多个
文件结构如下
加载不同服务
通过 ServiceLoader 来加载接口的不同实现类, 然后会得到迭代器, 在迭代器中可以拿到不同实现类全限定名, 然后通过反射动态加载实例就可以调用 display 方法了.
- ServiceLoader<Display> loader = ServiceLoader.load(IDisplay.class);
- mIterator =loader.iterator();
- while(mIterator.hasNext()){
- mIterator.next().display();
- }
源码分析
感觉有点很神奇
ServiceLoader loader = ServiceLoader.load(Display.class);
就可以拿到 Display.class 接口的所有实现类了, amazing!(感觉这里跟 Retrift 使用有点类似) 下面来分析一下这个背后到底隐藏了什么
核心类 ServiceLoader.java
先看下几个重要的成员变量
PREFIX 就是配置文件所在的包目录路径;
service 就是接口名称, 在我们这个例子中就是 Display;
loader 就是类加载器, 其实最终都是通过反射加载实例;
providers 就是不同实现类的缓存, key 就是实现类的全限定名, value 就是实现类的实例
lookupIterator 就是内部类 LazyIterator 的实例.
- private static final String PREFIX = "META-INF/services/";
- // The class or interface representing the service being loaded
- private Class<S> service;
- // The class loader used to locate, load, and instantiate providers
- private ClassLoader loader;
- // Cached providers, in instantiation order
- private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
- // The current lazy-lookup iterator
- private LazyIterator lookupIterator;
之前 spi 加载的三个关键步骤
- ServiceLoader loader = ServiceLoader.load(IDisplay.class);
- mIterator =loader.iterator();
- while(mIterator.hasNext()){
- mIterator.next().display();
- }
获取实现接口集合
ServiceLoader 提供了两个静态的 load 方法, 如果我们没有传入类加载器, ServiceLoader 会自动为我们获得一个当前线程的类加载器, 最终都是调用构造函数.
- public static <S> ServiceLoader<S> load(Class<S> service) {
- ClassLoader cl = Thread.currentThread().getContextClassLoader();
- return ServiceLoader.load(service, cl);
- }
- public static <S> ServiceLoader<S> load(Class<S> service,
- ClassLoader loader){
- return new ServiceLoader<>(service, loader);
- }
构造函数中有一个重要的函数 reload
- public void reload() {
- providers.clear();
- lookupIterator = new LazyIterator(service, loader);
- }
- private LazyIterator(Class<S> service, ClassLoader loader) {
- this.service = service;
- this.loader = loader;
- }
所以看到当我们 load class 之后并没有得到什么实现类, 那么在何时加载的呢?
懒加载
那么 service provider 在什么地方进行加载? 我们接着看第二个步骤 loader.iterator(),
首先会到 providers 中去查找有没有存在的实例, 有就直接返回, 没有再到 LazyIterator 中查找
- 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();
- }
- };
- }
其实就是返回一个迭代器. 我们看下官方文档的解释, 这个就是懒加载实现的地方,
焦点聚焦在 LazyIterator 上
hasNext()
首先拿到配置文件名 fullName, 我们这个例子中是 com.example.Display
通过类加载器获得所有模块的配置文件 Enumeration configs configs
依次扫描每个配置文件的内容, 返回配置文件内容 Iterator pending, 每个配置文件中可能有多个实现类的全限定名, 所以 pending 也是个迭代器.
- public boolean hasNext() {
- if (nextName != null) {
- return true;
- }
- if (configs == null) {
- try {
- // 首先拿到配置文件名 fullName
- 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;
- }
- // 依次扫描每个配置文件的内容, 返回配置文件内容 Iterator<String> pending
- pending = parse(service, configs.nextElement());
- }
- nextName = pending.next();
- return true;
- }
- Tips
关于 ClassLoader.getSystemResources(fullName) 可以查阅
Java 目录总结 (七. 路径的获取二 --ClassLoader 的使用) http://zyjustin9.iteye.com/blog/2022654
Class.getResource() 和 ClassLoader.getResource() 的区别 https://blog.csdn.net/walkerjong/article/details/13019671
next()
在上面 hasNext() 方法中拿到的 nextName 就是实现类的全限定名, 接下来我们去看看具体实例化工作的地方 next():
1. 首先根据 nextName,Class.forName 加载拿到具体实现类的 class 对象
2.Class.newInstance() 实例化拿到具体实现类的实例对象
3. 将实例对象转换 service.cast 为接口
4. 将实例对象放到缓存中, providers.put(cn, p),key 就是实现类的全限定名, value 是实例对象.
5. 返回实例对象
- public S next() {
- if (!hasNext()) {
- throw new NoSuchElementException();
- }
- String cn = nextName;
- nextName = null;
- Class<?> c = null;
- try {
- // 首先根据 nextName,Class.forName 加载拿到具体实现类的 class 对象
- 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 {
- // 将实例对象转换 service.cast 为接口
- S p = service.cast(c.newInstance());
- // 将实例对象放到缓存中, providers.put(cn, p),key 就是实现类的全限定名, value 是实例对象
- providers.put(cn, p);
- return p;
- } catch (Throwable x) {
- fail(service,
- "Provider" + cn + "could not be instantiated:" + x, x);
- }
- throw new Error(); // This cannot happen
- }
总结
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, 独立成工程.
来源: https://juejin.im/entry/5b23ccee51882574e321d522