什么是 SPI,和API有什么区别?

面试回答

Java 中区分 API 和 SPI,通俗的讲:API 和 SPI 都是相对的概念,他们的差别只在语义上,API 直接被应用开发人员使用,SPI 被框架扩展人员使用。

API Application Programming Interface

大多数情况下,都是实现方来制定接口并完成对接口的不同实现,调用方仅仅依赖却无权选择不同实现。

SPI Service Provider Interface

而如果是调用方来制定接口,实现方来针对接口实现不同的实现。调用方来选择自己需要的实现方。

知识扩展

如何定义一个 SPI

步骤1、定义一组接口(假设是 com.chiyi.test.IShout),并写出接口的一个或多个实现,(假设是 com.chiyi.test.Dogcom.chiyi.test.Cat)。

复制代码
public interface IShout {
    void shout();
}

public class Dog implements IShout{
    @Override
    public void shout() {
        System.out.println("wang wang");
    }
}

public class Cat implements IShout{
    @Override
    public void shout() {
        System.out.println("miao miao");
    }
}

步骤2、在 src/main/resources/ 下建立 /META-INF/services目录,新增一个以接口命名的文件(com.chiyi.test.IShout 文件),内容是要应用的实现类(这里是 com.chiyi.test.Dogcom.chiyi.test.Cat,每行一个类)。

复制代码
com.chiyi.test.Dog
com.chiyi.test.Cat

步骤3、使用 ServiceLoader 来加载配置文件中指定的实现。

复制代码
public class Main {


    public static void main(String[] args) {
        ServiceLoader<IShout> shouts=ServiceLoader.load(IShout.class);
        for(IShout s:shouts){
            s.shout();
        }
    }
}

代码输出:

wang wang

miao miao

SPI 的实现原理

看 ServiceLoader 类的签名类的成员变量:

复制代码
public final class ServiceLoader<S>
    implements Iterable<S>
{

    private static final String PREFIX = "META-INF/services/";

    // 代表被加载的类或者接口
    private final Class<S> service;

    // 用于定位,加载和实例化 providers 的类加载器
    private final ClassLoader loader;

    // 创建 ServiceLoader 时采用的访问控制上下文
    private final AccessControlContext acc;

    // 缓存 providers,按实例化的顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 懒查找迭代器
    private LazyIterator lookupIterator;
	······
}

参考具体源码,梳理了一下,实现的流程如下:

  1. 应用程序调用 ServiceLoader.load 方法,ServiceLoader.load方法内先创建一个新的 ServiceLoader,并实例化该类中的成员变量,包括:
    1. loader(ClassLoader 类型,类加载器)
    2. acc(AccessControlContext 类型,访问控制器)
    3. providers(LinkedHashMap 类型,用于缓存加载成功的类)
    4. lookupIterator(实现迭代器功能)
  1. 应用程序通过迭代器接口获取对象实例
    1. ServiceLoader 先判断成员变量 providers 对象中(LinkedHashMap 类型)是否有缓存实例对象,如果有缓存,直接返回。
    2. 如果没有缓存,执行类的装载:
      1. 读取 META-INF/services/ 下的配置文件,获得所有能被实例化的类的名称
      2. 通过反射方法 Class.forName() 加载类对象,并用 instance() 方法将类实例化
      3. 把实例化的类缓存到 providers 对象中(LinkedHashMap 类型)
      4. 然后返回实例对象

SPI 的应用场景

概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略。

比如常见的例子:

  1. 数据库驱动加载接口实现类的加载
  2. JDBC 加载不同类型数据库的驱动
  3. 日志门面接口实现类加载
  4. SLF4J 加载不同提供商的日志实现类

Spring

Spring 中大量使用了 SPI,比如:对 servlet3.0 规范对 ServletContainerInitializer 的实现、自动类型转换 Type Conversion SPI(Converter SPI、Formatter SPI)等

Dubbo

Dubbo 中也大量使用 SPI的方式实现框架的扩展,不过它对 java 提供的原生 SPI 做了封装,允许用户扩展实现 Filter 接口。

相关推荐
IT枫斗者枫哥10 分钟前
CompletableFuture 超时了,任务还在跑?用三个实验分清超时与取消
java
chushiyunen22 分钟前
mockito笔记
java·开发语言·笔记
悟天特斯23 分钟前
AI驱动的楼宇节能:从粗放管控到精准降碳的实践路径
开发语言·人工智能·python·物联网
名字还没想好☜24 分钟前
Python 用 tempfile 安全创建临时文件:NamedTemporaryFile、TemporaryDirectory 与别自己拼 /tmp 的坑
开发语言·后端·python·安全·编程语言
lvshuocool28 分钟前
golang 多版本管理工具 -- g
开发语言·后端·golang
matlab代码32 分钟前
基于matlab数字图像处理的指纹识别系统 点线特征(GUI界面)【源码69期】
开发语言·matlab
Doubbbbbbble云32 分钟前
基于空间局部性的排序算法性能重构思路4
java·重构·排序算法
linx29538 分钟前
单元三 · 那 C 的底层知识怎么办
c语言·开发语言·数据结构·c++·嵌入式硬件
MetaLite1 小时前
Java 时间处理的两个坑:单位误判,半秒算成一秒
java·开发语言·前端