0%

SPI

1、什么是SPI?

SPI全称为Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如现在的Dubbo、JDBC中都使用到了SPI机制。为什么要学SPI,因为在SpringBoot的自动装配中其实有使用到SPI机制,所以掌握了这部分对于SpringBoot的学习还是很有帮助的。

​ 总结:SPI就是我定义接口,你去实现这些接口,然后在某个文件里告诉我实现是什么即可。

2、案例

​ 我们Java在进行数据库连接时使用的JDBC,数据库的种类比较多,我们常用的就是MySQL和Oracle,如果没有一个访问数据库的统一接口,那么连接不同的数据库就需要写不同的程序,这样对程序员来说比较繁琐,而JDBC就提供了一些接口,定义了操作数据库的抽象行为,由各个数据库厂商自己实现。

2.1 程序是怎么知道这些接口是由哪些Java类实现的呢?

​ 基于这个点,SPI定义了一个规范,实现者要在META-INF/services下创建接口名的文件,然后在文件中是接口的具体实现类。

2.2 案例介绍

​ 先定义接口项目

​ 然后创建一个扩展的实现,先导入上面接口项目的依赖

1
2
3
4
5
<dependency>
<groupId>com.liyyao</groupId>
<artifactId>MyDataBase</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

​ 然后创建接口的实现

1
2
3
4
5
6
7
8
9
10
/**
* SPI:MySQL对于getUrl的一种实现
*/
public class MySqlConnection implements Connection {
@Override
public String getUrl() {
System.out.println("mysql...");
return null;
}
}

​ 然后在resources目录下创建META-INF/services目录,然后在目录中创建一个文件,名称必须是定义的接口的全类路径名称。然后在文件中写上接口的实现类的全类路径名称。

同样的再创建一个扩展的实现

然后创建一个测试的项目,导入扩展的项目,然后进行测试,测试代码如下

1
2
3
4
5
6
7
8
public static void main(String[] args) {
ServiceLoader<Connection> load = ServiceLoader.load(Connection.class);
Iterator<Connection> iterator = load.iterator();
while (iterator.hasNext()) {
Connection con = iterator.next();
con.getUrl();
}
}

根据不同的导入,执行的逻辑会有不同

3、源码查看

3.1、ServiceLoader

​ 首先来看下ServiceLoader的类结构

1
2
3
4
5
6
7
8
9
10
11
12
// 配置文件的路径
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;

3.2、load

​ load方法创建了一些属性,重要的是实例化了内部类,LazyIterator。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final class ServiceLoader<S> implements Iterable<S> {
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);
}
}

​ 查找实现类和创建实现类的过程,都在LazyIterator完成。当我们调用iterator.hasNext和iterator.next方法时,实际上调用的都是LazyIterator的相应方法。

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
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;

private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
//META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
String fullName = PREFIX + service.getName();
//将文件路径转成URL对象
configs = loader.getResources(fullName);
}
while ((pending == null) || !pending.hasNext()) {
//解析URL文件对象,读取内容,最后返回
pending = parse(service, configs.nextElement());
}
//拿到第一个实现类的类名
nextName = pending.next();
return true;
}
}

​ 创建实例对象,当然,调用next方法时,实际调用到的是lookupIterator.nextService。它通过反射的方式,创建实现类的实例并返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private class LazyIterator implements Iterator<S> {
private S nextService() {
//全限定类名
String cn = nextName;
nextName = null;
//创建类的Class对象
Class<?> c = Class.forName(cn, false, loader);
//通过newInstance实例化
S p = service.cast(c.newInstance());
//放入集合,返回实例
providers.put(cn, p);
return p;
}
}

​ 看到这儿应该很清楚了,获取到类的实例,然后就可以做事情了。

-------------本文结束感谢您的阅读-------------

本文标题:SPI

文章作者:

发布时间:2023年02月04日 - 11:02

最后更新:2023年02月04日 - 17:02

原始链接:http://www.liyyao.com/202302041119.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。