1、什么是SPI
SPI ,全称为 Service Provider Interface,是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。
JAVA SPI = 基于接口的编程 + 策略模式 + 配置文件 的动态加载机制
2、产生背景
在面向对象的设计原则中,一般推荐模块之间基于接口编程,通常情况下调用方模块是不会感知到被调用方模块的内部具体实现。一旦代码里面涉及具体实现类,就违反了开闭原则。如果需要替换一种实现,就需要修改代码。
为了实现在模块装配的时候不用在程序里面动态指明,这就需要一种服务发现机制,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现方解耦,能够提升程序的扩展性、可维护性,修改或者替换服务实现并不需要修改调用方。
3、使用场景
大量框架都使用了 Java 的 SPI 机制,举例如下:
(1)JDK,JDBC加载数据库驱动
(2)SLF4J,加载不同日志实现
(3)Spring,各种组件的插拔
(4)Dubbo,各种组件的插拔
(5)SpringBoot,启动bean自动装配机制
(6)Sharding-JDBC,主键生成策略
4、优缺点
由于classLoader加载类的时候采用的是 双亲委派机制,即先委托父类去加载器获取,若父类加载器存在则直接返回,若加载器无法完成此加载任务,自己才去加载。该加载存在的弊端就是上层的类加载永远无法加载下层的类加载器所加载的类,所以通过spi解决了该问题。
优点:
(1)实现服务接口与服务实现分离,从而达到模块解耦,提升了程序可扩展性;
(2)可以根据业务情况启用框架扩展或替换框架组件,实现了懒加载。
缺点:
(1)由于SPI是通过循环加载实现类,会导致所有的类全部一起加载,从而造成资源浪费;
(2)当存在多个并发多线程使用ServiceLoader类的实例时时不安全的,需要加锁控制;
(3)获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
5、SPI使用
(1)原生JDK SPI使用
1⃣️ 定义通用的服务接口,针对服务接口,提供具体实现类;
2⃣️ 在jar包的META-INF/services/目录中,新建一个文件,文件名为接口的 “全限定名”, 文件内容为该接口的具体实现类的 “全限定名”;
3⃣️ 将SPI所在jar包放在主程序的classpath中;
4⃣️ 服务调用方用java.util.ServiceLoader,用服务接口为参数,去动态加载具体的实现类到JVM中。
(2)第三方框架 SPI使用
1⃣️ 在pom.xml引入Google的AutoService组件包;
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0.1</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service-annotations</artifactId>
<version>1.0.1</version>
</dependency>
2⃣️ 定义通用的服务接口,针对服务接口,提供具体实现类,实现类上添加@AutoService注解;
3⃣️ 执行mvn命令生成jar,会发现jar包中生成了一个META-INF/services/xx.xx.xx.xx.Xxxxxxx文件。
6、SPI案例
(1)创建Maven项目
在IDEA中创建Maven项目spi-test,如下所示:
(2)编辑pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.jetco</groupId>
<artifactId>spi-test</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Spring Boot web 启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.13.RELEASE</version>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0.1</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service-annotations</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.1.13.RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<maxmem>512M</maxmem>
</configuration>
</plugin>
</plugins>
</build>
</project>
(3)创建接口
在org.jecto.spi.file包下创建FileService接口,接口中有上传和下载两个方法,打印相应的信息。代码如下:
package org.jetco.spi.file;
/**
* <p>
* 文件服务
* </p>
*
* @author lhw
* @version 1.0
* @since 2022-09-24
*/
public interface FileService {
/**
* 上传
*/
void upload();
/**
* 下载
*/
void download();
}
(4)创建接口的实现类,实现类使用@AutoService注解
1⃣️ 创建第一个实现类LocalFileService
在org.jetco.spi.file.impl包下创建LocalFileService类,实现FileService接口。代码如下:
package org.jetco.spi.file.impl;
import com.google.auto.service.AutoService;
import org.jetco.spi.file.FileService;
/**
* <p>
* 本地文件实现
* </p>
*
* @author lhw
* @version 1.0
* @since 2022-09-24
*/
@AutoService(FileService.class)
public class LocalFileService implements FileService {
@Override
public void upload() {
System.out.println("本地文件实现上传!");
}
@Override
public void download() {
System.out.println("本地文件实现下载!");
}
}
2⃣️ 创建第二个实现类HdfsFileService
在org.jetco.spi.file.impl包下创建HdfsFileService类,实现FileService接口。代码如下:
package com.gisinfo.sand.file;
import com.gisinfo.sand.spi.FileService;
import com.google.auto.service.AutoService;
/**
* <p>
* HDFS文件实现
* </p>
*
* @author lhw
* @version 1.0
* @since 2022-09-24
*/
@AutoService(FileService.class)
public class HdfsFileService implements FileService {
@Override
public void upload() {
System.out.println("HDFS文件实现上传!");
}
@Override
public void download() {
System.out.println("HDFS文件实现下载!");
}
}
3⃣️ 创建第三个实现类MinioFileService
在org.jetco.spi.file.impl包下创建MinioFileService类,实现FileService接口。代码如下:
package com.gisinfo.sand.file;
import com.gisinfo.sand.spi.FileService;
import com.google.auto.service.AutoService;
/**
* <p>
* MinIO文件实现
* </p>
*
* @author lhw
* @version 1.0
* @since 2022-09-24
*/
@AutoService(FileService.class)
public class MinioFileService implements FileService {
@Override
public void upload() {
System.out.println("MinIO文件实现上传!");
}
@Override
public void download() {
System.out.println("MinIO文件实现下载!");
}
}
(5) 执行mvn package命令生成jar
执行命令后,jar中生成了一个META-INF/services/org.jetco.spi.file.FileService文件,文件内容是三个文件实现类的全限定类名,省去了手动创建的过程,加快了编码效率和防止编写错误的情况。
(6)测试
package org.jetco.spi.file.impl;
import org.jetco.spi.file.FileService;
import org.junit.Before;
import org.junit.Test;
import java.util.ServiceLoader;
public class FileServiceTest {
private FileService fileService;
@Before
public void init() {
fileService = new LocalFileService();
}
@Test
public void upload() {
fileService.upload();
}
@Test
public void download() {
fileService.download();
}
@Test
public void testAllMethod() {
ServiceLoader<FileService> fileServices = ServiceLoader.load(FileService.class);
for (FileService fileService : fileServices) {
fileService.upload();
fileService.download();
}
}
}
7、JDK1.8 SPI 源码解析
根据之前测试所有接口实现类的方法,有使用到ServiceLoader类,点击进去,发现该类存在于rt.jar java.util工具包下,源码注释如下:
package java.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.AccessController;
import java.security.AccessControlContext;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
/**
* A simple service-provider loading facility.
*
* @param <S>
* The type of the service to be loaded by this loader
*
* @author Mark Reinhold
* @since 1.6
*/
// 这个类被final所修饰,所以是不可被继承修改
// ServiceLoader实现了Iterable接口,可以遍历所有的服务实现者
public final class ServiceLoader<S>
implements Iterable<S>
{
// 配置文件的路径
private static final String PREFIX = "META-INF/services/";
// 要被加载的服务类或接口
private final Class<S> service;
// 这个ClassLoader用来定位,加载,实例化服务提供者
private final ClassLoader loader;
// 当ClassLoader创建后,访问控制上下文作用
private final AccessControlContext acc;
// 缓存已实例化的服务类集合,按照实例化的顺序存储
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 迭代器,真正加载服务类
private LazyIterator lookupIterator;
// 重新加载,用于新的服务提供者加载到正在运行的Java虚拟机的情况。
public void reload() {
// 清空缓存中所有已实例化的服务提供者
providers.clear();
// 新建一个迭代器,该迭代器会从头查找和实例化服务提供者
lookupIterator = new LazyIterator(service, loader);
}
// 私有构造器
// 使用指定的类加载器和服务创建服务加载器
// 如果没有指定类加载器,使用系统类加载器,就是应用类加载器。
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// 要加载的接口
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// 访问控制器
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
// 重新加载
reload();
}
// 处理失败的3个重载方法
private static void fail(Class<?> service, String msg, Throwable cause) throws ServiceConfigurationError{
throw new ServiceConfigurationError(service.getName() + ": " + msg,
cause);
}
private static void fail(Class<?> service, String msg) throws ServiceConfigurationError {
throw new ServiceConfigurationError(service.getName() + ": " + msg);
}
private static void fail(Class<?> service, URL u, int line, String msg)
throws ServiceConfigurationError{
fail(service, u + ":" + line + ": " + msg);
}
// 解析服务提供者配置文件中的一行,先去掉注释校验,然后保存,接着返回下一行行号
// 重复的配置项和已经实例化的配置项不会被保存
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names)
throws IOException, ServiceConfigurationError{
// 读取一行
String ln = r.readLine();
if (ln == null) {
return -1;
}
// #号代表注释行
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
// 解析配置文件,解析指定的url配置文件
// 使用parseLine方法进行解析,未被实例化的服务提供者会被保存到缓存中去
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}
// 查找实现类和创建实现类的过程,都在 LazyIterator 完成。当我们调用 iterator.hasNext 和 iterator.next 方法的时候,实际上调用的都是 LazyIterator 的相应方法。
private class LazyIterator
implements Iterator<S>
{
// 服务提供者接口
Class<S> service;
// 类加载器
ClassLoader loader;
// 保存实现类的url
Enumeration<URL> configs = null;
// 保存实现类的全名
Iterator<String> pending = null;
// 迭代器中下一个实现类的全名
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
// 第二次调用的时候,已经解析完成了,直接返回
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 通过PREFIX(META-INF/services/)和类名获取对应的配置文件,得到具体的实现类
String fullName = PREFIX + service.getName();
// 将文件路径转成URL对象
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 解析URL文件对象,读取内容,最后返回
pending = parse(service, configs.nextElement());
}
// 拿到第一个实现类的类名
nextName = pending.next();
return true;
}
// 调用 next 方法的时候,实际调用到的是,lookupIterator.nextService。它通过反射的方式,创建实现类的实例并返回。
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
// 全限定类名
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 创建类的Class对象
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
// 通过newInstance实例化
S p = service.cast(c.newInstance());
// 放入集合,返回实例
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
/**
* 获取迭代器
* 返回遍历服务提供者的迭代器,以懒加载的方式加载可用的服务提供者
* 懒加载的实现是:解析配置文件和实例化服务提供者的工作由迭代器本身完成
**/
public Iterator<S> iterator() {
return new Iterator<S>() {
// 按照实例化顺序返回已经缓存的服务提供者实例
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
// 创建ServiceLoader的两个重载方法
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
// 使用扩展类加载器为指定的服务创建ServiceLoader
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
public String toString() {
return "java.util.ServiceLoader[" + service.getName() + "]";
}
}
从源码中知道,实现类的加载是一种懒加载机制,创建 ServiceLoader 并不会去加载接口实现,而是在遍历的时候再去加载。
创建 ServiceLoader 实例流程如下所示:
加载给定接口服务流程如下:
8、API和SPI区别
API接口调用示意图如下所示:
SPI接口调用示意图如下所示:
从以上两个示意图中可知,当实现端提供了接口和接口实现,那么调用端可以通过调用实现方的接口从而拥有实现端给我们提供的能力,这就是API,这种接口和实现都是放在实现端的。
当接口存在于调用端这边时,就是SPI,由接口调用端提供接口标准,然后由不同的接口实现端去根据这个标准对这个接口进行实现,从而提供不同的服务能力。