Java SPI 代码示例

Java Service Provider Interface 是JDK自带的服务提供者接口又叫服务发现机制更是一种面向接口的设计思想。即JDK本身提供接口类, 第三方实现其接口,并作为jar包或其他方式注入到其中, 在运行时会被JDK ServiceLoader 发现并加载,自动调用实现类的方法。

1. 在本地测试SPI机制

本人使用类似日志记录组件, 项目一提供接口,由第三方(项目二)提供自定义日志实现类。tips: 一般影响力大的机构或组织才可以定义SPI接口 比如Java , SLF4J, Spring ~

JDK: Java21

  1. 在原有项目一(sample)创建测试类,以及接口文件
  2. 新建项目二(spitest) 提供多个实现类
  3. 最重要的:在spitestMETA-INF/services 文件夹下增加配置文件, 命名为 sample中接口类的全路径限定名
  4. 执行测试类,查看结果

1.1 项目一 测试类及接口

1
2
3
4
5
6
7
8
9
import java.util.ServiceLoader;

public class JdkSpi {
public static void main(String[] args) {
ServiceLoader<LoggerForSpiTest> spiTests = ServiceLoader.load(LoggerForSpiTest.class);
spiTests.forEach( logger -> System.out.println(logger.log("hello ")));
}
}

接口:

1
2
3
4
public interface LoggerForSpiTest {
String log (String msg);
}

2.1 项目二 实现类

由于实现类需要引入此接口类LoggerForSpiTest, 因此打包时候可以去除掉接口类即可

实现类LoggerA

1
2
3
4
5
6
7
8
9
import com.jay.base.LoggerForSpiTest;

public class LoggerA implements LoggerForSpiTest {
@Override
public String log(String msg) {
return msg + "i am LoggerA";
}
}

实现类LoggerB

1
2
3
4
5
6
7
8
9
import com.jay.base.LoggerForSpiTest;

public class LoggerB implements LoggerForSpiTest {
@Override
public String log(String msg) {
return msg + "i am LoggerB";
}
}

3.1 项目二中添加配置文件

1
2
com.jay.spi.LoggerA
com.jay.spi.LoggerB

其中内容是 实现类的路径限定名, 由于我实现了两个Logger为了区分效果,实际可能只需要一个,例如mysql jdbc connector jar包中


其中jdk 提供了Driver接口类, 让第三方如 Mysql, Oracle, SqlServer 等实现其厂家自己的实现类.

4.1 打包项目二spitest为jar,导入到项目一sample

打包只用包含 spitest compile output即可,即 只需要spitest中自己写的代码的编译文件打成jar

导入成功后,打开jar只能看到三个文件即 LoggerA, LoggerB, 以及META-INF/services里面的配置文件

4.2 运行测试类JdkSpi.java

输出结果为:

hello i am LoggerA
hello i am LoggerB

2. FYI.

类似于我们使用的日志打印组件,代码会在ServiceLoader.load(LoggerForSpiTest.class) 方法中构造ServiceLoader对象, 而其是继承了Iterable接口,在代码spiTests.forEach( logger -> System.out.println(logger.log("hello "))); 中迭代去使用的时候去加载META-INF/services下的所有实现类 LoggerA, LoggerB至此本文不再扩展,会再次以新文章介绍。
`

1
2
3
4
5
@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader(); //获取当前类加载器
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl); // 构造ServiceLoader(调用类, 接口类, 类加载器)
}

关于Reflection.getCallerClass()看到源码注释是会跳过所有反射的过程直达调用的地方,类似编码过程中发现报错我们可以通过一些IDE看到stack info 并看到最初调用的地方排查问题。由于本文不介绍反射,所以不宜继续扩展。