深入理解ServiceLoader类与SPI机制

最近我们自己在重构项目,系统为了符合82原则(希望是80%的业务能通过穷举的方式固定下来,只有20%的允许特殊的定义),那么在固定一些标准流程以后,比如我们放大了原子服务的能力,当放大原子服务能力的时候,你就会发现,虽然抽象上看做的事情是一个意思,但是到实际去实现的时候发现还是各不相同。

在这里为了解决一个实现不同,但流程相同的问题,以及团队协作上的问题。我们引入的SPI (Service Provider Interface)

使用案例

通常情况下,使用ServiceLoader来实现SPI机制。 SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制, 比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。

SPI机制可以归纳为如下的图:

http://static.cyblogs.com/20191111203102234.png

如果大家看过源代码或者说看过一些博客文章大概都清楚,在一些开源项目中大量的使用了SPI的方式,比如:mysql-connector-javadubbo等。

我们大概看眼MySQL的一个SPI实现

http://static.cyblogs.com/WechatIMG450.png

JDBC中的接口即为:java.sql.Driver

SPI机制的实现核心类为:java.util.ServiceLoader

Provider则为:com.mysql.jdbc.Driver

1
2
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

简单写个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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.vernon.test.spi;

/**
* Created with vernon-test
*
* @description:
* @author: chenyuan
* @date: 2020/4/2
* @time: 11:08 上午
*/
public interface IRepository {
void save(String data);
}

package com.vernon.test.spi.impl;

import com.vernon.test.spi.IRepository;

/**
* Created with vernon-test
*
* @description:
* @author: chenyuan
* @date: 2020/4/2
* @time: 11:09 上午
*/
public class MongoRepository implements IRepository {
@Override
public void save(String data) {
System.out.println("Save " + data + " to Mongo");
}
}

package com.vernon.test.spi.impl;

import com.vernon.test.spi.IRepository;

/**
* Created with vernon-test
*
* @description:
* @author: chenyuan
* @date: 2020/4/2
* @time: 11:08 上午
*/
public class MysqlRepository implements IRepository {

@Override
public void save(String data) {
System.out.println("Save " + data + " to Mysql");
}

}

调用函数定义:

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
package com.vernon.test.spi;

import java.util.Iterator;
import java.util.ServiceLoader;

/**
* Created with vernon-test
*
* @description:
* @author: chenyuan
* @date: 2020/4/2
* @time: 11:12 上午
*/
public class SPIMain {

public static void main(String[] args) {
ServiceLoader<IRepository> serviceLoader = ServiceLoader.load(IRepository.class);
Iterator<IRepository> it = serviceLoader.iterator();
while (it != null && it.hasNext()) {
IRepository demoService = it.next();
System.out.println("class:" + demoService.getClass().getName());
demoService.save("tom");
}
}

}

执行结果:

1
2
3
4
5
6
Connected to the target VM, address: '127.0.0.1:58517', transport: 'socket'
class:com.vernon.test.spi.impl.MongoRepository
Save tom to Mongo
class:com.vernon.test.spi.impl.MysqlRepository
Save tom to Mysql
Disconnected from the target VM, address: '127.0.0.1:58517', transport: 'socket'

ServiceLoader类的内部实现逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}

public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}

SercviceLoader的初始化跑完如上代码就结束了。但是实际上联系待实现接口和实现接口的类之间的关系并不只是在构造ServiceLoader类的过程中完成的,而是在迭代器的方法hasNext()中实现的。

动态调用的实现

在使用案例中写的forEach语句内部逻辑就是迭代器,迭代器的重要方法就是hasNext()

ServiceLoader是一个实现了接口Iterable接口的类。

hasNext()方法的源代码:

1
2
3
4
5
6
7
8
9
10
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}

抛出复杂的确保安全的操作,可以将上述代码看作就是调用了方法:hasNextService.

hasNextService()方法的源代码:

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

上述代码中比较重要的代码块是:

1
2
3
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);

此处PREFIX(前缀)是一个常量字符串(用于规定配置文件放置的目录,使用相对路径,说明其上层目录为以项目名为名的文件夹):

1
private static final String PREFIX = "META-INF/services/";

那么fullName会被赋值为:META-INF/services/com.vernon.test.spi.IRepository

然后调用方法getSystemResourcesgetResources将fullName参数视作为URL,返回配置文件的URL集合 。

1
pending = parse(service, configs.nextElement());

parse方法是凭借 参数1:接口的Class对象 和 参数2:配置文件的URL来解析配置文件,返回值是含有配置文件里面的内容,也就是实现类的全名(包名+类名)字符串的迭代器;

最后调用下面的代码,得到下面要加载的类的完成类路径字符串,相对路径。在使用案例中,此值就可以为:

1
2
com.vernon.test.spi.impl.MongoRepository
com.vernon.test.spi.impl.MysqlRepository

这仅仅是迭代器判断是否还有下一个迭代元素的方法,而获取每轮迭代元素的方法为:nextService()方法。

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
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}

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
}

总结

1、SPI的理念:通过动态加载机制实现面向接口编程,提高了框架和底层实现的分离;
2、ServiceLoader 类提供的 SPI 实现方法只能通过遍历迭代的方法实现获得Provider的实例对象,如果要注册了多个接口的实现类,那么显得效率不高;
3、虽然通过静态方法返回,但是每一次Service.load方法的调用都会产生一个ServiceLoader实例,不属于单例设计模式;
4、ServiceLoaderClassLoader是类似的,都可以负责一定的类加载工作,但是前者只是单纯地加载特定的类,即要求实现了Service接口的特定实现类;而后者几乎是可以加载所有Java类;
5、对于SPi机制的理解有两个要点:

  • 理解动态加载的过程,知道配置文件是如何被利用,最终找到相关路径下的类文件,并加载的;
  • 理解 SPI 的设计模式:接口框架 和底层实现代码分离;

6、之所以将ServiceLoader类内部的迭代器对象称为LazyInterator,是因为在ServiceLoader对象创建完毕时,迭代器内部并没有相关元素引用,只有真正迭代的时候,才会去解析、加载、最终返回相关类(迭代的元素);

参考地址

如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员简栈文化-小助手(lastpass4u),他会拉你们进群。

简栈文化服务订阅号