![图片[1]-Java SPI机制及Sentinel中的应用:揭秘框架的可扩展设计 - 速优课-速优课](http://www.suyouke.com/wp-content/uploads/2026/08/doubao_img_2304x1728_20260803_094414-1024x768.png)
Java SPI机制及Sentinel中的应用:揭秘框架的可扩展设计
前言
在学习开源框架的过程中,你可能经常听到”SPI”这个词。Dubbo用了SPI,RocketMQ用了SPI,Sentinel也用了SPI。
那SPI到底是什么?它为什么这么受开源框架的青睐?Sentinel又是怎么利用SPI实现灵活扩展的?
这篇文章,我们就从Java SPI的基本概念讲起,再看看Sentinel是怎么用SPI的,最后手把手教你自定义SlotChainBuilder。
一、什么是SPI
SPI全称是Service Provider Interface,直译就是”服务提供者接口”。它是Java内置的一种服务发现机制。
简单说,SPI的本质是:
将接口实现类的全限定名写在配置文件里,由服务加载器读取配置文件,动态加载实现类。这样就能在运行时,根据配置动态替换接口的实现。
听起来有点绕,我们用一个通俗的例子来理解:
想象你买了一台台式机,主板上有各种PCIe插槽(这就是”接口”)。你可以插显卡、可以插声卡、可以插网卡(这些就是”实现类”)。你不需要改主板,只要换插槽上的卡,就能改变电脑的功能。
SPI就是这样一种”插槽机制”——框架定义好接口(插槽),你可以提供自己的实现(插卡),框架通过配置文件发现并加载你的实现。
SPI的典型应用
SPI在很多阿里开源项目中都有广泛应用:
- Dubbo:自己实现了一套SPI(比Java原生更强大,支持key-value配置和自适应扩展)
- RocketMQ:使用Java原生SPI
- Sentinel:使用Java原生SPI
二、Java SPI快速入门
光说不练假把式,我们用一个”Hello World”级别的例子,快速上手Java SPI。
第一步:定义接口
假设我们有一个登录服务接口:
public interface LoginService {
void login(String username, String password);
}
第二步:编写实现类
我们提供两种登录实现:一个基于Shiro,一个基于Spring。
public class ShiroLoginService implements LoginService {
@Override
public void login(String username, String password) {
System.out.println("使用Shiro登录:" + username);
}
}
public class SpringLoginService implements LoginService {
@Override
public void login(String username, String password) {
System.out.println("使用Spring登录:" + username);
}
}
第三步:创建配置文件
这一步是关键。在resources/META-INF/services目录下,创建一个以接口全限定名命名的文件。
文件名:com.wujiuye.spi.LoginService
文件内容:写上实现类的全限定名(多个实现类用换行分隔)
com.wujiuye.spi.ShiroLoginService
com.wujiuye.spi.SpringLoginService
小技巧:在IDEA中,只要目录和文件名正确,编写内容时IDEA会自动提示有哪些实现类,非常方便。
第四步:测试使用
public class JavaSPITest {
public static void main(String[] args) {
ServiceLoader<LoginService> serviceLoader = ServiceLoader.load(LoginService.class);
for (LoginService service : serviceLoader) {
service.login("wujiuye", "123456");
}
}
}
运行结果:
使用Shiro登录:wujiuye
使用Spring登录:wujiuye
ServiceLoader的工作原理
ServiceLoader是Java提供的SPI核心类。它的工作原理并不复杂:
- 接收一个接口类型
- 拼接配置文件路径:
META-INF/services/+ 接口全类名 - 读取配置文件,解析出所有实现类的全限定名
- 遍历的时候,才会用
Class.forName()加载类,并用反射创建实例
注意:ServiceLoader是懒加载的——调用load()方法时并不会立即加载和实例化,只有在遍历迭代器的时候才会真正加载。
SPI的适用场景
什么时候适合用SPI?
- 责任链/拦截器/过滤器模式:需要可插拔地添加或移除处理器
- 策略模式:多种算法实现,根据配置选择使用哪一种
- 插件化架构:框架提供扩展点,第三方可以编写插件
举个例子:一个画图程序,定义一个Shape接口。一开始只有矩形、三角形,后来想加个圆形,只需要新增一个实现类,在配置文件里加一行,完全不用改原有代码。这就是开闭原则的完美体现。
三、SPI在Sentinel中的应用
了解了Java SPI的基本用法,我们来看看Sentinel是怎么用SPI的。
Sentinel中的两个核心SPI
在sentinel-core模块的resources/META-INF/services目录下,有两个配置文件:
| 配置文件 | 接口 | 作用 |
|---|---|---|
com.alibaba.csp.sentinel.slotchain.SlotChainBuilder | SlotChainBuilder | 构造Slot责任链 |
com.alibaba.csp.sentinel.init.InitFunc | InitFunc | 初始化函数 |
两个文件的默认配置:
SlotChainBuilder的默认配置:
# Default slot chain builder
com.alibaba.csp.sentinel.slots.DefaultSlotChainBuilder
InitFunc的默认配置:
com.alibaba.csp.sentinel.metric.extension.MetricCallbackInit
两种SPI的加载策略
虽然都用了ServiceLoader加载,但Sentinel对这两个接口的加载策略不一样:
| 接口 | 加载策略 | 说明 |
|---|---|---|
| InitFunc | 加载所有实现类 | 允许多个初始化函数并存 |
| SlotChainBuilder | 只加载第一个非默认实现 | 只能有一个Slot链构建器 |
SlotChainBuilder的加载逻辑
SlotChainBuilder的加载逻辑在SpiLoader.loadFirstInstanceOrDefault方法中:
public final class SpiLoader {
public static <T> T loadFirstInstanceOrDefault(Class<T> clazz,
Class<? extends T> defaultClass) {
try {
// 遍历所有实现类
for (T instance : serviceLoader) {
// 返回第一个非默认类的实例
if (instance.getClass() != defaultClass) {
return instance;
}
}
// 没有自定义实现,就用默认的
return defaultClass.newInstance();
} catch (Throwable t) {
return null;
}
}
}
逻辑很清晰:
- 遍历所有实现类
- 找到第一个不是默认实现类的,直接返回
- 如果遍历完都没有找到,就返回默认实现类的实例
所以,如果你写了自己的SlotChainBuilder,只要配置上了,就会优先用你的。
InitFunc的加载逻辑
InitFunc的加载逻辑在InitExecutor.doInit方法中:
public final class InitExecutor {
public static void doInit() {
try {
// 加载所有InitFunc实现
ServiceLoader<InitFunc> loader = ServiceLoaderUtil.getServiceLoader(InitFunc.class);
List<OrderWrapper> initList = new ArrayList<OrderWrapper>();
for (InitFunc initFunc : loader) {
// 插入并排序
insertSorted(initList, initFunc);
}
// 按顺序调用初始化方法
for (OrderWrapper w : initList) {
w.func.init();
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
InitFunc允许多个实现类并存,而且还支持排序(通过@Order注解)。
不过在实际的Web项目中,InitFunc用得不多。通常我们会监听Spring容器刷新完成事件来初始化Sentinel规则,如果用了动态数据源,还可以在配置变更时重新加载规则。
一个重要的细节:配置文件合并
有个细节需要注意:你不需要修改Sentinel源码里的配置文件。
你只需要在自己项目的resources/META-INF/services目录下,创建同名的配置文件,添加你自己的实现类就行。
项目编译后,Sentinel自带的配置文件和你项目里的配置文件会合并成一个,而不是覆盖。这样就能实现”不修改框架源码,扩展框架功能”。
四、实战:自定义SlotChainBuilder
说了这么多,我们来动手实践一下——自定义一个SlotChainBuilder,去掉不需要的Slot,优化性能。
为什么要自定义?
Sentinel默认的SlotChainBuilder会把所有Slot都加上:
- NodeSelectorSlot
- ClusterBuilderSlot
- LogSlot
- StatisticSlot
- AuthoritySlot
- SystemSlot
- FlowSlot
- DegradeSlot
但不是每个项目都需要所有功能。比如:
- 不需要黑白名单?可以去掉AuthoritySlot
- 不需要系统自适应限流?可以去掉SystemSlot
- 不需要日志?可以去掉LogSlot
少一个Slot就少一次方法调用,性能就能好一点。对于性能敏感的场景,自定义SlotChainBuilder是很有价值的优化。
第一步:编写自定义Builder
假设我们只需要核心的限流和熔断功能,去掉LogSlot、AuthoritySlot、SystemSlot:
public class MySlotChainBuilder implements SlotChainBuilder {
@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
// 节点选择(必须有)
chain.addLast(new NodeSelectorSlot());
// 集群节点构建(必须有)
chain.addLast(new ClusterBuilderSlot());
// 统计(必须有)
chain.addLast(new StatisticSlot());
// 限流
chain.addLast(new FlowSlot());
// 熔断
chain.addLast(new DegradeSlot());
return chain;
}
}
第二步:配置SPI
在项目的resources/META-INF/services目录下,创建文件:
文件名:com.alibaba.csp.sentinel.slotchain.SlotChainBuilder
文件内容:
com.wujiuye.sck.provider.config.MySlotChainBuilder
第三步:注意事项
自定义SlotChainBuilder时,有些Slot的顺序是不能乱的:
- NodeSelectorSlot 必须在 ClusterBuilderSlot 前面
- ClusterBuilderSlot 必须在 StatisticSlot 前面
- StatisticSlot 必须在各种功能Slot前面
为什么?因为后面的Slot依赖前面构建出来的Node。顺序乱了,就会出现空指针等各种问题。
但功能类Slot之间的顺序是可以调整的,比如你可以把DegradeSlot放在FlowSlot前面,意思是”先检查熔断,再检查限流”。这个就看业务需求了。
五、Sentinel 1.7.2+的新特性
在Sentinel 1.7.2版本之后,支持了更灵活的扩展方式:直接用SPI注册ProcessorSlot,并且支持排序。
也就是说,你不需要自定义整个SlotChainBuilder了,可以单独添加一个自定义的ProcessorSlot,Sentinel会自动帮你加到链里。
这对于只想加一个自定义功能Slot的场景来说,方便太多了。
总结与思考
这篇文章我们从Java SPI的基本概念讲起,到Sentinel中SPI的应用,再到自定义SlotChainBuilder的实战。
几个关键要点回顾:
- SPI是一种服务发现机制:通过配置文件动态加载实现类,遵循开闭原则
- ServiceLoader是懒加载的:只有遍历的时候才会加载和实例化
- Sentinel有两个核心SPI:SlotChainBuilder(单实现)和InitFunc(多实现)
- 配置文件会合并:在自己项目里加配置就行,不用改框架源码
- 自定义SlotChainBuilder可以优化性能:去掉不需要的Slot,但要注意顺序不能乱
思考一下:
- SPI和Spring的IoC有什么区别?(SPI是服务发现,IoC是依赖注入,两者解决的问题不同,但可以配合使用)
- 为什么Dubbo要自己实现一套SPI,而不用Java原生的?(原生SPI功能太弱,不支持key-value、不支持自适应扩展、不支持按需加载等)
- 除了SlotChainBuilder,你还能想到Sentinel的哪些地方可以用SPI扩展?(动态数据源、集群限流TokenServer等都用到了SPI)
SPI是一个非常实用的设计思想,掌握它不仅能帮助你更好地理解Sentinel,也能让你在自己的项目设计中,写出更具扩展性的代码。
下一篇,我们将深入Sentinel的资源指标统计实现,看看StatisticSlot是怎么工作的。










请登录后查看评论内容