概述

SPI全称叫做 service provider interface,是Jdk提供的供第三方的用来扩展已有服务的一种方案,通过这种机制我们将接口和实现分开, 并可以在外部灵活的装配实现,这样说可能有点拗口,简单点说就是在面向接口编程的过程中提供了注入具体实现的策略。其核心类是ServiceLoader ,该类会去默认指定的路径下加载对应的实现,并通过反射的方式来生成实现类的对象(多说一句,既然是通过反射的方式来生成对应的实现类的对象,那么 可定是要求实现类提供一个默认的构造器,接下来可以验证以下)

代码及流程演示

定义接口及实现

接口如下

1
2
3
4
public interface Person {

void say();
}

我们接下来定义两个实现,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Teacher implements Person {
@Override
public void say() {
System.out.println("i'm a teacher !");
}
}


public class Student implements Person {

@Override
public void say() {
System.out.println("i'm a student");
}
}

实现发现

对于服务的发现我们需要在classpath下新建一个目录,路径为:META-INF/services,并在该路径下新建一个接口的全限定名文件, 然后将该接口对应的实现类的全限定名写入,如下:

验证结果

接下来我们就可以通过测试来验证SPI机制了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.ServiceLoader;

public class SPITest {

public static void main(String[] args) {
ServiceLoader<Person> peoples = ServiceLoader.load(Person.class);

for (Person p : peoples) {
p.say();
}
}
}

上面我们说过serviceLoader在完成类的加载之后,会通过反射的方式来生成实现类对应的对象,因此需要一个默认的构造器, 我们可以验证一下,如下给Student类提供一个含参的构造器:

1
2
3
4
5
6
7
8
9
10
public class Student implements Person {

private int name;

public Student(int name) {
this.name = name;
}

、、、、
}

接下来我们再测试一下就会报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Exception in thread "main" java.util.ServiceConfigurationError: com.h3c.Person: Provider com.h3c.Student could not be instantiated
at java.util.ServiceLoader.fail(ServiceLoader.java:232)
at java.util.ServiceLoader.access$100(ServiceLoader.java:185)
at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:384)
at java.util.ServiceLoader$LazyIterator.next(ServiceLoader.java:404)
at java.util.ServiceLoader$1.next(ServiceLoader.java:480)
at com.h3c.SPITest.main(SPITest.java:10)
Caused by: java.lang.InstantiationException: com.h3c.Student
at java.lang.Class.newInstance(Class.java:427)
at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:380)
... 3 more
Caused by: java.lang.NoSuchMethodException: com.h3c.Student.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.newInstance(Class.java:412)
... 4 more

因此这也验证了我们上面的猜想是正确的。

原理说明

我们在调用 ServiceLoader.load()方法的时候,会构建出一个新的serviceLoader对象,而该对象内部封装了一个内部类LazyIterator,通过 类的名称我们可以知道这个类为一个迭代器,而且应该具备lazy的机制,也就是在我们生成serviceloader对象的时候,并不会为我们提供的实现类生成对象, 而只有在遍历的时候才会初始化,核心代码如下:

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
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;
}


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 {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}

这里PREFIX就是我们存放接口全路径的文件夹的地址,通过这段代码我们可以知道,在遍历的时候我们会访问指定路径下的文件,并通过反射的方式生成对象 ,然后将类和对应的对象缓存起来。

使用场景

数据库驱动的加载,class.forName(“com.mysql.jdbc.Driver”),待完善

总结

使用serviceLoader可以将服务的装配与调用分离,实现业务的解偶,不过多线程的情况下是线程不安全的。 参考链接:https://www.cnkirito.moe/spi/