SPI是什么

SPI全称为Service Provider Interface,直译就是服务提供方的接口。一种用于定义服务提供商与应用程序之间通信的接口,通常用于实现模块化和可插拔的系统。

Q:为什么会有SPI这种机制?

A:因为在Java面向对象编程中,基于开闭原则和解耦的需要,一般建议用接口进行模块之间的通信编程,通常情况下调用方模块是不会感知到被调用方模块的内部具体实现。

为了实现在模块装配的时候不用在程序里面动态指明,这就需要一种服务发现机制。Java SPI 就是提供了这样一个机制:为某个接口寻找服务实现的机制。这有点类似 IoC 的思想,将装配的控制权移交到了程序之外。

Java SPI机制

Java实现SPI机制的核心类叫 ServiceLoader ,Java的SPI实现约定了以下两件事:

  • 文件必须放在META-INF/services/目录底下,并且必须使用UTF-8编码。
  • 文件名必须为接口的全限定名,内容为接口实现的全限定名。

示例

第一步,定义一个接口,以及它的实现类。

1
2
3
public interface EventListener {
void onEvent(String event);
}
1
2
3
4
5
6
public class PrinterEventListener implements EventListener {
@Override
public void onEvent(String event) {
System.out.println("this is PrinterEventListener: " + event);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class BackupEventListener implements EventListener {

@Override
public void onEvent(String event) {
System.out.println("this is BackupEventListener: " + event);
this.backup(event);
}

private void backup(String event) {
// 保存...
}
}

第二步,在 META-INF/services 目录下创建 EventListener 类的全限定名的文件,文件内容为 PrinterEventListener、BackupEventListener 类的全限定名。

image-20230919094459759

image-20230919094528669

第三步,定义一个测试类,使用Java的 ServiceLoader 加载文件,并发起调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class JavaSpiDemo {

public static void main(String[] args) {
// 通过Java SPI加载
ServiceLoader<EventListener> serviceLoader = ServiceLoader.load(EventListener.class);
// 获取接口迭代器
Iterator<EventListener> listenerIterator = serviceLoader.iterator();
// 定义事件监听消息
String event = "Java SPI机制";

// 遍历调用事件监听器
listenerIterator.forEachRemaining(eventListener -> {
eventListener.onEvent(event);
});
}
}

测试结果:

image-20230919094753460

此时成功通过Java SPI获取到了实现类。

实现原理

通过测试类 JavaSpiDemo 的代码大致可知,首先通过 ServiceLoader 加载指定类 ,然后获取到一个迭代器,迭代器的元素就是指定类的实现类。

Q:为什么通过ServiceLoader加载的是“指定类”,而不是接口?

A:因为提供者提供的类也可以是抽象类,实现类可以通过extends继承的方式实现。Java SPI 判断文件中的类是否为实现类的方式就是 Class.isInstance(Object obj) 方法,该方法等同于 instanceof 。

先看加载原理:

image-20230919095229544

首先获取一个fullName,其实就是 META-INF/services/ 指定类的全限定名。

然后通过ClassLoader获取到资源,其实就是接口的全限定名文件对应的资源,然后交给 parse 方法解析资源。

image-20230919100654882

image-20230919100929032

parse方法其实就是通过IO流读取文件的内容,然后通过 parseLine 方法获取实现类的全限定名。

最后在迭代器调用 next 方法时,调用内部的 nextService 方法,通过反射构造实现类的实例化对象。

image-20230919101514809

💡注意:通过此处源码可以看出,ServiceLoader实例化实现类的方式是 Class.newInstance() 方法,所以通过Java SPI实现SPI机制时,实现类必须要有无参构造方法。

优缺点

从示例代码和源码可以看出,Java实现的SPI机制比较简单,所以更容易于开发者理解和使用。也因为此会有一点缺点。

第一点就是浪费资源,虽然例子中只有一个实现类,但是实际情况下可能会有很多实现类,而Java的SPI会一股脑全进行实例化,但是这些实现了不一定都用得着,所以就会白白浪费资源。

第二点就是无法对区分具体的实现,也就是这么多实现类,到底该用哪个实现呢?如果要判断具体使用哪个,只能依靠接口本身的设计,比如接口可以设计为一个策略接口,又或者接口可以设计带有优先级的,但是不论怎样设计,框架作者都得写代码进行判断。

所以总得来说就是ServiceLoader无法做到按需加载或者按需获取某个具体的实现。

使用场景

一般适用于:

  • 不需要选择具体的实现,每个被加载的实现都需要被用到
  • 虽然需要选择具体的实现,但是可以通过对接口的设计来解决

例如:全局拦截器、过滤器、校验器。

我在项目中用过 javax.validation.spi.ValidationProvider ,hibernate对其做了较为全面的封装,其实现类为 org.hibernate.validator.HibernateValidator 。

Spring SPI机制

Spring实现SPI机制的核心类叫 SpringFactoriesLoader ,Spring的SPI机制约定如下:

  • 配置文件必须在META-INF/目录下,文件名必须为spring.factories
  • 文件内容为键值对,一个键可以有多个值,只需要用逗号分割就行,同时键值都需要是类的全限定名,键和值可以没有任何类与类之间的关系,当然也可以有实现的关系。

从约定内容可以看出,Spring的SPI机制与Java的SPI机制相比,文件约定和接口类约定都不相同。

示例

第一步,定义文件。

image-20230919112022912

image-20230919111948099

第二步,定义测试类。

1
2
3
4
5
6
7
8
9
10
11
12
public class SpringSpiDemo {

public static void main(String[] args) {
List<EventListener> eventListeners = SpringFactoriesLoader.loadFactories(EventListener.class, null);
String event = "Spring SPI机制";
eventListeners.forEach(eventListener -> eventListener.onEvent(event));

// Spring 6.0开始废弃此方法
List<String> factoryNames = SpringFactoriesLoader.loadFactoryNames(EventListener.class, null);
System.out.println(factoryNames);
}
}

测试结果:

image-20230919112138626

成功获取到实现对象。

实现原理

大致原理为:

  1. 解析 META-INF/spring.factories 文件,获取实现类的全限定名。
  2. 反射实例化对象。
  3. Spring 排序。

image-20230919112747587

第一步,解析文件是Spring实现SPI机制的关键,它默认解析 FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories" 下的文件内容。源码如下:

image-20230919112433720

第二步,实例化对象时也是先获取Class类对象,再通过反射构造实例化对象,与Java SPI不同的是,它是使用的 Constructor.newInstance() 方法,但并不支持有参构造,还是得需要无参构造方法。

image-20230919113511676

第三部,Spring内置排序功能,AnnotationAwareOrderComparator.sort(List<?> list) 方法的作用是判断对象是否实现了 Ordered 类,或者是否标记了 Order 注解。排序功能的实现类为 AnnotationAwareOrderComparator

这也是Spring SPI机制与Java SPI机制的不同点,ServiceLoader是按顺序加载并返回,而SpringFactoriesLoader是按顺序加载,正序排序后再返回。

使用场景

Spring的SPI机制在内部使用的非常多,尤其在SpringBoot中大量使用,SpringBoot启动过程中很多扩展点都是通过SPI机制来实现的,例如:

1、自动装配

在SpringBoot3.0之前的版本,自动装配是通过SpringFactoriesLoader来加载的。

image-20230919114212629

但是SpringBoot3.0之后不再使用SpringFactoriesLoader,而是Spring重新从 META-INF/spring/ 目录下的 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中读取了。

2、PropertySourceLoader的加载

PropertySourceLoader是用来解析application配置文件的,它是一个接口。

image-20230919114314107

SpringBoot默认提供了 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader两个实现,就是对应properties和yaml文件格式的解析。

SpringBoot在加载PropertySourceLoader时就用了SPI机制。

image-20230919114432063

与Java SPI机制对比

首先Spring的SPI机制对Java的SPI机制对进行了一些简化,Java的SPI每个接口都需要对应的文件,而Spring的SPI机制只需要一个spring.factories文件。

其次是内容,Java的SPI机制文件内容必须为接口的实现类,而Spring的SPI并不要求键值对必须有什么关系,更加灵活。

第三点就是Spring的SPI机制提供了获取类限定名的方法loadFactoryNames,而Java的SPI机制是没有的。通过这个方法获取到类限定名之后就可以将这些类注入到Spring容器中,用Spring容器加载这些Bean,而不仅仅是通过反射。

但是Spring的SPI也同样没有实现获取指定某个指定实现类的功能,所以要想能够找到具体的某个实现类,还得依靠具体接口的设计。

所以不知道你有没有发现,PropertySourceLoader它其实就是一个策略接口,注释也有说,所以当你的配置文件是properties格式的时候,他可以找到解析properties格式的PropertiesPropertySourceLoader对象来解析配置文件。

总结

通过以上分析可以看出,实现SPI机制的核心原理就是通过IO流读取指定文件的内容,然后解析,最后加入一些自己的特性。