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

快来打我* 2024-02-21 10:41 123阅读 0赞

说在前面

在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.接口和实现类

接口

  1. public interface DemoService {
  2. void sayHello();
  3. }

实现类

  1. public class RedService implements DemoService{
  2. @Override
  3. public void sayHello() {
  4. System.out.println("red");
  5. }
  6. }
  7. public class BlueService implements DemoService{
  8. @Override
  9. public void sayHello() {
  10. System.out.println("blue");
  11. }
  12. }

2.配置文件

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

文件内容

  1. com.example.demo.spi.RedService
  2. com.example.demo.spi.BlueService

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

4.实际调用

  1. public class ServiceMain {
  2. public static void main(String[] args) {
  3. ServiceLoader<DemoService> spiLoader = ServiceLoader.load(DemoService.class);
  4. Iterator<DemoService> iteratorSpi = spiLoader.iterator();
  5. while (iteratorSpi.hasNext()) {
  6. DemoService demoService = iteratorSpi.next();
  7. demoService.sayHello();
  8. }
  9. }
  10. }

调用结果

  1. red
  2. blue

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

e55318f4154143fe84745f2db0ee62e6.png

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 机制用的多么广泛:

a46724982f09422cb703e32bda3bfe4e.png

SPI 和 API 在使用上的区别?

那 SPI 和 API 有啥区别?

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

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

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

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

a3ff041d53984a26ae6a60952180d2df.png

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

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

当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 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核心代码介绍

  1. public final class ServiceLoader<S>
  2. implements Iterable<S>
  3. {
  4. private static final String PREFIX = "META-INF/services/";
  5. // The class or interface representing the service being loaded
  6. private final Class<S> service;
  7. // The class loader used to locate, load, and instantiate providers
  8. private final ClassLoader loader;
  9. // The access control context taken when the ServiceLoader is created
  10. private final AccessControlContext acc;
  11. // Cached providers, in instantiation order
  12. private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
  13. // The current lazy-lookup iterator
  14. private LazyIterator lookupIterator;
  15. public static <S> ServiceLoader<S> load(Class<S> service,
  16. ClassLoader loader)
  17. {
  18. return new ServiceLoader<>(service, loader);
  19. }
  20. public void reload() {
  21. providers.clear();
  22. lookupIterator = new LazyIterator(service, loader);
  23. }
  24. private ServiceLoader(Class<S> svc, ClassLoader cl) {
  25. service = Objects.requireNonNull(svc, "Service interface cannot be null");
  26. loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
  27. acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
  28. reload();
  29. }

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

  1. public Iterator<S> iterator() {
  2. return new Iterator<S>() {
  3. Iterator<Map.Entry<String,S>> knownProviders
  4. = providers.entrySet().iterator();
  5. public boolean hasNext() {
  6. if (knownProviders.hasNext())
  7. return true;
  8. return lookupIterator.hasNext();
  9. }
  10. public S next() {
  11. if (knownProviders.hasNext())
  12. return knownProviders.next().getValue();
  13. return lookupIterator.next();
  14. }
  15. public void remove() {
  16. throw new UnsupportedOperationException();
  17. }
  18. };
  19. }

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

  1. private class LazyIterator
  2. implements Iterator<S>
  3. {
  4. Class<S> service;
  5. ClassLoader loader;
  6. Enumeration<URL> configs = null;
  7. Iterator<String> pending = null;
  8. String nextName = null;
  9. private LazyIterator(Class<S> service, ClassLoader loader) {
  10. this.service = service;
  11. this.loader = loader;
  12. }
  13. private boolean hasNextService() {
  14. if (nextName != null) {
  15. return true;
  16. }
  17. if (configs == null) {
  18. try {
  19. String fullName = PREFIX + service.getName();
  20. if (loader == null)
  21. configs = ClassLoader.getSystemResources(fullName);
  22. else
  23. configs = loader.getResources(fullName);
  24. } catch (IOException x) {
  25. fail(service, "Error locating configuration files", x);
  26. }
  27. }
  28. while ((pending == null) || !pending.hasNext()) {
  29. if (!configs.hasMoreElements()) {
  30. return false;
  31. }
  32. pending = parse(service, configs.nextElement());
  33. }
  34. nextName = pending.next();
  35. return true;
  36. }
  37. private S nextService() {
  38. if (!hasNextService())
  39. throw new NoSuchElementException();
  40. String cn = nextName;
  41. nextName = null;
  42. Class<?> c = null;
  43. try {
  44. c = Class.forName(cn, false, loader);
  45. } catch (ClassNotFoundException x) {
  46. fail(service,
  47. "Provider " + cn + " not found");
  48. }
  49. if (!service.isAssignableFrom(c)) {
  50. fail(service,
  51. "Provider " + cn + " not a subtype");
  52. }
  53. try {
  54. S p = service.cast(c.newInstance());
  55. providers.put(cn, p);
  56. return p;
  57. } catch (Throwable x) {
  58. fail(service,
  59. "Provider " + cn + " could not be instantiated",
  60. x);
  61. }
  62. throw new Error(); // This cannot happen
  63. }
  64. public boolean hasNext() {
  65. if (acc == null) {
  66. return hasNextService();
  67. } else {
  68. PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
  69. public Boolean run() {
  70. return hasNextService(); }
  71. };
  72. return AccessController.doPrivileged(action, acc);
  73. }
  74. }
  75. public S next() {
  76. if (acc == null) {
  77. return nextService();
  78. } else {
  79. PrivilegedAction<S> action = new PrivilegedAction<S>() {
  80. public S run() {
  81. return nextService(); }
  82. };
  83. return AccessController.doPrivileged(action, acc);
  84. }
  85. }
  86. public void remove() {
  87. throw new UnsupportedOperationException();
  88. }
  89. }

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,请到下面公号【技术自由圈】取↓↓↓

发表评论

表情:
评论列表 (有 0 条评论,123人围观)

还没有评论,来说两句吧...

相关阅读