Fork me on GitHub

Java SPI

1. SPI是什么?

最近在看 Spring 和 Dubbo 的源码,发现这两个框架都有自己的 SPI 机制,决定了解下 SPI 。

SPI (Service Provider Interface)其实是一种思想,但凡涉及到思想的东西,都是看一眼感觉自己会,看代码也看得懂,然后自己就是不会用,或者用的不是那么顺手(参考设计模式)。

2. 写个HelloWorld

首先定义一个接口,两个实现类。

代码如下:

1
2
3
4
5
package com.xiaofa.common.spi;

public interface SpiService {
void hello();
}
1
2
3
4
5
6
7
8
package com.xiaofa.common.spi;

public class SpiServiceOne implements SpiService {

@Override
public void hello() {
System.out.println("I am service one");
}
1
2
3
4
5
6
7
8
9
package com.xiaofa.common.spi;

public class SpiServiceTwo implements SpiService {

@Override
public void hello() {
System.out.println("I am service two");
}
}

在 resources 目录下新建 META-INF/services ,里面新建一个文件,com.xiaofa.common.spi.SpiService (接口的全路径)。截图如下:

文件的内容如下:

com.xiaofa.common.spi.SpiServiceOne

内容是实现类的全路径。

写个单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.xiaofa.common;

import com.xiaofa.common.spi.SpiService;
import org.junit.Test;

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

public class SpiTest {

@Test
public void spiTest() {
ServiceLoader<SpiService> spiServices = ServiceLoader.load(SpiService.class);
Iterator<SpiService> iterator = spiServices.iterator();
while (iterator.hasNext()) {
SpiService next = iterator.next();
next.hello();
}
}

}

执行结果如下,调用了 SpiServiceOne 的方法。

I am service one

如果在 com.xiaofa.common.spi.SpiService 文件中写入的是两个实现类,那么打印结果就是两行了。目前我们已经知道 SPI 的简单用法了,那原理是什么呢?

原理:ServiceLoader.load(Search.class) 方法在加载某接口时,会去 META-INF/services 下找接口的全限定名文件,再根据里面的内容加载相应的实现类。

为什么是在 META-INF/services 下呢?我们看下 ServiceLoader 类的源码。

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

可以发现,查找实现类的路径前缀就是 META-INF/services/ 。完整路径是前缀 + 接口名。

1
String fullName = PREFIX + service.getName();

3. 源码中的使用

还记得我们最开始学习 JDBC 时写的代码吗?大致如下吧。

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
public class JdbcTest {

public static void main(String[] args) throws Exception {

String URL = "jdbc:mysql://192.168.1.1:3306/xiaofa?useUnicode=true&characterEncoding=utf-8&useSSL=false";
String USER = "root";
String PASSWORD = "";
// 1.不需要手动加载驱动了
Class.forName("com.mysql.jdbc.Driver");
// 2.获得数据库链接
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
// 3.通过数据库的连接操作数据库,实现增删改查(使用Statement类)
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("select * from a");
// 4.处理数据库的返回结果(使用ResultSet类)
while (rs.next()) {
System.out.println(rs.getString("id"));
}
//关闭资源
rs.close();
st.close();
conn.close();

}
}

第一步就是加载驱动,但是在某个 JDK 版本之后(具体哪个版本不清楚),就不再需要手动加载驱动了。有兴趣可以自行测试下。为什么呢?

结合之前讲的 SPI 机制很容易猜想到,JDBC 是不是也采用了这种机制。我们来看看 DriverManager 的源码

1
2
3
4
5
6
7
8
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

先看方法的注释,翻译一下:

通过校验系统配置文件 jdbc.properties 加载初始化 JDBC 驱动,然后使用 ServiceLoader 机制。而 ServiceLoader 就是 SPI 机制的实现类。

我们再看看 loadInitialDrivers 的核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// If the driver is packaged as a Service Provider, load it. 如果驱动以服务提供的方式打包好了,就加载它;
// Get all the drivers through the classloader 通过类加载器获取所有驱动;
// exposed as a java.sql.Driver.class service. 以驱动的形式向外暴露;
// ServiceLoader.load() replaces the sun.misc.Providers() 服务加载;
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 看我 看我 看我 是不是很熟悉啊
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;
}
});

可以看出,这个方法主要是通过 ServiceLoader 加载驱动,之后遍历所有驱动,挨个注入。看一看 mysql-connector-java:8.0.15 包中 META-INF/services 下确实有文件名 java.sql.Driver 的文件,内容是 com.mysql.cj.jdbc.Driver。

下面看看真正注入的方法,调用 ServiceLoader 的 next 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public Iterator<S> iterator() {
return new Iterator<S>() {

Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();

public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
// 1
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}

public void remove() {
throw new UnsupportedOperationException();
}

};
}
1
2
3
4
5
6
7
8
9
10
11
12
// 2
public S next() {
if (acc == null) {
// 3
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
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
// 3
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、2、3 所示。最终我们发现,还是执行了 Class.forName 方法。通过 SPI 的方式把手动加载变成框架自动处理。

4. SPI和API

待续。。。

彩蛋:后续应该还会写 Spring SPI、Dubbo SPI 相关的文章。

参考资料:

https://www.cnblogs.com/xrq730/p/11440174.html

本文标题:Java SPI

原始链接:https://zhaoxiaofa.com/2019/01/08/Java-SPI/

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