SPI全称(Service Provider Interface)即 服务提供者接口,它是JDK内置的一种服务提供发现机制,这样说可能比较抽象,下面我们来举个例子来类比一下:
Spring项目中,我们写service层的时候都会约定俗成的有一个接口来定义规范,然后通过spring中的依赖注入,可以使用@Autowired等方式注入这个接口实现类的实例对象,之后的调用操作都是对基于这个接口调用。
简单来说就是这样的:
如图所示,接口、实现类都是由服务提供方提供,我们可以把controller看作是服务调用方,调用方只关心接口的调用就好了。
在上面这个例子里,service层和其中的方法我们都可以称之为API,而我们要讨论的SPI和它相比,有类似也有差异,看以下图:
简单来说,就是定义一个接口规范,可以由不同的服务提供者实现,并且,调用方可以通过某种机制来发现服务提供方,并通过这个接口调用它的能力。
通过对比,我们可以看出它们虽然都有着接口这一层面,但还是有很大不同。
API中的接口只是给我们调用方提供一个功能列表,调用方直接调用就完事了,而SPI是定义一个接口规范,提供给服务方实现,然后调用方可以通过某种机制发现服务提供者。
说白了就是我给你接口规范,你只要按照我的规范去实现,我就能通过某种机制发现服务提供者,并且通过这个接口调用它的能力。
简单来说,就是我们定义一个标准接口,让第三方平台去实现这个接口,调用者可以根据配置来动态加载不同的第三方库,实现动态扩展功能。例如: 实现我这个接口的厂商有A、B、C,那么我就可以根据配置来加载其中一个库。
好处就是解耦、可插拔、面向接口编程、动态类加载、扩展性强。
如果这么说还是有点抽象,那么就看下面的案例
在这个案例里我们就以智能电风扇为例,一般智能电风扇的核心功能有: 开关、摆头、定时、调节档次。
假设我们有A、B、C三个不同型号的智能电风扇,但是我们的主要功能是一样的,设想一下,如果不同型号的智能电风扇各写各的接口,那么对接起来是不是很麻烦。
那么怎么解决呢?很简单,这个时候就用到了我们的SPI,我先定义一套接口规范,你按照我的接口规范实现,我就能通过某种服务发现机制找到你,使用这种方式的话,后续还有其他型号生产,只要按照这套规范实现,我都能找到。
废话不多说,上代码
新建一个项目,名为fan-spi-demo
接下来新建一个接口,定义接口规范
package com.hmg.spi.service;
/**
* @author hmg
* @version 1.0
* @date 2023-04-21 16:16
* @description: fan core service
*/
public interface IFanCoreService {
/**
* 获取风扇类型
* @return 类型
*/
String getType();
/**
* 开关
*/
void turnOnOff();
/**
* 调节风速
*/
void adjustSpeed();
/**
* 摆头
*/
void frontSway();
/**
* 定时
*/
void timer();
}
这个接口定义好了,后面要给服务提供者实现,用maven把它打成jar包,方便给服务提供者引入
//使用这条命令打包
mvn clean install
打完包之后服务提供者就可以引入了
新建一个服务提供者A 模块,名为fanA-type-provider
引入标准接口jar包
com.hmg.spi
fan-spi-demo
1.0-SNAPSHOT
创建服务实现类
package com.hmg.spi.service.impl;
import com.hmg.spi.service.IFanCoreService;
/**
* @author hmg
* @version 1.0
* @date 2023-04-21 16:44
* @description: A型号风扇实现类
*/
public class AaTypeFanServiceImpl implements IFanCoreService {
@Override
public String getType() {
return "A";
}
@Override
public void turnOnOff() {
System.out.println("A型号风扇开关");
}
@Override
public void adjustSpeed() {
System.out.println("A型号风扇调节风速");
}
@Override
public void frontSway() {
System.out.println("A型风扇摆头");
}
@Override
public void timer() {
System.out.println("A型风扇定时");
}
}
在项目的resources目录下创建META-INF/services目录,然后以接口全限定名创建文件,并在文件中写入实现类的全限定类名
这样我们就完成了一个服务提供者的实现,用maven打成jar包,就可以提供给调用方使用了
mvn clean install
接下来创建服务提供者B模块,和A模块同理
在pom.xml中引入标准接口jar包
com.hmg.spi
fan-spi-demo
1.0-SNAPSHOT
创建接口实现类
package com.hmg.spi.service.impl;
import com.hmg.spi.service.IFanCoreService;
/**
* @author hmg
* @version 1.0
* @date 2023-04-21 17:36
* @description: B型号风扇实现类
*/
public class BbTypeFanServiceImpl implements IFanCoreService {
@Override
public String getType() {
return "B";
}
@Override
public void turnOnOff() {
System.out.println("B型号风扇开关");
}
@Override
public void adjustSpeed() {
System.out.println("B型号风扇调节风速");
}
@Override
public void frontSway() {
System.out.println("B型风扇摆头");
}
@Override
public void timer() {
System.out.println("B型风扇定时");
}
}
在resources目录下创建META-INF/services目录,新建一个普通文件,以接口全限定名作为文件名,文件内容就是实现类的全限定类名
使用maven打成jar包,提供给调用方使用
mvn clean install
C模块同理,一样的操作,这里就不再演示了。
现在三个服务提供者都实现了接口,下一步就是最关键的服务发现了,不过这一步java中的spi发现机制已经帮我们实现好了。
新建项目fan-app
引入那三个服务提供者jar包
com.hmg.spi
fanA-type-provider
1.0-SNAPSHOT
com.hmg.spi
fanB-type-provider
1.0-SNAPSHOT
com.hmg.spi
fanC-type-provider
1.0-SNAPSHOT
编写主程序代码
按照之前的说法,虽然每个服务提供者都对接口做了实现,但是调用者无需关心具体实现类,我们要做的是通过接口来调用服务提供者实现的方法。
下面就是关键的服务发现环节,我们编写一个方法,根据型号去调用智能电风扇的开关方法
package com.hmg.spi;
import com.hmg.spi.service.IFanCoreService;
import java.util.ServiceLoader;
/**
* @author hmg
* @version 1.0
* @date 2023-04-21 23:37
* @description: 调用者测试
*/
public class Main {
/**
* A型号的风扇
*/
private final static String A_TYPE = "A";
public static void main(String[] args) {
new Main().turnOn(A_TYPE);
}
private void turnOn(String type){
//通过ServiceLoader类的load方法去发现服务提供者并加载
ServiceLoader<IFanCoreService> fanCoreServices = ServiceLoader.load(IFanCoreService.class);
fanCoreServices.forEach(fanCoreService -> {
System.out.println("检测到的类名:" + fanCoreService.getClass().getSimpleName());
//判断是否是A型号的风扇,是的话直接开启
if (type.equals(fanCoreService.getType())) {
fanCoreService.turnOnOff();
}
});
}
}
测试结果
ServiceLoader.load()方法
load()方法帮我们干了什么事呢?他其实就干了一件事,把接口.class和多线程上下文保存到LazyIterator(懒加载迭代器),我怎么知道的?当然是看源码,请看以下图:
有读者可能会有疑问,线程的上下文类加载器是用来干嘛的?先别急,后面会说到,继续往下走,看源码:
新创建一个ServiceLoader对象,把服务和类加载器都传进去
对于providers和lookupIterator属性,有些读者可能在这里会有疑问,这两个是什么?看图:
从上面创建新的懒加载迭代器点进来看实现,对传进来的参数进行赋值
到目前为止,load()方法就结束了,所以load方法干了什么事应该都有了大致了解。
接下来就是整个ServiceLoader的核心了, 当我们遍历ServiceLoader.load()方法得到的结果后,发现它会调用iterator()方法,为什么?当然是因为ServiceLoader实现了Iterable这个接口,细心的同学应该早就发现了,而整个服务发现的核心就在iterator()这个方法里,接下来让我们一探究竟。
里面的acc是访问控制上下文,ServiceLoader创建的时候用的,前面判断的时候为空,所以这里直接看hasNextService()就好了
路径前缀,所以这也就是为什么要在resources下创建META-INF/services目录了
至此,ServiceLoader的整个执行流程源码我们就看完了,在迭代器的迭代过程中,会完成所有实现类的实例化,其实归根结底,还是基于 java 反射去实现的。
大概的核心流程就是先通过ServiceLoader.load方法清空缓存,并且创建懒加载迭代器,把class对象和类加载器先装到懒加载迭代器,做好真正加载的工作,然后遍历load得到的结果,就来到了iterator()方法,进来就先获取缓存,然后来到hasNext()方法(这个方法主要用于判断缓存中是否有值,如果有直接返回true,没有的话就使用lookupIterator调用hasNext方法,hasNext方法里又调用hasNextService方法获取实现类全限定类名,放到nextName中,然后返回true),判断是否有下一个值完了之后,就到了next()方法,进来也是先判断缓存里是否有值,有的话直接返回,没有就使用lookupIterator调用next方法,next方法中调用nextService()方法(这个方法主要是通过Class.forName加载Class对象,然后通过Class对象的newIntstance()方法实例化实现类对象,最后再放到缓存里(全限定类名作为key,实例化后的实现类对象作为value,大概就是这样了,如果到这还是看不太懂的话,那就多debug看几遍就会了。
SPI应用场景有很多,比如: Spring、Common-Logging、JDBC、Dubbo、可插拔架构、框架开发等等
Java中的SPI机制整体上来说还是很不错的,通过接口灵活的将服务调用者与服务提供者分离,提供给第三方实现扩展时还是很方便的,不过它也有缺点,比如不能按需加载,只能一次性全部加载进来,如果加载到某些不需要的实现类,那就会造成资源浪费,还有每一个标准接口都需要创建一个新的文件来存放具体实现类,这是非常不方便的,最后ServiceLoader多线程使用是不安全的,不过整体上来说还是很不错的,提供了一种非常不错的思想。
看了上面的Java SPI,相信大家对SPI都有大概的认识了,其实Spring SPI也是基于JavaSPI思想来做的,从而实现了自己的SPI。
这里还是使用JavaSPI里的案例(智能电风扇)
创建spring项目名为fan-spi-spring
这里我还是使用聚合项目,我就不过多阐述这个了
引入spring依赖
org.springframework
spring-core
5.3.26
定义接口
package com.hmg.spi.fanspi;
/**
* @author hmg
* @version 1.0
* @date 2023-04-23 0:28
* @description: fan core spi service
*/
public interface IFanCoreSpiService {
/**
* 获取风扇类型
* @return 类型
*/
String getType();
/**
* 开关
*/
void turnOnOff();
/**
* 调节风速
*/
void adjustSpeed();
/**
* 摆头
*/
void frontSway();
/**
* 定时
*/
void timer();
}
使用maven打包
mvn clean install
新建项目,名为fanA-type-provider-spring
引入fan-spi-spring项目的依赖
com.hmg.spi
fan-spi-spring
1.0-SNAPSHOT
创建接口实现类
package com.hmg.spi.service.impl;
import com.hmg.spi.fanspi.IFanCoreSpiService;
/**
* @author hmg
* @version 1.0
* @date 2023-04-23 0:50
* @description: A型号风扇spi实现类
*/
public class AaTypeFanCoreSpiServiceImpl implements IFanCoreSpiService {
@Override
public String getType() {
return "A";
}
@Override
public void turnOnOff() {
System.out.println("A型号风扇开关");
}
@Override
public void adjustSpeed() {
System.out.println("A型号风扇调节风速");
}
@Override
public void frontSway() {
System.out.println("A型风扇摆头");
}
@Override
public void timer() {
System.out.println("A型风扇定时");
}
}
在resources目录下创建META-INF目录,并创建spring.factories文件,在文件里写入接口全限定名=实现类全限定类名,多个实现类用逗号隔开,例如:接口全限定名=实现类全限定类名, 实现类全限定类名
使用maven打包,提供给调用者使用
mvn clean install
新建项目,名为fanB-type-provider-spring
引入fan-spi-spring项目的依赖
com.hmg.spi
fan-spi-spring
1.0-SNAPSHOT
创建接口实现类
package com.hmg.spi.service.impl;
import com.hmg.spi.fanspi.IFanCoreSpiService;
/**
* @author hmg
* @version 1.0
* @date 2023-04-23 1:02
* @description: B型号风扇spi实现类
*/
public class BbTypeFanCoreSpiServiceImpl implements IFanCoreSpiService {
@Override
public String getType() {
return "B";
}
@Override
public void turnOnOff() {
System.out.println("B型号风扇开关");
}
@Override
public void adjustSpeed() {
System.out.println("B型号风扇调节风速");
}
@Override
public void frontSway() {
System.out.println("B型风扇摆头");
}
@Override
public void timer() {
System.out.println("B型风扇定时");
}
}
在resources目录下创建META-INF目录,并创建spring.factories文件,在文件里写入接口全限定名=实现类全限定类名,多个实现类用逗号隔开,例如:接口全限定名=实现类全限定类名, 实现类全限定类名
使用maven打包,提供给调用者使用
mvn clean install
C模块同理,一样的操作,这里就不再演示了。
新建项目fan-app-spring
引入三个服务提供者模块的依赖
com.hmg.spi
fanA-type-provider-spring
1.0-SNAPSHOT
com.hmg.spi
fanB-type-provider-spring
1.0-SNAPSHOT
com.hmg.spi
fanC-type-provider-spring
1.0-SNAPSHOT
编写调用者代码
package com.hmg.spi;
import com.hmg.spi.fanspi.IFanCoreSpiService;
import org.springframework.core.io.support.SpringFactoriesLoader;
import java.util.List;
/**
* @author hmg
* @version 1.0
* @date 2023-04-23 1:12
* @description: 调用者测试
*/
public class Main {
public static void main(String[] args) {
new Main().turnOn("B");
}
private void turnOn(String type){
//Spring SPI使用SpringFactoriesLoader去发现并加载实现类
List<IFanCoreSpiService> fanCoreSpiServices =
SpringFactoriesLoader.loadFactories(IFanCoreSpiService.class, Main.class.getClassLoader());
fanCoreSpiServices.forEach(iFanCoreSpiService -> {
System.out.println("检测到的实现类有:" + iFanCoreSpiService.getClass().getSimpleName());
if (type.equals(iFanCoreSpiService.getType())) {
iFanCoreSpiService.turnOnOff();
}
});
}
}
测试
因为ServiceLoader基于图片形式讲解,感觉比较麻烦,所以SpringFactoriesLoader我还是贴代码讲解吧,在代码里加注释。
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
//断言,factoryType为空直接报错factoryType' must not be null
Assert.notNull(factoryType, "'factoryType' must not be null");
ClassLoader classLoaderToUse = classLoader;
//判断是否有类加载器,没有的话就使用SpringFactoriesLoader的
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
//加载实现类全限定名,我们进去看下loadFactoryNames()实现,看它怎么加载的实现类全限定名
List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
if (logger.isTraceEnabled()) {
logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);
}
List<T> result = new ArrayList<>(factoryImplementationNames.size());
for (String factoryImplementationName : factoryImplementationNames) {
/**
* 重点关注instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse),这里面主要是通过反射实例化对象
* 接下来我们探究一下它的源码
*
**/
result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
}
//对结果进行排序
AnnotationAwareOrderComparator.sort(result);
return result;
}
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
//判断是否有类加载器,没有的话就使用SpringFactoriesLoader的
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
//获取接口全限定名
String factoryTypeName = factoryType.getName();
//主要看loadSpringFactories(classLoaderToUse)方法,这里面是找到spring.factories的源码
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
//先从缓存里面取,如果有数据直接返回,则继续往下执行(注意:key是classLoader)
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
//根据路径获取所有资源,FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
//加载配置文件拿到实现类全限定名,为什么可以用Properties加载,因为配置是K V的
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
//以逗号分隔的实现类全限定类名转成字符串数组
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
//重新计算key,不存在则添加,存在则直接返回
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
//给结果去重
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
//把结果添加到缓存里,classLoader作为key, 结果集作为value
cache.put(classLoader, result);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
//返回结果
return result;
}
这个方法的实现就很少了,比较简单
private static <T> T instantiateFactory(String factoryImplementationName, Class<T> factoryType, ClassLoader classLoader) {
try {
//通过ClassUtils.forName()加载Class对象
Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);
//判断实现类是不是实现了标准接口
if (!factoryType.isAssignableFrom(factoryImplementationClass)) {
throw new IllegalArgumentException(
"Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]");
}
//先获得构造器,然后再实例化对象
return (T) ReflectionUtils.accessibleConstructor(factoryImplementationClass).newInstance();
}
catch (Throwable ex) {
throw new IllegalArgumentException(
"Unable to instantiate factory class [" + factoryImplementationName + "] for factory type [" + factoryType.getName() + "]",
ex);
}
}
到这里SpringFactoriesLoader源码解读也就结束了,整体上还是比较简单的,阅读完源码后来个小总结
大概说一下核心流程,就是我们在进入loadFactories方法时,第一个核心点就是loadFactoryNames方法里调用的loadSpringFactories方法,它所做的事就是加载实现类全限定名,第二个核心点就是instantiateFactory方法了,它所做的事就是通过反射实例化对象了。
阅读完SpringFactoriesLoader源码发现比JavaSPI的ServiceLoader简洁了不少,不过他们的整体流程相似度还是很高的,还有就是spring spi的所有配置是放到一个文件里的,省去了写一大堆文件的麻烦,而java spi是一个标准接口一个文件,这样的话就比较麻烦了。
它们最大的区别就是配置文件了,JavaSPI 是一个接口一个配置文件,而Spring SPI是集中在一个配置文件里,也就是spring.factories,还有一个就是java spi是在遍历的时候才真正加载实现类,而spring spi是在loadFactories的时候就加载了。