什么是 SPI?
Dubbo 的源码中大量涉及了 Java SPI设计思想,所以理解 SPI对理解 Dubbo源码有很大帮助。
Java SPI全称 Java Service Provider Interface
,是 Java 提供的一种服务提供者发现机制。其核心功能是通过接口找到其实现类。在实际运用中,主要用在程序启动或者运行时,通过 SPI 机制,加载并装配接口的实现类,实现组件替换和动态扩展。
数据库驱动——SPI
在使用 MySQL/Oracle 数据库时,只需要引入 MySQL驱动 jar包或者 Oracle驱动 jar包就可以了,它的实现关键点就是 DriverManager
:
DriverManager
通过 SPI机制加载不同厂商的驱动;DriverManger
使用厂商的驱动获取连接。
//DriverManager使用SPI加载Driver扩展实现,如com.mysql.jdbc.Driver
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//DriverManager使用加载到的厂商驱动获取连接。
Connection con = aDriver.driver.connect(url, info);
这里 ServiceLoader.load(xxx.class)
实现了 Driver 的加载,它会加载我们引入的 mysql-connector-java.jar
里的 /META-INF/services/java.sql.Driver
文件,该文件里的内容为 com.mysql.jdbc.Driver
。
这样做用什么好处呢?
- JDK 数据库连接操作和驱动的实现彻底解耦;
- 使用方只需要关心自己使用的数据库驱动,如果用 MySQL 的驱动,就不用管 Oracle 的驱动;
- 使用方不需要加载所有的数据库驱动;
- 使用方几乎不需要配置,只是切换数据库驱动。
使用 Java SPI设计协议组件
根据数据库驱动的例子,我们知道了 Java SPI 的基本用法:
- 在 jar 包的META-INF/services下面,创建一个接口全限定名的文件;
- 在接口全限定名文件中,逐行填写具体的实现类;
- 使用方引入 jar 包;
- 使用ServiceLoader.load加载实现类;
- 遍历获取实现类。
假定有这样一个需求:需要实现一个通信软件,它支持 TCP/HTTP 协议,并且使用者能够在两个协议中自由切换,如果只使用 TCP协议,就不需要关注 HTTP协议。
可以按照以下思路实现:
- 抽象协议接口模块,里面包含
IProtocol
协议接口和RequestUtil
对协议调用的封装; - 新建 HTTP 协议实现包,用于实现 HTTP 协议,可以把它想象成 MySQL 的驱动包;
- 新建 TCP 协议实现包,用于实现 TCP 协议,可以把它想象成 Oracle 的驱动包;
- 使用时,只需要引入自己用到的协议包即可。
新建协议接口包
- 新建
IProtocol
接口,用于协议的抽象; - 新建
RequestUtil
,里面使用 SPI 自动加载协议实现类。
/**
* 通讯协议接口
*/
public interface IProtocol {
//发送请求
void sendRequest(String message);
}
// 工具类
public class RequestUtil {
//根据依赖倒置原则,这里依赖的是IProtocol接口,而不是具体的实现类。
private IProtocol protocol;
public void sendRequest(String message){
//根据里氏替换原则,这个protocol可以使用子类HttpProtocol或TcpProtocol替换
protocol.sendRequest(message);
}
//获取RequestUtil实例
private static RequestUtil requestUtil;
public static RequestUtil getInstance(){
if(requestUtil == null){
requestUtil = new RequestUtil();
}
return requestUtil;
}
public RequestUtil() {
//初始化时,使用Java SPI初始化具体的协议实现类
ServiceLoader<IProtocol> protocols = ServiceLoader.load(IProtocol.class);
Iterator<IProtocol> iterator = protocols.iterator();
if(iterator.hasNext()){
protocol = iterator.next();
}
}
public RequestUtil(IProtocol protocol) {
this.protocol = protocol;
}
public IProtocol getProtocol() {
return protocol;
}
public void setProtocol(IProtocol protocol) {
this.protocol = protocol;
}
}
新建 HTTP 协议实现包
- 新建
HttpProtocol
实现类,实现IProtocol
接口; - 新建
resources/META-INF/services/com.yuqiao.deeplearningdubbo.javaspi.protocol.v2.IProtocol
文件,并在文件中填写com.yuqiao.deeplearningdubbo.javaspi.protocol.v2.http.HttpProtocol
项目结构。
/**
* Http通讯协议
*/
public class HttpProtocol implements IProtocol {
@Override
public void sendRequest(String message) {
//示意代码,省略实现细节
System.out.println("使用Http发送消息:" + message);
}
}
同理新建 TCP 协议实现包。
具体使用
- 引入将要使用的协议包(这里以 HTTTP为例)
<dependency>
<groupId>com.yuqiao.deeplearningdubbo</groupId>
<artifactId>java_spi_protocol_v2_http</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 调用 API
/**
* 协议调用方
*/
public class JavaSpiProtocolInvoker {
public static void main(String[] args) {
RequestUtil.getInstance().sendRequest("hello!");
}
}
SPI 实现原理分析
Java SPI
的核心实现类是 ServiceLoader
,先调用 ServiceLoader.load
加载实现类,然后遍历获取实现类。代码如下:
//第一步:调用ServiceLoader.load加载实现类
ServiceLoader<IProtocol> protocols = ServiceLoader.load(IProtocol.class);
//第二步:通过遍历获取实现类
Iterator<IProtocol> iterator = protocols.iterator();
while (iterator.hasNext()){
IProtocol protocol = iterator.next();
System.out.println(protocol);
}
ServiceLoader 类
类的相关核心属性如下:
public final class ServiceLoader<S>
implements Iterable<S>
{
// 实现类的默认文件路径前缀
private static final String PREFIX = "META-INF/services/";
// 被加载的类或者接口(IProtocol)
private final Class<S> service;
// 实现类的类加载器,默认为调用load方法的线程的上下文类加载器
private final ClassLoader loader;
// 访问控制上下文,用于控制类加载器的权限
private final AccessControlContext acc;
// 缓存已经实例化后的实现类
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒迭代器,支持遍历时懒加载
private LazyIterator lookupIterator;
接下来对 ServiceLoader.load()
方法的具体步骤进行源码分析:
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// 这里实例化了一个 ServiceLoader 对象
return new ServiceLoader<>(service, loader);
}
// ServiceLoader 的构造函数
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();
}
// reload 清空缓存的已加载的实现类实例,初始化 LazyIterator 迭代器
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
可以发现上述代码,都只是做了初始化,并没有真正地加载实现类,实现类的加载在后面。
ServiceLoader
类实现了 Iterator 接口,重写了 iterator()
方法实现,代码如下:
public Iterator<S> iterator() {
return new Iterator<S>() {
// knowProviders 缓存已经实例化的实现类
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
// 如果实现类有缓存,直接返回;否则调用 LazyIterator 的 hasNext() 方法
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
// 不存在已加载的实现类,就调用 LazyIterator 的 next() 方法
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
LazyIterator 类
LazyIterator
类的核心属性如下:
private class LazyIterator
implements Iterator<S>
{
//要加载的类或者接口
Class<S> service;
//类加载器,加载文件资源和类
ClassLoader loader;
//存储加载到的文件资源(META-INF/services/接口或类全限定名)
Enumeration<URL> configs = null;
//从文件路径中加载到的实现类全限定名
Iterator<String> pending = null;
String nextName = null;
}
上面提到过,ServiceLoader
类在调用 Iterator()
接口时如果没有发现实现类缓存,就会调用 LazyIterator
的 nextService()
和 hasNextService()
。
hasNextService()
方法通过传入的类加载器加载了 META-INF/services/
文件夹下的文件,文件内容为接口实现类的全限定类名,然后存储到 configs
变量中。
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//全路径名=META-INF/services/ + 类或接口的全限定名
String fullName = PREFIX + service.getName();
//加载所有的全路径名文件
//如:META-INF/services/com.yuqiao.deeplearningdubbo.javaspi.protocol.v2.IProtocol
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;
}
//解析文件中所有的实现类名
//如:com.yuqiao.deeplearningdubbo.javaspi.protocol.v2.tcp.TcpProtocol
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
nextService()
方法根据 hasNextService()
方法获取到的类名来加载类,并进行类的实例化,最后将实例化的对象保存到缓存中。