SPI 概述

SPI 全称为(Service Provider Interface),是 JDK 内置的一种服务发现机制。它可以动态的为某个接口寻找服务实现,有点类似 IOC(Inversion of Control)的思想,将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

使用 SPI 机制需要在 classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。

以 JavaMail 程序为例,协议层只需要定义好邮件发送接口,业务层实现对应的协议如 SMTP, IMAP, POP 就可以了。

JavaMail API

SPI 实例说明

接下来通过一个具体的例子来讲解 SPI 的用法,例子的代码在 github 上可以看到。

例子代码在IDEA中的结构如图:

java-spi-example

(1)首先我们提供一个接口类 IOperation 以及它的两个实现类 PlusOperationImplDivisionOperationImpl,都在 com.zhoukaibo.spi 这个包路径下。

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

public interface IOperation {

int operation(int numberA, int numberB);
}

public class PlusOperationImpl implements IOperation {

public PlusOperationImpl() {
System.out.println("plusOperation construct");
}

public int operation(int numberA, int numberB) {
System.out.println("Operation: plus");

return numberA + numberB;
}
}

public class DivisionOperationImpl implements IOperation {

public DivisionOperationImpl() {
System.out.println("division construct");
}

@Override
public int operation(int numberA, int numberB) {
System.out.println("Operation: division");
return numberA / numberB;
}
}

(2)接着在 classpath 下创建文件夹 META-INF/services,在文件夹中新建一个文件 com.zhoukaibo.spi.IOperation。并在文件中写入具体的实现类:com.zhoukaibo.spi.PlusOperationImplcom.zhoukaibo.spi.DivisionOperationImpl

(3)最后,我们就可以利用 ServiceLoader 进行服务发现了:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
ServiceLoader<IOperation> operations = ServiceLoader.load(IOperation.class);

int numberA = 6;
int numberB = 3;
System.out.println("NumberA: " + numberA + ", NumberB: " + numberB);
Iterator<IOperation> iterator = operations.iterator();
while (iterator.hasNext()) {
IOperation operation = iterator.next();
System.out.println(operation.operation(numberA, numberB));
}
}

(4)程序输出:

1
2
3
4
5
6
7
NumberA: 6, NumberB: 3
division construct
Operation: division
2
plusOperation construct
Operation: plus
9

SPI 实现原理

  1. 应用程序调用 ServiceLoader.load 方法

ServiceLoader.load 方法内先创建一个新的ServiceLoader,并实例化该类中的成员变数,包括:

  • ClassLoader loader(类载入器)
  • AccessControlContext acc(访问控制器)
  • LinkedHashMap<String, S> providers(用于缓存载入成功的类)
  • LazyIterator lookupIterator(实现迭代器功能)
  1. 应用程序通过迭代器获取对象实例

ServiceLoader 先判断成员变量 providers 对象中否有缓存实例对象,如果有缓存,直接返回。

如果没有缓存,执行类的装载:读取 META-INF/services/ 下的配置文件,获得所有能被实例化的类的名称,通过反射方法 Class.forName() 载入类对象,并用 instance() 方法将类实例化。把实例化后的类缓存到providers 对象中然后返回实例对象。

总结

JDK 内置的 SPI 机制本身有它的优点,但由于实现比较简单,也有不少缺点。

优点

使用 Java SPI 机制的优势是实现解耦,使得接口的定义与具体业务实现分离,而不是耦合在一起。应用程序可以根据实际业务情况启用或替换具体组件。

缺点

  • 不能按需加载。虽然 ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
  • 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
  • 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
  • 加载不到实现类时抛出并不是真正原因的异常,错误很难定位。

鉴于 SPI 的诸多缺点,很多系统都是自己实现了一套类加载机制,例如 dubbo。用户也可以自定义classloader+反射机制来加载,实现并不复杂。此外开源的类加载解决方案有 Plugin Framework for Java (PF4J) 等。

附录