Java 中经常被提到的 SPI 到底是什么?

举报
小白同学111 发表于 2022/12/25 20:36:20 2022/12/25
【摘要】 Java 中经常被提到的 SPI 到底是什么?

`Java` 程序员在日常工作中经常会听到 `SPI`,而且很多框架都使用了 `SPI` 的技术,那么问题来了,到底什么是 `SPI` 呢?今天阿粉就带大家好好了解一下 SPI。

## SPI 概念

`SPI` 全称是 `Service Provider Interface`,是一种 `JDK` 内置的动态加载实现扩展点的机制,通过 `SPI` 技术我们可以动态获取接口的实现类,不用自己来创建。

这里提到了接口和实现类,那么 `SPI` 技术上具体有哪些技术细节呢?

1.  接口:需要有一个功能接口;
2.  实现类:接口只是规范,具体的执行需要有实现类才行,所以不可缺少的需要有实现类;
3.  配置文件:要实现 `SPI` 机制,必须有一个与接口同名的文件存放于类路径下面的  `META-INF/services` 文件夹中,并且文件中的每一行的内容都是一个实现类的全路径;
4.  类加载器 `ServiceLoader`:`JDK` 内置的一个类加载器,用于加载配置文件中的实现类;

## 举个栗子

上面说了 `SPI` 的几个概念,接下来阿粉就通过一个栗子来带大家感受一下具体的用法。

### 第一步

创建一个接口,这里我们创建一个解压缩的接口,其中定义了压缩和解压的两个方法。

```
package com.example.demo.spi;
/** * <br> * <b>Function:</b><br> * <b>Author:</b>
@author ziyou<br> * <b>Date:</b>2022-10-28 11:31<br> * <b>Desc:</b>无<br> */
public interface Compresser 
{  
byte[] compress(byte[] bytes);  
byte[] decompress(byte[] bytes);
}
```

### 第二步

再写两个对应的实现类,分别是 `GzipCompresser.java` 和 `WinRarCompresser.java` 代码如下

```
package com.example.demo.spi.impl;
import com.example.demo.spi.Compresser;
import java.nio.charset.StandardCharsets;
/** * <br> * <b>Function:</b><br> * <b>Author:</b>
@author ziyou<br> * <b>Date:</b>2022-10-28 11:31<br> * <b>Desc:</b>无<br> */public class GzipCompresser implements Compresser
{  
@Override  public byte[] compress(byte[] bytes) 
{   
 return"compress by Gzip".getBytes(StandardCharsets.UTF_8);  
}  
@Override  public byte[] decompress(byte[] bytes) 
{    
return "decompress by Gzip".getBytes(StandardCharsets.UTF_8);  
}
}
```

```
package com.example.demo.spi.impl;
import com.example.demo.spi.Compresser;
import java.nio.charset.StandardCharsets;
/** * <br> * <b>Function:</b><br> * <b>Author:</b>
@author ziyou<br> * <b>Date:</b>2022-10-28 11:31<br> * <b>Desc:</b>无<br> */
public class WinRarCompresser implements Compresser 
{  
@Override  public byte[] compress(byte[] bytes) 
{    
return "compress by WinRar".getBytes(StandardCharsets.UTF_8);  
}  
@Override  public byte[] decompress(byte[] bytes) 
{    
return "decompress by WinRar".getBytes(StandardCharsets.UTF_8);  
}
}
```

### 第三步

创建配置文件,我们接着在 `resources` 目录下创建一个名为 `META-INF/services` 的文件夹,在其中创建一个名为 `com.example.demo.spi.Compresser` 的文件,其中的内容如下:

```
com.example.demo.spi.impl.WinRarCompressercom.example.demo.spi.impl.GzipCompresser
```

注意该文件的名称必须是接口的全路径,文件里面的内容每一行都是一个实现类的全路径,多个实现类就写在多行里面,效果如下。

![image.png](https://img-blog.csdnimg.cn/img_convert/e0a8b07733a56166a0c39b0a8661c5f1.png)


### 第四步

有了上面的接口,实现类和配置文件,接下来我们就可以使用 `ServiceLoader` 动态加载实现类,来实现 `SPI` 技术了,如下所示:

```
package com.example.demo;
import com.example.demo.spi.Compresser;import java.nio.charset.StandardCharsets;
import java.util.ServiceLoader;
public class TestSPI 
{  
public static void main(String[] args) 
{    
ServiceLoader<Compresser> compressers = ServiceLoader.load(Compresser.class);    
for (Compresser compresser : compressers) 
{      
System.out.println(compresser.getClass());    
}  
}
}
```

运行的结果如下
![image.png](https://img-blog.csdnimg.cn/img_convert/57bf607dc5941cd38a83a11164d42fc7.png)

可以看到我们正常的获取到了接口的实现类,并且可以直接使用实现类的解压缩方法。

## 原理

知道了如何使用 `SPI` 接下来我们来研究一下是如何实现的,通过上面的测试我们可以看到,核心的逻辑是 `ServiceLoader.load()` 方法,这个方法有点类似于 `Spring` 中的根据接口获取所有实现类一样。

点开 `ServiceLoader` 我们可以看到有一个常量 `PREFIX`,如下所示,这也是为什么我们必须在这个路径下面创建配置文件,因为` JDK` 代码里面会从这个路径里面去读取我们的文件。

![image.png](https://img-blog.csdnimg.cn/img_convert/9a2ea6dd592f06b908d27b44960c82ec.png)


同时又因为在读取文件的时候使用了 `class` 的路径名称,因为我们使用 `load` 方法的时候只会传递一个` class`,所以我们的文件名也必须是接口的全路径。

![image.png](https://img-blog.csdnimg.cn/img_convert/4d2296d46d3ecf7aff6b9a1e604eaba9.png)


通过 `load` 方法我们可以看到底层构造了一个 `java.util.ServiceLoader.LazyIterator` 迭代器。

![image.png](https://img-blog.csdnimg.cn/img_convert/bd57d195c5586593c4bede16327f6545.png)


在迭代器中的 `parse` 方法中,就获取了配置文件中的实现类名称集合,然后在通过反射创建出具体的实现类对象存放到 `LinkedHashMap<String,S> providers = new LinkedHashMap<>();` 中。

![image.png](https://img-blog.csdnimg.cn/img_convert/ebefebece3893df2da3b3dc50ba64cd9.png)

## 常用的框架

SPI 技术的使用非常广泛,比如在 `Dubble`,不过 `Dubble` 中的 `SPI` 有经过改造的,还有我们很常见的数据库的驱动中也使用了 `SPI`,感兴趣的小伙伴可以去翻翻看,还有 `SLF4J` 用来加载不同提供商的日志实现类以及 `Spring` 框架等。

## 优缺点

前面介绍了 `SPI` 的原理和使用,那 `SPI` 有什么优缺点呢?

### 优点

优点当然是解耦,服务方只要定义好接口规范就好了,具体的实现可以由不同的 `Jar` 进行实现,只要按照规范实现功能就可以被直接拿来使用,在某些场合会被进行热插拔使用,实现了解耦的功能。

### 缺点

一个很明显的缺点那就是做不到按需加载,通过源码我们看到了是会将所有的实现类都进行创建的,这种做法会降低性能,如果某些实现类实现很耗时了话将影响加载时间。同时实现类的命名也没有规范,让使用者不方便引用。

## 总结

阿粉今天给大家介绍了一个 `SPI` 的原理和实现,感兴趣的小伙伴可以自己去尝试一下,多动手有利于加深记忆哦。
【这里想说,因为自己也走了很多弯路过来的,所以才下定决心整理,收集过程虽不易,但想到能帮助到一部分自学java想提升Java架构师技术的,P5-P6-P7-P8 的人,心里也是甜的!有需要的伙伴请点㊦方】↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。