网易面试:什么是SPI,SPI和API有什么区别?

说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:

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

最近有小伙伴在面网易,又遇到了相关的面试题。小伙伴懵了, 他从来没有用过SPI,SO,面挂了。

所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V119版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注公众号【技术自由圈】获取

文章目录

    • 说在前面
    • 何谓 SPI?
    • Java SPI 的应用Demo
    • SPI 使用场景
    • SPI 和 API 在使用上的区别?
    • SPI 和 API 在本质上的区别
    • SPI 源码分析
      • 1、SPI的核心就是`ServiceLoader.load()`方法
      • 2、ServiceLoader核心代码介绍
    • SPI 的优缺点?
    • 说在最后
    • 推荐阅读

何谓 SPI?

SPI 即 Service Provider Interface ,字面意思就是: “服务提供者的接口”,一般理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。

SPI 的合作作用: 解耦。

SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。

很多框架都使用了 Java 的 SPI 机制,比如: Spring 框架、数据库加载驱动、日志接口、以及Dubbo 的扩展实现等等。

Java SPI 的应用Demo

Java SPI机制

Java SPI机制

Java SPI 是JDK内置的一种服务提供发现机制。

我们一般希望模块直接基于接口编程,调用服务不直接硬编码具体的实现,而是通过为某个接口寻找服务实现的机制,通过它就可以实现,不修改原来jar的情况下, 为 API 新增一种实现。

Java SPI 有点类似 IOC 的思想,将装配的控制权移到了程序之外。

对于 Java 原生 SPI,只需要满足下面几个条件:

  • 1.定义服务的通用接口,针对通用的服务接口,提供具体的实现类

  • 2.在 src/main/resources/META-INF/services 或者 jar包的 META-INF/services/ 目录中,新建一个文件,文件名为 接口的全名。 文件内容为该接口的具体实现类的全名

  • 3.将 spi 所在 jar 放在主程序的 classpath 中

  • 4.服务调用方用java.util.ServiceLoader,用服务接口为参数,去动态加载具体的实现类到JVM中,然后就可以正常使用服务了

上面这一大段代码示例如下

1.接口和实现类

接口

public interface DemoService {void sayHello();
}

实现类

public class RedService implements DemoService{@Overridepublic void sayHello() {System.out.println("red");}
}
public class BlueService implements DemoService{@Overridepublic void sayHello() {System.out.println("blue");}
}

2.配置文件

META-INF/services文件夹下,路径名字一定分毫不差写对,配置文件名com.example.demo.spi.DemoService

文件内容

com.example.demo.spi.RedService
com.example.demo.spi.BlueService

3.jar包例如jdbc的需要导入classpath,我们这个示例程序自己写的代码就不用了

4.实际调用

public class ServiceMain {public static void main(String[] args) {ServiceLoader<DemoService> spiLoader = ServiceLoader.load(DemoService.class);Iterator<DemoService> iteratorSpi = spiLoader.iterator();while (iteratorSpi.hasNext()) {DemoService demoService = iteratorSpi.next();demoService.sayHello();}}
}

调用结果

red
blue

Java SPI 实际上是“基于接口的编程+ 配置文件”组合实现的动态加载机制。

SPI 有点类似 Spring IoC容器, 用于加载实例。

在 Spring IoC 容器中具有以下几种作用域:

  • singleton:单例模式,在整个Spring IoC容器中,使用singleton定义的Bean将只有一个实例,适用于无状态bean;
  • prototype:原型模式,`每次通过容器的getBean方法获取prototype定义的Bean时,都将产生一个新的Bean实例,适用于有状态的Bean;

但是SPI 与Spring 不同:

  • SPI 缺少实例的维护,作用域没有定义singleton和prototype的定义,不利于用户自由定制。

  • ServiceLoader不像 Spring,只能一次获取所有的接口实例, 不支持排序,随着新的实例加入,会出现排序不稳定的情况

SPI 使用场景

很多开源第三方jar包都有基于SPI的实现,在jar包META-INF/services中都有相关配置文件。

如下几个常见的场景:

1)JDBC加载不同类型的数据库驱动

2)Slf4j日志框架

3)Dubbo框架

看看 Dubbo 的扩展实现,就知道 SPI 机制用的多么广泛:

SPI 和 API 在使用上的区别?

那 SPI 和 API 有啥区别?

SPI 全称:Service Provider Interface , 服务提供接口

API 全称:Application Programming Interface, 即应用程序编程接口

说到 SPI 就不得不说一下 API 了,从广义上来说它们都属于接口,而且很容易混淆。

下面先用一张图说明一下:

一般模块之间都是通过接口进行通讯,

那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。

当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。

当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根绝这个规则对这个接口进行实现,从而提供服务。

SPI 和 API 在本质上的区别

SPI 区别于API模式,本质是一种服务接口规范定义权的转移,从服务提供者转移到服务消费者。

怎么理解呢?

API指: Provider 定义接口

服务提供方定义接口规范并按照接口规范完成服务具体实现,消费者需要遵守提供者的规则约束,否则无法消费

SPI指:consumer 定义接口

由消费方定义接口规范,服务提供者需要按照消费者定义的规范完成具体实现。否则无法消费。

SPI从理论上看,是一种接口定义和实现解耦的设计思路,以便于框架的简化和抽象;从实际看,是让服务提供者把接口规范定义权交岀去,至于交给谁是不一定的。

SPI定义权可以是服务消费者,也可以是任何一个第三方。一旦接口规范定义以后,只有消费者和服务提供者都遵循接口定义,才能匹配消费。

两者唯一的差别,在于服务提供者和服务消费者谁更加强势,仅此而已。

举个不恰当的例子:A国是C国工业制成品的消费国,C国只能提供相比A国更具性价比的产品,担心生产的产品会无法在A国销售。这时候,生产者必须遵守A国的生产标准。

谁有主动权,谁就有标准的制定权。在系统架构层面:谁是沉淀通用能力的平台方,谁就是主动权一方。

SPI 源码分析

1、SPI的核心就是ServiceLoader.load()方法

总结如下:

  1. 调用ServiceLoader.load(),创建一个ServiceLoader实例对象
  2. 创建LazyIterator实例对象lookupIterator
  3. 通过lookupIterator.hasNextService()方法读取固定目录META-INF/services/下面service全限定名文件,放在Enumeration对象configs
  4. 解析configs得到迭代器对象Iterator<String> pending
  5. 通过lookupIterator.nextService()方法初始化读取到的实现类,通过Class.forName()初始化

从上面的步骤可以总结以下几点

  1. 实现类工程必须创建定目录META-INF/services/,并创建service全限定名文件,文件内容是实现类全限定名
  2. 实现类必须有一个无参构造函数

2、ServiceLoader核心代码介绍

public final class ServiceLoader<S>implements Iterable<S>
{private static final String PREFIX = "META-INF/services/";// The class or interface representing the service being loadedprivate final Class<S> service;// The class loader used to locate, load, and instantiate providersprivate final ClassLoader loader;// The access control context taken when the ServiceLoader is createdprivate final AccessControlContext acc;// Cached providers, in instantiation orderprivate LinkedHashMap<String,S> providers = new LinkedHashMap<>();// The current lazy-lookup iteratorprivate LazyIterator lookupIterator;public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader)
{return new ServiceLoader<>(service, loader);
}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();
}

通过方法iterator()生成迭代器,内部调用LazyIterator实例对象

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();}};
}

内部类LazyIterator,读取配置文件META-INF/services/

private class LazyIteratorimplements Iterator<S>{Class<S> service;ClassLoader loader;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 {String fullName = PREFIX + service.getName();if (loader == null)configs = ClassLoader.getSystemResources(fullName);elseconfigs = loader.getResources(fullName);} catch (IOException x) {fail(service, "Error locating configuration files", x);}}while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return false;}pending = parse(service, configs.nextElement());}nextName = pending.next();return true;}private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn = nextName;nextName = null;Class<?> c = null;try {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 {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();}
}

SPI 的优缺点?

通过 SPI 机制能够大大地提高接口设计的灵活性,

但是 SPI 机制也存在一些缺点,比如:

  • 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
  • 当多个 ServiceLoader 同时 load 时,会有并发问题。
  • SPI 缺少实例的维护,作用域没有定义singleton和prototype的定义,不利于用户自由定制。
  • ServiceLoader不像 Spring,只能一次获取所有的接口实例, 不支持排序,随着新的实例加入,会出现排序不稳定的情况,作用域没有定义singleton和prototype的定义,不利于用户自由定制

说在最后

SPI 面试题,是非常常见的面试题。

以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。

在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,并且在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。

最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。

推荐阅读

《百亿级访问量,如何做缓存架构设计》

《多级缓存 架构设计》

《消息推送 架构设计》

《阿里2面:你们部署多少节点?1000W并发,当如何部署?》

《美团2面:5个9高可用99.999%,如何实现?》

《网易一面:单节点2000Wtps,Kafka怎么做的?》

《字节一面:事务补偿和事务重试,关系是什么?》

《网易一面:25Wqps高吞吐写Mysql,100W数据4秒写完,如何实现?》

《亿级短视频,如何架构?》

《炸裂,靠“吹牛”过京东一面,月薪40K》

《太猛了,靠“吹牛”过顺丰一面,月薪30K》

《炸裂了…京东一面索命40问,过了就50W+》

《问麻了…阿里一面索命27问,过了就60W+》

《百度狂问3小时,大厂offer到手,小伙真狠!》

《饿了么太狠:面个高级Java,抖这多硬活、狠活》

《字节狂问一小时,小伙offer到手,太狠了!》

《收个滴滴Offer:从小伙三面经历,看看需要学点啥?》

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/144620.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

STM32内部flash闪存的总结

最近在做无人船和机巢远程在线升级的项目&#xff0c;牵扯到flash的操作&#xff0c;特此记录&#xff0c;便于以后查找。IMU也用到过&#xff0c;当时没记录 具体细节看 E:\Documets\AY\a-project\IMU\IMU16500\S0IMU v3.3 study\User\Driver\source eeprom.c E:\Documets\A…

蓝桥杯每日一题2023.10.22

题目描述 灵能传输 - 蓝桥云课 (lanqiao.cn) 题目分析 发现每一次的灵能传输都是对前缀和s[i - 1]和s[i]的一次交换 故为求max(s[i], s[i - 1])的最小值&#xff08;发现当s单调时可以成立&#xff09; 由于s[0]和s[n]的位置不变&#xff0c;但是s[0]和s[n]不一定是最大值或…

怎么写一个可以鼠标控制旋转的div?

说在前面 鼠标控制元素旋转在现在也是一个很常见的功能&#xff0c;让我们从实现div元素的旋转控制开始来了解元素旋转的具体原理和实现方法吧。 效果展示 体验地址 https://code.juejin.cn/pen/7290719197439459386 实现步骤 画一个div 首先我们需要先画一个div&#xff0…

模板再认识

在前面的文章中我写了关于模板的一些简单的认识&#xff0c;现在我们来再次认识模板文章目录 1.非类型模板参数2.模板特化1). 模板特化的写法2). 类模板特化3). 函数模板特化4). 模板全/偏特化 3.模板分离编译 1.非类型模板参数 在模板中还有一种是非类型的模板参数。我们代码…

人工智能(6):机器学习基础环境安装与使用

1 库的安装 整个机器学习基础阶段会用到Matplotlib、Numpy、Pandas等库&#xff0c;为了统一版本号在环境中使用&#xff0c;将所有的库及其版本放到了文件requirements.txt当中&#xff0c;然后统一安装 新建一个用于人工智能环境的虚拟环境 mkvirtualenv ai matplotlib3.8…

DALL·E 3:OpenAI的革命性图像生成模型与ChatGPT的融合

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

Unity⭐️Win和Mac安卓打包环境配置

文章目录 🟥 配置Android SDK1️⃣ 配置 SDK Platforms2️⃣ 配置 SDK Tools🎁 Android SDK Build-Tools🎁 Android SDK Command-line Tools(latest)🎁 Android SDK Tools(Obsolete)🟧 配置NDK🟩 配置JDK前情提示: 此方法适用于Windows/Mac 在配置时注意开启 🪜 …

ubuntu安装Anaconda

下载 Anaconda 进入 Ubuntu&#xff0c;自己新建下载路径&#xff0c;输入以下命令开始下载 注意&#xff0c;如果不是 x86_64&#xff0c;需要去镜像看对应的版本&#xff08;https://mirrors.bfsu.edu.cn/anaconda/archive/?CM&OA&#xff09; wget https://mirrors.…

蓝桥杯中级题目之组合(c++)

系列文章目录 数位递增数_睡觉觉觉得的博客-CSDN博客拉线开关。_睡觉觉觉得的博客-CSDN博客蓝桥杯中级题目之数字组合&#xff08;c&#xff09;_睡觉觉觉得的博客-CSDN博客 文章目录 系列文章目录前言一、个人名片二、描述三、输入输出以及代码示例1.输入2.输出3.代码示例 总…

怎么从A和B仓库执行分别fetch操作?

目录 1.问题2.描述3.解决问题 1.问题 我希望从A仓库拉代码后推送到B仓库&#xff0c;结果A仓库代码新增分支后 在执行fetch时默认仓库地址为B仓库&#xff0c;导致fetch失败。 2.描述 在实际项目开发中我们可能会出现需要将同一个服务的代码推送到不同的代码仓库&#xff0c…

【C++】C++11新特性之右值引用与移动语义

文章目录 一、左值与左值引用二、右值与右值引用三、 左值引用与右值引用比较四、右值引用使用场景和意义1.左值引用的短板2.移动构造和移动赋值3.STL中右值引用的使用 五、万能引用与完美转发1.万能引用2.完美转发 一、左值与左值引用 在C11之前&#xff0c;我们把数据分为常…

开源博客项目Blog .NET Core源码学习(5:mapster使用浅析)

开源博客项目Blog使用mapster框架映射对象&#xff0c;主要是在数据库表对象及前端数据对象之间进行映射&#xff0c;本文学习并记录项目中mapster的使用方式。   App.Hosting项目的program文件中调用builder.Services.AddMapper函数进行对象模型自动映射&#xff0c;而该函数…