Spi的基本使用

概述

  • 什么是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
2
3
4
5
6

package com.example;

public interface IDisplay {
String display();
}

在主工程和bdisplay 模块中的实现该接口

创建spi描述文件

在工程的main目录下新建目录resources/META-INF/services,以服务接口名为文件名新建spi描述文件,内容为具体的服务实现类权限定名,可以有多个

文件结构如下

文件结构

加载不同服务

通过ServiceLoader来加载接口的不同实现类,然后会得到迭代器,在迭代器中可以拿到不同实现类全限定名,然后通过反射动态加载实例就可以调用display方法了。

1
2
3
4
5
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

ServiceLoader

先看下几个重要的成员变量

  • PREFIX就是配置文件所在的包目录路径;
  • service就是接口名称,在我们这个例子中就是Display;
  • loader就是类加载器,其实最终都是通过反射加载实例;
  • providers就是不同实现类的缓存,key就是实现类的全限定名,value就是实现类的实例
  • lookupIterator就是内部类LazyIterator的实例。
1
2
3
4
5
6
7
8
9
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会自动为我们获得一个当前线程的类加载器,最终都是调用构造函数。

1
2
3
4
5
6
7
8
9
10

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

1
2
3
4
5
6
7
8
9
10

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中查找
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

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()
  1. 首先拿到配置文件名fullName,我们这个例子中是com.example.Display
  2. 通过类加载器获得所有模块的配置文件Enumeration configs configs
  3. 依次扫描每个配置文件的内容,返回配置文件内容Iterator pending,每个配置文件中可能有多个实现类的全限定名,所以pending也是个迭代器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

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)可以查阅

  • next()

在上面hasNext()方法中拿到的nextName就是实现类的全限定名,接下来我们去看看具体实例化工作的地方next():

  • 1.首先根据nextName,Class.forName加载拿到具体实现类的class对象
  • 2.Class.newInstance()实例化拿到具体实现类的实例对象
  • 3.将实例对象转换service.cast为接口
  • 4.将实例对象放到缓存中,providers.put(cn, p),key就是实现类的全限定名,value是实例对象。
  • 5.返回实例对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

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拿到这些实现类的集合,统一处理。

  • 缺点
  1. Java中SPI是随jar发布的,每个不同的jar都可以包含一系列的SPI配置,而Android平台上,应用在构建的时候最终会将所有的jar合并,这样很容易造成相同的SPI冲突,常见的问题是DuplicatedZipEntryException异常
  2. 读取SPI配置信息是在运行时从jar包中读取,由于apk是签过名的,在从jar中读取的时候,签名校验的耗时问题会造成性能损失

后续可以改进的点

Java中使用ServiceLoader去读取SPI配置信息是在程序运行时,我们可以将这个读取配置信息提前,在编译时候就搞定,通过gradle插件,去扫描class文件,找到具体的服务类(可以通过标注来确定),然后生成新的java文件,这个文件中包含了具体的实现类。这样程序在运行时,就已经知道了所有的具体服务类,缺点就是编译时间会加长,自己需要重新写一套读取SPI信息、生成java文件等逻辑。

经过优化后,SPI已经偏离了原本的初衷,但是可以做更多的事,可以将业务服务分离,通过SPI找到业务服务入口,业务组件化,抽成单独的aar,独立成工程。

0%