手撕源码(六)Java、Spring、Dubbo三者SPI机制的原理和区别
目录
一、背景:微服务时代的扩展性挑战
在微服务架构中,系统需要频繁得进行功能扩展和模块解藕。假设你正在开发一个电商平台,需要支持多种支付方式(如支付宝、微信、银联),而这些支付方式的实现可能由不同的团队维护。**如何设计一个灵活的系统,使得新支付方式的接入无需修改核心代码?**这正是 SPI(Service Provider Interface) 机制的核心价值所在。
然而,当尝试使用 Java 原生的 SPI、SPring 的扩展机制 或 Dubbo 的分布式服务发现时,你可能会发现三者在实现方式、性能和适用场景上存在巨大差异。本文将深入解析这三种 SPI 机制的原理与区别,帮助你在世纪项目中做出更合理的技术选型。
二、SPI 介绍
2.1 什么是 SPI?
SPI 全称 Service Provider Interface,是一种动态替换发现的机制,一种解藕非常优秀的思想,SPI 可以很灵活的让接口实现分离,让 API 提供者只提供接口,第三方来实现,然后可以使用配置文件的方式来实现替换或者扩展,在框架中比较常见,提高框架的可扩展性。
简单来说 SPI 是一种非常优秀的设计思想,它的核心就是解藕、方便扩展。
2.2 三种 SPI 的定位
| 框架 | SPI类型 | 核心用途 | 扩展方式 |
|---|---|---|---|
| Java | 原生SPI | JDK标准服务发现 | META-INF/services/ |
| Spring | 扩展点机制 | 应用上下文扩展(如监听器) | spring.factories |
| Dubbo | 分布式SPI | 服务治理与协议扩展 | META-INF/services/、META-INF/dubbo/internal/ 、META-INF/dubbo/ 、META-INF/dubbo/external/ |
三、Java SPI 机制–ServiceLoader
3.1 使用约定
ServiceLoader 是 Java 提供的一种简单的 SPI 机制的是心啊,Java 的 SPI 实现约定了以下两件事:
- 文件必须放在
META-INF/services目录底下; - 文件名必须为接口的全限定名,内容为接口实现的全限定名。
这样就能够通过 ServiceLoader 加载到文件中接口的实现。
3.1 来个 demo
第一步,需要一个接口以及它的实现类:
package com.demo.java;
public interface LoadBalance {
}
package com.demo.java.impl;
import com.demo.java.LoadBalance;
public class RandomLoadBalance implements LoadBalance {
}
第二步,在 META-INF/services/ 目录创建一个文件名 LoadBalance 全限定名的文件,文件内容为 RandomLoadBalance 的全限定名,如下所示:
测试类:
import java.util.Iterator;
import java.util.ServiceLoader;
public class ServiceLoaderDemo {
public static void main(String[] args) {
ServiceLoader<LoadBalance> loadBalanceServiceLoader = ServiceLoader.load(LoadBalance.class);
Iterator<LoadBalance> iterator = loadBalanceServiceLoader.iterator();
while (iterator.hasNext()) {
LoadBalance loadBalance = iterator.next();
System.out.println("获取到负载均衡策略:" + loadBalance);
}
}
}
测试结果:
此时就成功获取到了实现实例。
在实际的架构设计中,上面这段测试代码其实是框架作者写到框架内不的,而对于框架的使用者来说,要想自定义 LoadBalance 实现,嵌入到框架。仅仅只需要些接口的实现和 SPI 文件即可。
3.3 实现原理
如下是 Service Loader 中的一段核心代码:
首先获取一个 fullName,其实就是 META-INF/services/接口的全限定名:
然后通过 ClassLoader 获取到资源,其实就是接口的全限定名赌赢的资源,然后交给 parse() 方法解析资源:
parse()方法其实就是通过 IO流读取文件的内容,这样就可以获取到接口实现的全限定名。
再后面其实就是通过反射实例化对象,这里就不展示了。
所以其实不难发现 ServiceLoader 实现原理比较简单,总结起来就是:
3.4 优缺点
由于 Java 的 SPI 机制实现的比较简单,所以它也有一些缺点:
- 第一点就是 浪费资源,虽然例子中只会有一个实现类,但是实际情况下可能会有很多实现类。
- 第二点就是 无法区分具体的实现,也就是这么多实现类,到底该用哪个实现呢?如果要判断具体使用哪个,只能依靠接口本身的设计,比如接口可以设计为一个策略接口,又或者接口可以设计带有优先级的,但是不论怎样设计,框架作者都得写代码进行判断。
所以总的来说就是 ServiceLoader 无法做到按需加载或者按需获取某个具体的实现。
3.5 使用场景
虽然说 ServiceLoader 可能有些缺点,但还是有使用场景的,比如说:
- 场景一:不需要选择具体的实现,每个被加载的实现都需要被用到。
- 场景二:虽然需要选择具体的实现,但是可以通过对接口的设计来解决。
四、Spring SPI 机制–SpringFactoriesLoader
4.1 使用约定
Spring 我们都不陌生,他也提供了一种 SPI 的实现 SpringFactoriesLoader。
Spring 的 SPI 的约定如下:
- 配置文件必须在
META-INF/目录下,文件名必须为spring.factories; - 文件内容为键值对,一个键可以有多个值,只需要用逗号分隔就行,同时兼职对都需要是类的全限定名,键和值可以没有任何类与类之间的关系,当然也可以有实现的关系。
所以,也可以看出 Spring 的 SPI 机制跟 Java 的 SPI 机制 不论是文件名还是内容约定都不一样。
4.2 来个 demo
在 META-INF/ 目录下创建 spring.factories 文件,LoadBalance 为键,RandomLoadBalance 为值,如下所示:
测试:
import com.demo.java.LoadBalance;
import org.springframework.core.io.support.SpringFactoriesLoader;
import java.util.List;
public class SpringFactoriesLoaderDemo {
public static void main(String[] args) {
List<LoadBalance> loadBalances = SpringFactoriesLoader.loadFactories(LoadBalance.class, SpringFactoriesLoaderDemo.class.getClassLoader());
for (LoadBalance loadBalance : loadBalances) {
System.out.println("获取到 LoadBalance 对象:" + loadBalance);
}
}
}
Spring 依赖如下:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.3.6</version> </dependency>
运行结果:
成功获取到了实现对象。
4.3 核心原理
如下是 SpringFactoriesLoader 中的一段核心代码:

其实从这里可以看出,跟 Java 实现的差不多,只不过读的是 META-INF/spring.factories 文件内容,然后解析出来键值对。
实现原理图如下:
graph LR
A[SpringFactoriesLoader.loadFactories] --> B[查找所有 Jar 包的<br>META-INF/spring.factories]
B --> C[根据传入的接口类型<br>查找对应的 Key]
C --> D[获取所有实现类的全限定名列表]
D --> E[反射实例化并排序<br>(支持 @Order 等注解)]
E --> F[返回指定的实现类列表]
4.4 使用场景
Spring 的 SPI 机制在内部使用的非常多,尤其在 SpringBoot 中大量使用,SpringBoot 启动过程中很多扩展点都是通过 SPI 机制来实现的,这里我举两个例子。
1)自动装配
在 SpringBoot 3.0 之前的版本,自动装配是通过 SpringFactoriesLoader 来加载的。
从 SpringBoot 启动到自动装配的调用链如下:
第1层:com.example.DemoApplication.main():10
第2层:org.springframework.boot.SpringApplication.run():1234
第3层:org.springframework.boot.SpringApplication.run():1256
第4层:org.springframework.boot.SpringApplication.run():347
第5层:org.springframework.boot.SpringApplication.refreshContext():412
第6层:org.springframework.context.support.AbstractApplicationContext.refresh():589
第7层:org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors():767
第8层:org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors():138
第9层:org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors():105
第10层:org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry():332
第11层:org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions():396
第12层:org.springframework.context.annotation.ConfigurationClassParser.parse():142
第13层:org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass():321
第14层:org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass():295
第15层:org.springframework.context.annotation.ConfigurationClassParser.processImports():646
第16层:org.springframework.context.annotation.ConfigurationClassParser.processImports():698
第17层:org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.process():52
第18层:org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.getAutoConfigurationEntry():102
第19层:org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.getCandidateConfigurations():156
第20层:org.springframework.core.io.support.SpringFactoriesLoader.loadFactoryNames():229
但是 SpringBoot 3.0 之后不再使用 SpringFactoriesLoader,而是 Spring 重新从 META-INF/spring/ 目录下的 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中读取了。
至于如何读取的,其实猜也能猜到,就跟上面 SPI 机制读取的方式大概差不多,就是文件路径和名称不一样。
2)PropertySourceLoader 的加载
PropertySourceLoader 是用来解析 application 配置文件的,它是一个接口。
SpringBoot 默认提供了 PropertiesProertySourceLoader 和 YamlPropertySourceLoader 两个实现,就是对应 properties 和 yaml 文件格式的解析。
SpringBoot 在加载 PropertySourceLoader 时就用了 SPI 机制。
4.5 与 Java SPI 机制对比
首先,Spring 的 SPI 机制对 Java 的 SPI 机制进行了一些简化,Java 的 SPI 每个接口都需要对应的文件,而 Spring 的 SPI 机制只需要一个 spring.factories 文件。
其次,在内容方面,Java 的 SPI 机制文件内容必须是接口的实现类,而 Spring 的 SPI 并不要求键值对必须有什么关系,更加灵活。
最后,Spring 的 SPI 机制提供了获取类限定名的方法 loadFactoryNames,而 Java 的 SPI 机制是没有的。通过这个方法获取到类限定名之后就可以将这些类注入到 Spring 容器中,用 Spring 容器加载这些 Bean,而不仅仅是通过反射。
4.6 不足
Spring 的 API 机制也同样没有实现获取获取某个指定实现类的功能,所以要想能够找到具体的某个实现勒,还得依靠具体接口的设计。
所以不知道大家有没有发现,PropertySourceLoader 它其实就是一个策略接口,注释也有说。所以当你的配置文件是 properties 格式的时候,他可以找到解析 properties 格式的 PropertiesPropertySourceLoader 对象来解析配置文件。
五、Dubbo SPI 机制–ExtensionLoader
5.1 使用约定
ExtensionLoader 是 dubbo 的 SPI 机制的实现类。每一个接口都会有一个自己的 ExtensionLoader 实例对象,这点跟 Java 的 SPI 机制是一样的。
同样地,Dubbo 的 SPI 机制也做了以下几点约定:
- 接口必须要加
@SPI注解; - 配置文件可以放在
META-INF/sourvices/、META-INF/dubbo/internal/、META-INF/dubbo/、META-INF/dubbo/external/这四个目录底下,文件名也是接口全限定名。 - 内容为键值对,键为短名称(可以理解为 Spring 中 Bean 的名称),值为实现类的全限定名。
5.2 来个 demo
首先在 LoadBalance 接口上添加 @SPI 注解:
package com.demo.dubbo;
import com.alibaba.dubbo.common.extension.SPI;
@SPI
public interface LoadBalance {
}
package com.demo.dubbo.impl;
import com.demo.dubbo.LoadBalance;
public class RandomLoadBalance implements LoadBalance {
}
Dubbo 依赖如下:
<!-- dubbo依赖 --> <dependency> <groupId>com.alibaba.spring.boot</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency>
然后,修改一下 Java 的 SPI 机制测试时配置文件的内容,改为键值对。因为 Dubbo 的 SPI 机制也可以从 META-INF/services/ 目录下读取文件,而且配置文件也是根据接口的全限定名进行命名,所以配置文件如下:
random=com.demo.dubbo.impl.RandomLoadBalance
测试类:
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class ExtensionLoaderDemo {
public static void main(String[] args) {
ExtensionLoader<LoadBalance> extensionLoader = ExtensionLoader.getExtensionLoader(LoadBalance.class);
LoadBalance loadBalance = extensionLoader.getExtension("random");
System.out.println("获取到 random 键对应的实现类对象:" + loadBalance);
}
}
通过 ExtensionLoader 的 getExtension() 方法,传入短名称,这样就可以精确地找到短名称对应的实现类。
所以,从这里可以看出,Dubbo 的 SPI 机制解决了前面提到的无法获取指定实现类的问题。
测试结果:
Dubbo 的 SPI 机制除了解决了无法获取指定实现类的问题,还提供了很多额外的功能,这些功能在 Dubbo 内部用的非常多,接下来就详细讲讲。
5.3 dubbo 核心机制
1)自适应机制
自适应、自适应扩展类 的含义是说,基于参数,在运行时动态选择到具体的目标类,然后执行。
每个接口有且只有一个自适应类,通过 ExtensionLoader 的 getAdaptiveExtension() 方法就可以获取到这个类的对象,这个对象可以根据运行时具体的参数找到目标实现类对象,然后调用目标对象的方法。
举个例子,假设上面的 LoadBalance 有个自适应对象,那么获取到这个自适应对象之后,如果在运行期间传入了 random 这个 key,那么这个自适应对象就会找到 random 这个 key 对应的实现类,调用那个实现类的方法,如果动态传入了其它的 key,就路由到其它的实现类。
自适应类有两种方式产生,第一种就是自己指定,在接口的实现类上加 @Adaptive 注解,那么这个实现类就是自适应实现类。
@Adaptive
public class RandomLoadBalance implements LoadBalance {
}
除了自己代码指定,还有一种就是 dubbo 会根据一些条件帮你动态生成一个自适应类,生成过程比较复杂,这里就不展开了。
自适应机制在 dubbo 中用的非常多,而且很多都是自动生成的,如果你不知道 dubbo 的自适应机制,你在阅读源码的时候可能都不知道为什么代码可以走到那里。
2)IOC 和 AOP
一提到 IOC 和 AOP,立马想到的都是 Spring,但是 IOC 和 AOP 并不是 Spring 特有的概念,Dubbo 也实现 IOC 和 AOP 的功能,但是是一个轻量级的。
2.1)依赖注入
Dubbo 依赖注入是通过 setter 注入的方式,注入的对象默认就是上面提到的自适应对象,在 Spring 环境下可以注入 Spring Bean。
public class RoundRobinLoadBalance implements LoadBalance {
private LoadBalance loadBalance;
public void setLoadBalance(LoadBalance loadBalance) {
this.loadBalance = loadBalance;
}
}
如上代码,RoundeRobinLoadBalance 中有一个 setLoadBalance 方法,参数 LoadBalace,在创建 RoundRobinLoadBalance 的时候,在非 Spring 环境底下,Dubbo 就会找到 LoadBalance 自适应对象然后通过反射注入。
这种方式在 Dubbo 中也很常见,比如下面的一个场景:
RegistryProtocol 中会注入一个 Protocol,其实这个注入的 Protocol 就是一个自适应对心啊哥。
2.2)接口回调
Dubbo 也提供了一些类似于 Spring 的一些接口的回调功能,比如说,如果你的类实现了 Filter 接口,那么过滤的时候就会调用以下的方法:
在 dubbo 3.x 的某个版本之后,dubbo 提供了更多接口的回调,比如说 ExtensionPostProcessor、ExtensionAccessorAware,命名跟 Spring 的非常相似,作用也差不多。
2.3)自动包装
自动包装 其实就是 AOP 的功能实现,对目标对象进行代理,并且这个 AOP 功能在默认情况下就是开启的。
在 Dubbo 的 SPI 接口的实现中,有一种特殊的类,被称为 Wrapper 类,这个类的作用就是来实现 AOP 的。
判断 Wrapper 类的唯一标准就是这个类中必须要有这么一个构造参数,这个构造方法的参数只有一个,并且参数类型就是接口的类型,如下代码:
package com.demo.dubbo.impl;
import com.demo.dubbo.LoadBalance;
public class RoundRobinLoadBalance implements LoadBalance {
private final LoadBalance loadBalance;
public RoundRobinLoadBalance(LoadBalance loadBalance) {
this.loadBalance = loadBalance;
}
}
此时 RoundRobinLoadBalance 就是一个 Wrapper 类。
当通过 random 获取 RandomLoadBalance 目标对象时,那么默认情况下就对 RandomLoadBalance 进行包装,真正获取到的其实是 RoundRobinLoadBalance 对象,RoundRobinLoadBalance 内部引用的对象是 RandomLoadBalance。
在配置文件中加入:
roundrobin=com.demo.dubbo.impl.RoundRobinLoadBalance
测试一下:
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class ExtensionLoaderDemo {
public static void main(String[] args) {
ExtensionLoader<LoadBalance> extensionLoader = ExtensionLoader.getExtensionLoader(LoadBalance.class);
LoadBalance loadBalance = extensionLoader.getExtension("random");
System.out.println("获取到 random 键对应的实现类对象:" + loadBalance);
}
}
测试结果:
从结果可以看出,虽然指定了 random,但是实际获取到的是 RoundRobinLoadBalance,而 RoundRobinLoadBalance 内部引用了 RandomLoadBalance,如下所示:
如果有很多的包装类,那么就会形成一个责任链条,一个套一个。
所以 Dubbo 的 AOP 跟 Spring 的 AOP 实现是不一样的,Spring 的 AOP 底层是基于动态代理来的,而 Dubbo 的 AOP 其实算是静态代理,Dubbo 会帮你自动组装这个代理,形成一条责任链。
到这其实我们已经知道,Dubbo 的 SPI 接口的实现类已经有两种类型了:
- 自适应类;
- Wrapper类。
除了这两种类型,其实还有一种,叫做默认类,就是 @SPI 注解的值对应的实现类,,比如:
@SPI("random")
public interface LoadBalance {
}
此时 random 这个 key 对应的实现类就是默认实现,通过 getDefaultExtension() 这个方法就可以获取到默认实现对象。
3)自动激活
所谓的 自动激活,就是根据你的入参,动态地选择一批实现类返回给你。
自动激活的实现类上需要加上 Activate 注解,这里就又学习了一种实现类的分类。
@Activate
public interface RandomLoadBalance {
}
此时 RandomLoadBalance 就属于可以被自动激活的类。
获取自动激活类的方法是 getActivateExtension(),所以根据这个方法的入参,可以动态选择一批实现类。
自动激活这个机制在 Dubbo 一个核心的使用场景就是 Filter 过滤器链中。
Filter 是 dubbo 中的一个扩展点,可以在请求发起前或者是相应获取之后就进行拦截,作用有点像 Spring MVC 中的 HandlerInterceptor。
Filter 的一些实现类如下:
如上 Filter 有很多实现,所以为了能够区分 Filter 的实现是作用于 provider 端还是 consumer 端,所以就可以用自动激活的机制来根据入参动态选择一批 Filter 实现。
比如说 ConsumerContextFilter 这个 Filter 就作用于 Consumer 端。
实现原理图:
flowchart TD
A["ExtensionLoader.getExtension('dubbo')"] --> B[扫描所有JAR包的<br>META-INF/dubbo/ 目录]
B --> C{找到接口文件<br>org.apache.dubbo.rpc.Protocol?}
C -- 是 --> D[读取文件内容, 解析Key-Value对]
D --> E[根据Key 'dubbo'<br>定位实现类全限定名]
E --> F[反射创建<br>DubboProtocol实例]
F --> G[遍历setter方法, 进行依赖注入<br>(查找并注入其他扩展点)]
G --> H[遍历所有Wrapper类<br>(AOP增强, 形成责任链)]
H --> I[返回包装后的、<br>完整的DubboProtocol实例]
C -- 否 --> J[抛出异常: Extension not found]
最后,这里并没有对dubbo的SPI机制进行源码分析,感兴趣的同学可以看一下**面试常问的dubbo的spi机制到底是什么?(上)**和 **面试常问的dubbo的spi机制到底是什么?(下)**两篇文章。
六、总结
通过以上分析可以看出,实现SPI机制的核心原理就是通过IO流读取指定文件的内容,然后解析,最后加入一些自己的特性。
为了更清晰地展示三者的区别,我们通过一个表格进行总结:
| 特性维度 | Java SPI (ServiceLoader) | Spring SPI (SpringFactoriesLoader) | Dubbo SPI (ExtensionLoader) |
|---|---|---|---|
| 配置文件路径 | META-INF/services/ |
META-INF/spring.factories |
META-INF/services/、META-INF/dubbo/internal/ 、META-INF/dubbo/ 、META-INF/dubbo/external/ |
| 配置文件格式 | 全类名(每行一个) | Key=Value1,Value2… (Properties格式) | Name=Class (Properties格式) |
| 加载方式 | ServiceLoader.load() + 迭代器 |
SpringFactoriesLoader.loadFactories() |
ExtensionLoader.getExtension(name) |
| 获取实现 | 加载所有实现 | 按接口类型加载所有实现 | 按名称获取指定实现 |
| IoC/AOP支持 | 不支持 | 与Spring容器集成,支持IoC | 支持IoC注入和Wrapper(AOP) |
| 其他特性 | 无 | 支持排序(@Order) |
自适应(@Adaptive)、自动激活(@Activate) |
| 优点 | JDK标准,简单 | 与Spring生态无缝集成,功能较Java SPI强 | 功能最强大,是高性能扩展框架的基石 |
| 缺点 | 笨重,无法按需加载,功能单一 | 依赖Spring容器 | 概念复杂,与Dubbo框架强绑定 |
| 典型应用场景 | JDBC驱动加载等基础扩展 | Spring Boot自动配置 | Dubbo的所有组件扩展(协议、集群、负载均衡等) |
结论:
三者的SPI机制是一脉相承又不断进化的关系。
- Java SPI 是基础,实现的比较简单,并没有什么其它功能。
- Spring SPI 是在企业应用层面的一次优秀实践,得益于自身的ioc和aop的功能,但是也没有实现太复杂的SPI机制,仅仅是对Java做了一点简化和优化
- Dubbo SPI 则是面向高性能RPC框架的终极进化形态。为了满足自身框架的使用要求,实现的功能就比较多,不仅将ioc和aop的功能集成到SPI机制中,还提供注入自适应和自动激活等功能。
整理完毕,完结撒花~🌻
参考地址:
1.阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别,https://zhuanlan.zhihu.com/p/614159801
2.Dubbo-SPI机制深度解析,https://juejin.cn/post/7485584242039242802
更多推荐


所有评论(0)