一. 什么是SPI?
搞清楚这个概念相对不难,SPI全称是:Service provider interface ,翻译成中文就是:服务提供发现接口。这里的服务发现和我们常听到的微服务中的服务发现并不相同。
Java SPI提供这样了这样一个机制:为某个接口寻找服务实现的机制。这有点类似IOC的思想,将装配的控制权移到了程序之外。
说了这么多,我只有一个问题,SPI到底是什么?
SPI其实是一种思想,一种面向接口编程的思想;
分析一下这张图:
- 先看看接口属于实现方的情况,这个很容易理解,实现方提供了接口和实现,我们可以引用接口来达到调用某实现类的功能,这就是我们经常说的api,它具有以下特征:
- 概念上更接近实现方;
- 组织上位于实现方所在的包中;
- 实现和接口在一个包中;
- 当接口属于调用方时,我们就将其称为spi,全称为:service provider interface,它具有以下特征:
- 概念上更依赖调用方;
- 组织上位于调用方所在的包中;
- 实现位于独立的包中(也可认为在提供方中);
如图所示:
二. SPI怎么实现?
先放一张SPI实现的脑图
-
创建一个接口和它的实现类:
/* * bq.com * Copyright (C) 2018-2020 All Rights Reserved. */ package com.example.spitest; /** * @author liuyuan * @version SpiTestService.java, v 0.1 2020-08-14 17:07 */ public interface SpiTestService { void sayHello(); } /* * bq.com * Copyright (C) 2018-2020 All Rights Reserved. */ package com.example.spitest.impl; import com.example.spitest.SpiTestService; /** * @author liuyuan * @version SpiTestServiceImpl.java, v 0.1 2020-08-14 17:07 */ public class SpiTest01ServiceImpl implements SpiTestService { @Override public void sayHello() { System.out.println("hello spi SpiTest01ServiceImpl >>> 华为初始化..."); } } /* * bq.com * Copyright (C) 2018-2020 All Rights Reserved. */ package com.example.spitest.impl; import com.example.spitest.SpiTestService; /** * @author liuyuan * @version SpiTest02ServiceImpl.java, v 0.1 2020-08-14 17:08 */ public class SpiTest02ServiceImpl implements SpiTestService { @Override public void sayHello() { System.out.println("hello spi SpiTest02ServiceImpl >>> 小米初始化..."); } }
-
在特定位置创建一个文件夹,文件夹下创建一个以接口全路径类名命名的文件:
- src -main -resources - META-INF - services - com.example.spitest.SpiTestService
-
配置文件中指定实现类:
com.example.spitest.impl.SpiTest01ServiceImpl com.example.spitest.impl.SpiTest02ServiceImpl
-
加载配置文件中指定的实现;
/* * bq.com * Copyright (C) 2018-2020 All Rights Reserved. */ package com.example.spitest; import com.example.spitest.impl.SpiTest02ServiceImpl; import org.springframework.stereotype.Service; import java.util.Optional; import java.util.ServiceLoader; import java.util.stream.StreamSupport; /** * @author liuyuan * @version SpiService.java, v 0.1 2020-08-14 17:16 */ @Service public class SpiService { public SpiTestService test() { // 使用 ServiceLoader 来加载配置文件中指定的实现(核心) ServiceLoader<SpiTestService> spiTestServices = ServiceLoader.load(SpiTestService.class); // 取第一个实现 final Optional<SpiTestService> spiTestService = StreamSupport.stream(spiTestServices.spliterator(), false) .findFirst(); // 如果实现累不存在,手动创建一个 return spiTestService.orElse(new SpiTest02ServiceImpl()); } }
-
测试:
package com.example.spitest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class SpiTestApplicationTests { @Autowired private SpiService spiService; @Test void spiTest() { SpiTestService test = spiService.test(); // 结果:hello spi SpiTest01ServiceImpl >>> 华为初始化... test.sayHello(); } }
三. SPI的使用场景
- 我们先看看上面自定义的一个SPI:
- 我们定义了一个SpiService接口,比如说这个接口是根据用户需要初始化不同厂商的插件;
- 现在我们有两个开源的插件(华为和小米),用户如果想要用小米,只需要依赖小米的依赖即可,不做任何其它操作;
- 那么小米和华为这两个厂商的jar需要做什么呢?他们只需要去实现SpiService接口,实现自己的业务逻辑,并且重复一下上面2、3、4步的内容,就可以自定义一个插件;
这里也就用到了SPI的思想:使用方提供规则,提供方根据规则把自己加载到使用方中;
- DriverManager spi案例
-
针对于一个数据库,市面上有很多种数据库驱动,对于用户来说,在使用某一个驱动时,不希望修改代码,只需要更换依赖和指定的配置即可;
-
打开mysql-connector-java的jar包,在META-INF/services发现了接口路径,打开里面的内容,可以看到是com.mysql.jdbc.Driver,那么我们是不是可以猜测,不同的驱动都需要去实现Driver接口;
-
查看DriverManager的源码,可以看到其内部的静态代码块中有一个loadInitialDrivers方法,在注释中我们看到用到了上文提到的spi工具类ServiceLoader;
点开这个方法,可以看到如下代码:
-
走到这里,发现DriverManager和我们自定义SPI的2、3、4步大致一致,已经可以确定,DriverManager初始化时也运用了spi的思想,使用ServiceLoader把写到配置文件里的Driver都加载了进来。
其实用到SPI思想的框架还有很多,比如常用的Dubbo、插件体系、Spring等等…只要是能满足用户按照系统规则来自定义,并且可以注册到系统中的功能点,都带有着spi的思想。
四.补充
-
配置文件为什么要放在META-INF/services下面?
我们打开ServiceLoader类,查看源码,可以看到里面定义了一个PREFIX:
-
参考文档:https://zhuanlan.zhihu.com/p/28909673