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

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

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

SPI 概念

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

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

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

举个栗子

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

第一步

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

package com.example.demo.spi;


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;


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;


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.WinRarCompresser
com.example.demo.spi.impl.GzipCompresser

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

图片

第四步

有了上面的接口,实现类和配置文件,接下来我们就可以使用 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());
}
}
}

运行的结果如下

图片

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

原理

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

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

图片

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

图片

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

图片

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

图片

常用的框架

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

优缺点

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

优点

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

缺点

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

责任编辑:武晓燕 来源: Java极客技术
相关推荐

2018-02-01 09:26:12

面试算法题程序员

2017-09-07 14:44:10

程序员

2019-03-06 14:26:31

Javascript面试前端

2023-10-11 08:29:54

volatileJava原子性

2022-01-12 09:08:37

索引JavaReference对象

2011-04-27 09:30:48

企业架构

2022-10-08 00:00:00

Spring数据库项目

2020-03-05 10:28:19

MySQLMRR磁盘读

2009-06-09 22:11:44

JavaScriptObject

2020-09-22 08:22:28

快充

2020-09-27 06:53:57

MavenCDNwrapper

2020-10-14 06:22:14

UWB技术感知

2010-11-01 01:25:36

Windows NT

2019-10-30 10:13:15

区块链技术支付宝

2010-04-22 14:14:29

Live-USB

2013-06-09 09:47:31

.NetPDBPDB文件

2020-08-04 14:20:20

数据湖Hadoop数据仓库

2021-09-03 09:12:09

Linux中断软件

2023-03-29 08:24:46

Rune类型开源项目

2019-12-25 15:40:28

内存Java虚拟机
点赞
收藏

51CTO技术栈公众号