Java-SPI机制

SPI简介

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件.
SPI其实就是 基于接口的编程 + 策略模式 + 配置文件 组合实现的动态加载机制.
Java SPI就是提供一个机制,通过为某个接口寻找服务的一个实现机制.通过读取你在配置文件中配置的参数,来为你提供相应的服务.核心思想就是解耦.将程序的装配权移动到程序之外,也就是配置文件中.

使用场景

就是调用这可以根据实际需要,启用,扩展和替换一些实现的策略.
举个例子:

  • 数据库驱动加载接口实现类的加载.jdbc通过配置不同,实现不同的数据库驱动的加载
  • 日志门面接口实现类加载 .SLF4J加载不同提供商的日志实现类

使用介绍:

首先定义一个接口与它的多个实现类:

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
package com.spj.spi;
public interface Robot {
void sayHello();
}
public class RobotBoy implements Robot {

@Override
public void sayHello() {
System.out.println("hello,I am a boy");
}
}
public class RobotGirl implements Robot {


@Override
public void sayHello() {
System.out.println("hello,I am a girl");
}
}
~~~
其次在 src/main/resources 目录下建立 META-INF/services 目录.新建一个以此接口命名的文件,我这里是com.spj.spi.Robot,然后在想文件中写入它的两个实现类.实现类必须要写类的全限定名称. 借着来用spi测试一下:
~~~java
public class JavaSpiTest {
public static void main(String[] args) throws IOException {
ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
Iterator<Robot> matchter = serviceLoader.iterator();
while (matchter.hasNext()){
Robot robot = matchter.next();
robot.sayHello();
}

}
}
控制台输出:
hello,I am a boy
hello,I am a girl

这里会将两个实现类加载进来. 通过ServiceLoader来进行接口服务的发现与加载.

具体实现:

首先看ServiceLoader的具体属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public final class ServiceLoader<S>
implements Iterable<S>
{
// 看到这个前缀表明了你必须得将配置文件放在这个目录下
private static final String PREFIX = "META-INF/services/";

// 这个就是上面要寻找的服务的接口
private final Class<S> service;

// 用来加载类
private final ClassLoader loader;

// 访问控制器
private final AccessControlContext acc;

// 用来缓存加载成功的类
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// 迭代器
private LazyIterator lookupIterator;
}

具体加载过程:
在查找下一个实现类的时候,首先会看缓存providers中是否有,如果没有,则进行加载配置文件中的类.实现如下:

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
46
47
48
49
50
51
52
53
54
55

private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 通过名称加载类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
// 获取到之后,将此类缓存到providers中.
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
}

// 判断还有没有下一个.
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
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;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}

总结

Java SPI机制主要是为了解耦,通过将具体实现与应用程序分开,方便管理,在需要配置不同的实现的情况下,不需要进行代码改动,而只需要改变配置就可以了.
当然,Java SPI机制也有一些缺点,虽然其中确实有延迟加载,但是如果想要获得某个实现类的话就只能遍历获取,将每个实现类多遍历一遍,就造成了浪费.