⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 码农参上 「Dr Hydra」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

八股文背多了,相信大家都听说过一个词,SPI 扩展

有的面试官就很喜欢问这个问题,SpringBoot 的自动装配是如何实现的?

基本上,你一说是基于 spring 的 SPI 扩展机制,再把spring.factories文件和EnableAutoConfiguration提一下,那么这个问题就答的八九不离十了。

就像四五年前,我去面试的时候被问到这个问题,SPI 动态扩展机制这几个词从嘴里一说出来,就把面试官唬的一愣一愣的。可能他们也没见过这么能装逼的,一句话能简简单单说明白,非要拽一个听上去很高大上的词。

话说回来,被唬住的可不止是面试官,其实还有我自己。至于 SPI 扩展究竟是个啥,是怎么实现的,我当时也根本不明白。

不过现在的面试就是这样,对线八股文,要想唬住面试官,就得先唬住自己。

那么我们今天暂且不提 spring 的 SPI 扩展,先来看看 java 本身自带的 SPI 扩展机制是怎么一回事。

1、简介

SPI 的全称是Service Provider Interface,翻译过来就是服务提供者的接口,它所实现的其实是一种服务的发现机制。

这么说起来可能还是有点不好理解,我举个例子来类比一下。

在 spring 项目中,写 service 层代码前,会约定俗成的会添加一个接口层。然后通过 spring 中的依赖注入,可以借助@Autowired等方式注入这个接口的实现类的实例对象,之后对于 service 的调用一般也基于接口操作。

简单形容就是这样的:

如图所示,接口、实现类都是由服务提供方提供,我们可以把 controller 看作服务调用者,调用方只管调用接口就可以了。

虽然也有声音认为,大部分情况下 service 只有一个实现类,接口层显得有些多余。但是在《Head First Design Patterns》这本书中,大佬们还是建议过:

Program to an interface, not an implementation.

没错,就是常说的要面向接口编程。至于好处,也不外乎是降低耦合度、方便日后扩展、提高了代码的灵活性和可维护性等等。

在上面这个例子里,这个接口层和其中的方法我们可以称之为API,而我们要讨论的SPI和它相比,有类似也有差异,还是先看图:

简单来说,就是服务的调用方定义一个接口规范,可以由不同的服务提供者实现。并且,调用方能够通过某种机制来发现服务提供方,并通过接口调用它的能力。

通过对比,我们可以看出它们虽然都有着接口这一层面,但还是有很大的不同:

API 中的接口是服务提供者给服务调用者的一个功能列表,而 SPI 中更多强调的是,服务调用者对服务实现的一种约束,服务提供者根据这种约束实现的服务,可以被服务调用者发现。

说白了,Java 中的 SPI 实现的就是,你按我的接口规范实现服务,我就能通过某种机制为这个接口寻找到这个服务。

这么说起来可能还有些抽象,下面我们举一个例子,类比具体描述一下这个过程。

2、定义接口

说起智能家居系统,大家现在都比较熟悉了,只要是相同品牌下的产品,连上 wifi 就能够通过手机 app 控制了,非常方便。

虽然产品不断更新换代,型号更新层出不穷,但是同种家电在 app 上操作起来,功能一般都是一样的。就拿空调来说,我们在 app 上操作起来一般也就三个主要功能:开关选模式调节温度

假设我现在在客厅、卧室、书房安装了 3 款不同型号的空调,并把它们都接入到了我 app 中,那么之后的操作都是相同的几个按键,简单粗暴。

思考一下,无论是开关还是调温,都是通过 app 去调用设备的接口罢了,那么如果不同型号的空调各写各的接口,后端 app 在开发的时候光对接接口都麻烦的要死。

解决方法也很简单,我先定义一套接口规范,不管你以后什么型号的空调,都按我的规范来实现接口。以后只要我能发现你的设备,那么都可以按相同的方法来调用接口。

那么下面就先来定义这么一套接口规范,如果你以后想要接入智能家居系统,那么就要遵循这个规范来开发接口。

新建一个项目作为标准,就叫aircondition-standard好了,然后创建一个接口。除了 3 个操作以外,我们再添加一个获取空调型号的方法。

public interface IAircondition {
// 获取型号
String getType();

// 开关
void turnOnOff();

// 调节温度
void adjustTemperature(int temperature);

// 模式变更
void changeModel(int modelId);
}

这个接口后面要给服务的实现方来使用,用 maven 把它打成 jar 包:

mvn clean install

之后服务提供者在项目中就可以引入这个 jar 包了,有了这套规范,就保证了产品后期不管怎么更新换代,都能接入到系统来。

3、服务实现

制定并发布完规则后,挂式空调作为第一个服务提供者就来了,新建一个项目aircondition-hanging-type,并引入刚才打好的 jar 包:

<dependency>
<groupId>com.cn.hydra</groupId>
<artifactId>aircondition-standard</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

创建服务类,并实现前面定义的接口:

public class HangingTypeAircondition
implements IAircondition{
public String getType() {
return "HangingType";
}

public void turnOnOff() {
System.out.println("挂式空调开关");
}

public void adjustTemperature(int i) {
System.out.println("挂式空调调节温度");
}

public void changeModel(int i) {
System.out.println("挂式空调更换模式");
}
}

在项目的resources的目录下,创建META-INF/services目录,然后以前面定义的接口名com.cn.hydra.IAircondition创建文件,并在文件中写入实现类的全限定名。

com.cn.hydra.HangingTypeAircondition

整个项目结构非常简单:

这样,一个服务方的简单实现就搞定了,用 maven 打成 jar 包,之后就可以提供给调用方使用了。

同理,我们可以再创建一个立式空调的项目aircondition-vertical-type,也只创建一个服务类:

public class VerticalTypeAircondition
implements IAircondition{
public String getType() {
return "VerticalType";
}

public void turnOnOff() {
System.out.println("立式空调开关");
}

public void adjustTemperature(int i) {
System.out.println("立式空调调节温度");
}

public void changeModel(int i) {
System.out.println("立式空调更换模式");
}
}

还是按上面的命名规则,创建一个配置文件:

com.cn.hydra.VerticalTypeAircondition

同样,打成 jar 包就完事了,至于服务调用者如何去发现和调用这两个服务,下面详细再说。

4、服务发现

现在两个服务提供方都实现了接口,下面关键的一步就是服务发现,这一步 java 中的 spi 发现机制已经帮我们实现好了。

创建一个新项目aircondition-app,引入上面打好的两个 jar 包。

<dependencies>
<dependency>
<groupId>com.cn.hydra</groupId>
<artifactId>aircondition-hanging-type</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>com.cn.hydra</groupId>
<artifactId>aircondition-vertical-type</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

按照上面的说法,虽然每个服务提供者对于接口都有不同的实现,但是作为调用者来说,它并不需要关心具体的实现类,我们要做的是通过接口来调用服务提供者实现的方法。

下面,就是关键的服务发现环节,我们写一个方法,根据型号去调用对应空调的开关方法。

public class AirconditionApp {
public static void main(String[] args) {
new AirconditionApp().turnOn("VerticalType");
}

public void turnOn(String type){
ServiceLoader<IAircondition> load = ServiceLoader
.load(IAircondition.class);

for (IAircondition iAircondition : load) {
System.out.println("检测到:"+iAircondition.getClass().getSimpleName());
if (type.equals(iAircondition.getType())){
iAircondition.turnOnOff();
}
}
}
}

测试结果:

可以看到,测试过程中,通过定义的接口IAircondition发现了两个实现类,并通过参数,调用了特定实现类的某个方法。整段代码中没有出现过具体的服务实现类,操作都是通过接口调用。

5、原理

了解了 spi 的工作流程,我们再来看看它的实现,其实最关键的就是上面代码中出现的ServiceLoader这个类。

上面的示例代码中,对于ServiceLoaderload()方法的结果,我们用for循环进行了遍历,这一点我们看一下源码就能明白,因为ServiceLoader实现了Iterable这一接口,而整个服务发现的核心,就在它的iterator()方法中。

注意这里面有两个关键的东西,找一下在源码中定义的地方:

注释写的非常明白,providers就是一个缓存,在迭代器中如果先从这里面进行查找,如果里面有就继续往下找,没有了的话就用这个懒加载的lookupIterator查找。

那么就简单了,接着往下看LazyIterator,看看它里面的hasNext()next()两个方法是怎么实现的。

这个acc是一个安全管理器,在前面通过System.getSecurityManager()判断并赋值,debug 看一下这里都是null,所以直接看hasNextService()nextService()方法就可以了。

hasNextService()方法中,会取出接口取出实现类的类名放到nextName中:

接下来,在nextService()方法中,则会先加载这个实现类,然后实例化对象,最终放入缓存中去。

在迭代器的迭代过程中,会完成所有实现类的实例化,其实归根结底,还是基于 java 反射去实现的。

6、应用

要说 spi 的实际应用,大家最常见的应该就是日志框架slf4j了,它利用 spi 实现了插槽式接入其他具体的日志框架。

说白了,slf4j本身就是个日志门面,并不提供具体的实现,需要绑定其他具体实现才能真正的引入日志功能。

例如我们可使用log4j2作为具体的绑定器,只需要在 pom 中引入slf4j-log4j12,就可以使用具体功能。

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>2.0.3</version>
</dependency>

引入项目后,点开它的 jar 包看一下具体结构:

有没有发现一个彩蛋,先说为什么我们 pom 中引入的明明是slf4j-log4j12,实际上引入的是slf4j-reload4j?翻一下官网的文档:

大意就是在 2015 年和 2022 年,log4j1.x就已经宣布end of life终止了,原因也不难猜,估计是因为频繁爆出的漏洞。在那之后,slf4j-log4j在构建阶段就会自动重定向到slf4j-reload4j了,并且官方也强烈建议使用slf4j-reload4j作为替代。

再回头看一下 jar 包的META-INF.services里面,通过 spi 注入了Reload4jServiceProvider这个实现类,它实现了SLF4JServiceProvider这一接口,在它的初始化方法initialize()中,会完成初始化等工作,后续可以继续获取到LoggerFactoryLogger等具体日志对象。

7、总结

Java 中的 SPI 提供了一种比较特别的服务发现和调用机制,通过接口灵活的将服务调用与服务提供者分离,用于提供给第三方实现扩展时还是很方便的。但是也有缺点,比方说一旦加载一个接口,就会把所有实现类都加载进来,可能会加载到不需要的冗余服务。不过站在整体角度上,还是给我们提供了一种非常不错的框架扩展、集成的思路。

文章目录
  1. 1. 1、简介
  2. 2. 2、定义接口
  3. 3. 3、服务实现
  4. 4. 4、服务发现
  5. 5. 5、原理
  6. 6. 6、应用
  7. 7. 7、总结