SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。

SPI的简单示例

当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader

第一步:准备一个简单的服务接口,

1
2
3
4
5
6
7
8
9
public interface SomeSer {

/**
* 一个简单的打印接口
* @param str str
*/
void printSome(String str);

}

第二步:实现这个接口,一般是第三方的jar包实现的这个接口:

1
2
3
4
5
6
public class OneImpl implements SomeSer {
@Override
public void printSome(String str) {
System.out.println("第一个服务的实现:"+ str);
}
}

第三步:在resouces下创建META-INF/services文件夹,并创建一个与接口同名的文件,如:com.abumaster.example.workcommon.spidemo.service.SomeSer,然后,在其中填写刚刚的实现类的全路径,这一步骤通常也是第三方jar包去创建,多个实现,可以填写多个实现类;
第四步:使用,通过ServiceLoader进行加载:

1
2
3
4
5
6
7
8
public class MainUsage {
public static void main(String[] args) {
ServiceLoader<SomeSer> load = ServiceLoader.load(SomeSer.class);
for (SomeSer next : load) {
next.printSome("load调用");
}
}
}

SPI机制的广泛应用

JDBC DriverManager

jdbc的接口定义位于jdk的包java.sql.Driver中,不提供具体的实现,具体的实现由各个数据库来提供,比如常用的postgresql和mysql,需要使用这些数据库的时候则需要引入对应的jar包。
这些jar包的META-INF/services文件夹下,也有对应的实现类,如postgresql的文件内容为:org.postgresql.Driver

如何使用的?
实际使用的时候,使用如下代码来初始化数据库链接:

1
2
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url,username,password);

向下跟踪,找到这个初始化函数:

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
34
35
36
37
38
39
40
41
42
43
44
45
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}

AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//使用SPI的ServiceLoader来加载接口的实现
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});

println("DriverManager.initialize: jdbc.drivers = " + drivers);

if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
  1. 从系统变量中获取有关驱动的定义。
  2. 使用SPI来获取驱动的实现。
  3. 遍历使用SPI获取到的具体实现,实例化各个实现类。
  4. 根据第一步获取到的驱动列表来实例化具体实现类。

SPI和API的区别

SPI - “接口”位于“调用方”所在的“包”中

  • 概念上更依赖调用方。
  • 组织上位于调用方所在的包中。
  • 实现位于独立的包中。
  • 常见的例子是:插件模式的插件。

API - “接口”位于“实现方”所在的“包”中

  • 概念上更接近实现方。
  • 组织上位于实现方所在的包中。
  • 实现和接口在一个包中。

SPI机制的缺陷

  1. 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
  2. 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
  3. 多个并发多线程使用 ServiceLoader 类的实例是不安全的。