SPI 概念及案例实践(上)
SPI 概念
面向对象设计鼓励模块间基于接口而非具体实现编程,以降低模块间的耦合,遵循依赖倒置原则,并支持开闭原则(对扩展开放,对修改封闭)。
SPI(Service Provider Interface)服务提供了一种服务发现机制,允许在程序外部动态指定具体实现。这与控制反转(IoC)的思想相似,将组件装配的控制权移交给了程序之外。SPI 主要用于实现模块化开发和插件化扩展。
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
设计思想:
- 面向接口
- 配置文件
- 反射技术
- 策略模式
本文源代码详见文末。
SPI 和 API 的区别
广义上来说它们都是接口。
一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。
- 当接口存在于调用方这边时,这就是 SPI 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。
例子感受 SPI
我们使用的主流 Java 开发框架中,几乎都使用到了 SPI 机制,比如 Servlet 容器、日志框架、ORM 框架、Spring 框架、Dubbo 的扩展实现。
例子 1:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。
例子 2:SLF4J (Simple Logging Facade for Java)是 Java 的一个日志门面(接口),其具体实现有几种,比如:Logback、Log4j、Log4j2 等等,而且还可以切换,在切换日志具体实现的时候我们是不需要更改项目代码的,只需要在 Maven 依赖里面修改一些 pom 依赖就好了。
例子 3:一个典型的 SPI 应用场景是 JDBC(Java 数据库连接库),不同的数据库驱动程序开发者可以使用 JDBC 库,然后定制自己的数据库驱动程序。
Java SPI 机制
一个标准的 SPI,由 3 个组件构成,分别是:
- 1️⃣Service:一个公开的接口或抽象类,定义了一个抽象的功能模块。
- 2️⃣Service Provider:是 Service 接口的实现类
- 3️⃣ServiceLoader:SPI 机制中的核心组件(对应 JDK 中的 ServiceLoader 类),负责在运行时发现并加载 Service Provider
Java SPI 的规范要素:
- 规范的配置文件:
- 文件路径:必须在 JAR 包中的 META-INF/services 目录下
- 文件名称:Service 接口的全限定名
- 文件内容:Service 实现类(即 Service Provider 类)的全限定名。如果有多个实现类,那么每一个实现类在文件中单独占据一行
- Service 接口的实现类 Service Provider 类,必须具备无参的默认构造方法。因为随后通过反射技术实例化它时是不带参数的。
- 保证能加载到配置文件和 Service Provider 类。
- 方式一:将 Service Provider 的 JAR 包放到 classpath 中(最常用)
- 方式二:将 JAR 包安装到 JRE 的扩展目录中
- 方式三:自定义一个 ClassLoader
手写 SPI 应用示例
本示例的背景介绍:有一家公司 A,它需要连接互联网。它定义了一个连接网络的 API,由中国移动和中国联通来提供网络服务。
这个场景涉及到了以下角色:
- 消费者:公司 A。提供使用以下模块:
simple-company
simple-api
- 服务提供者:中国移动、中国联通
simple-isp-mobile
(中国移动提供的 jar 包)simple-isp-unicom
(中国联通提供的 jar 包)
使用 Java 的 ServiceLoader
公司 A 在 simple-api
提供的服务接口(这就是 SPI 中 1️ Service 接口部分 1️⃣):
1 | package top.uuanqin; |
服务提供者中国移动在 simple-isp-mobile
在 pom.xml
中导入了 simple-api
,并为其编写了实现类(北京移动 BeijingMobile
和苏州移动 SuzhouMobile
的联网服务,这些就是 SPI 中的 Service Provider 部分 2️⃣):
1 | <dependencies> |
1 | package cn.mobile; |
中国移动在配置目录中放置 SPI 配置文件,文件名是 Service 接口的全限定名,内容是 Service Provider 类的全限定名。多行表示多个 Service Provider。
1 | cn.mobile.BeijingMobile |
中国移动 simple-isp-mobile
目录结构如下:
同样的,服务提供者中国联通 simple-isp-unicom
也编写了实现类,假设它只编写了一个实现类 ChinaUnicom
。
公司 A 在实际的应用项目 simple-company
中使用了上网服务(包含 SPI 的 ServiceLoader 部分 3️⃣):
1 | package top.uuanqin; |
pom.xml
中除了引用自己公司定义的 api 外,我想要使用哪个公司的上网服务,就导入哪个公司的 jar 包。
1 | <dependencies> |
运行 Main
方法结果如下:
1 | 【中国移动(北京移动)】欢迎使用中国移动联网服务! |
如果 pom.xml
导入的是中国联通的 jar 包,那么 Main
方法返回:
1 | 【中国联通】欢迎使用中国联通的无线上网服务! |
如果没打印成功,检查一下 META-INF.services
目录是一个文件夹还是两级文件夹!
META-INF.services
目录是一个文件夹还是两级文件夹!在 IDEA 中选择这个取消压缩中间文件夹名:
尝试写一个简单的 ServiceLoader
在阅读 Java ServiceLoader 源码之前,可以先看一下下面的代码进行理解。下面代码是一个简化了的 ServiceLoader。
代码引入工具包以减少编写的代码量:
1 | <!-- https://doc.hutool.cn/ --> |
1 | package top.uuanqin; |
把上例中 Main 函数的 ServiceLoader
换成 MyServiceLoader
将得到同样的结果。
更定制化的 ServiceLoader 实现以及相关应用场景详见这篇文章:SPI 概念及案例实践(下)
【拓展】Java SPI 与 SpringBoot 的自动配置
SpringBoot 自动配置,即大名鼎鼎的 Auto-Configuration 是指基于你引入的依赖 Jar 包,对 SpringBoot 应用进行自动配置。提供了自动配置功能的依赖 Jar 包通常被称为 starter,例如 mybatis-spring-boot-starter
等等。
SpringBoot 与 jdk 在 SPI 机制上,存在些许的差别,但本质上还是事先定义一套规范,来完成对实现类或者组件的动态发现。
- 在获取实现类名称集合的层面上,SpringBoot 借助于 SpringFactoriesLoader 加载
spring.factories
配置文件,而 jdk 借助于 ServiceLoader 读取指定路径。 - 在是否实例化实现类的层面上,SpringBoot 会依据
@Conditional
注解来判断是否进行实例化并注入进容器中,而 jdk 会在next()
方法内部懒加载实现类。
更多参考详见本站文章:SpringBoot 的原理以及写一个自定义 Starter
源代码
GitHub 仓库地址:uuanqin/SPI-simple-example (github.com)