Java中的SPI机制以及应用场景

SPI 全称是 Service Provider Interface,是 JDK1.5 新增的一个功能,允许不同的服务提供者去实现某个规定的接口,而且将具体的实现完全提供给使用方,允许使用方按需加载服务提供方的一些功能。

前言

提到 SPI,就不得不提下 API,以 dubbo 为例,服务提供方对外提供一系列 API,而使用方是不用关心服务提供方是如何实现具体的业务逻辑,只需要通过 RPC 调用远程服务即可。

API

这样的好处就是 client 端不用关心服务端的具体逻辑,方便服务的水平扩展以及解耦。

SPI

上面提到了 API 的相关知识,而 SPI 则是由服务方将具体实现提供给调用方,如何使用完全取决于调用方的具体业务逻辑,即调用方是可以拿到服务方的具体实现逻辑,然后决定是否使用,这有点像 Spring 的控制反转
SPI

实现该功能由如下两种方式:

  1. 直接在代码中硬编码
  2. 通过SPI来实现

第三方 jar

我们日常用 maven 将第三方 jar 导入到我们的项目中,大致思想和这个类似,都是将具体实现引入到 client,由 client 来决定使用的方式。
但是通过 maven 导入的有一个缺点,就是代码中存在硬编码,即需要 import 第三方包的类全路径,以后如果要替换具体实现,那么所有 import 了旧包的地方就需要全部修改一遍。

例如现在有一个接口如下:

1
2
3
public interface Buy {
Boolean buy(Integer var1);
}

而提供方则在自己的项目中实现了具体的逻辑:

1
2
3
4
5
6
public class AliPayBuy implements Buy {
public Boolean buy(Integer num) {
System.out.println("buy" + num + "with AliPayBuy");
return true;
}
}

下面就来看看 maven 和 SPI 是如何具体实现的。

首先项目的整体结构如下,然后分别 deploy 到自己的私服中

普通使用

如果不想通过 SPI 来调用,那么直接在项目中 new 一个也是可以的。

1
2
3
4
5
6
public class SimpleTest {
public static void main(String[] args) {
Buy buy = new AliPayBuy();
System.out.println(buy.buy(1));
}
}

这种硬编码会导致后续完全没有扩展性,以后如果需要将支付改为其他方式,那么所有涉及到 AliPayBuy 的地方全部都得替换。

SPI

而 JDK 的作者为了解决这个问题,引入了 SPI 机制,具体来说就是定义了一个文件夹「META-INF/services」,调用方规定一个接口,
提供方则在自己的项目中实现具体的逻辑,然后在自己的项目中将具体实现放置在「META-INF/services」即可。

1
2
3
4
5
6
7
8
public class SPITest {
public static void main(String[] args) {
ServiceLoader<Buy> shouts = ServiceLoader.load(Buy.class);
for (Buy s : shouts) {
System.out.println(s.buy(1));
}
}
}

而采用 SPI 机制,可以避免在代码中直接引入第三方 jar,ServiceLoader.load 加载的正是之前 deploy 进私服的 alipay jar 包。

META-INF/services

JDK 中规定,只有在这个文件夹中的 SPI 才会被加载,因为 JDK 已经将该路径硬编码到代码中了,而文件的命令也很有规范。

文件名称必须是接口的全路径名称(大小写也必须一致)
而文件里面的内容就是实现该接口的类的全路径名称,例如 xyz.somersames.Buy 这个里面的内容就是

1
xyz.somersames.AliPayBuy

双亲委派机制

通过 SPI 机制加载的类是会破坏双亲委派机制的,因为按照双亲委派的机制,当 classLoader 加载某一个类的时候是一层一层往上递增的,然后再逐级往下,但是 SPI 机制是通过 thread.contextClassLoader 直接加载了具体的实现类。
虽然说原生的 classLoader 是按照双亲委派机制在加载类,但是 thread.contextClassLoader 这里由于极大的灵活性可能会导致被用户的自定义 classLoader 覆盖,而如果用户自定义的 classLoader 不按照规范来,那么就直接破坏了双亲委派机制了

其二,因为 JDK 的 SPI 接口一般是位于 rt.jar 中,按照双亲委派机制,应该由 BootstrapClassLoader 加载,但是其实现类却是位于 classPath,由 AppClassLoader 加载,所以这也算是破坏的一种

作者

Somersames

发布于

2021-08-04

更新于

2021-08-06

许可协议

评论