SPI 在Sentinel中 的应用
SPI介绍
SPI全程是Service Provider Interface,直译就是服务提供者接口,是一种服务发现机制 ,是Java的一个内置标准,允许不同的开发者去实现某个特定的服务。SPI的本质是将接口实现类的全限定名配置在文件中,由服务加载器读取配置文件,加载实现类,实现在运行时动态替换接口的实现类。
使用 SPI 机制能够实现按配置加载接口的实现类,SPI 机制在阿里开源的项目中被广泛使用,例如 Dubbo、RocketMQ、以及Sentinel。
RocketMQ 与 Sentinel 使用的都是 Java 提供的 SPI 机制,而 Dubbo 则是使用自实现的一套 SPI,与 Java SPI 的配置方式不同,Dubbo SPI 使用 Key-Value 方式配置,目的是实现自适应扩展机制。
Java SPI 在 Sentinel 中的应用
在 sentinel-core 模块的 resources 资源目录下,有一个 META-INF/services 目录,该目录下有两个以接口全名命名的文件
- com.alibaba.csp.sentinel.slotchain.SlotChainBuilder 文件用于配置 SlotChainBuilder 接口的实现类
- com.alibaba.csp.sentinel.init.InitFunc 文件用于配置 InitFunc 接口的实现类
这两个配置文件中都配置了接口的默认实现类,如果我们不添加新的配置,Sentinel 将使用默认配置的接口实现类
com.alibaba.csp.sentinel.init.InitFunc 文件的默认配置如下:
com.alibaba.csp.sentinel.metric.extension.MetricCallbackInit
com.alibaba.csp.sentinel.slotchain.SlotChainBuilder 文件的默认配置如下:
com.alibaba.csp.sentinel.slots.DefaultSlotChainBuilder
ServiceLoader 可加载接口配置文件中配置的所有实现类并且使用反射创建对象,但是否全部加载以及实例化还是由使用者自己决定。
Sentinel 的 core 模块使用 Java SPI 机制加载 InitFunc 与 SlotChainBuilder 的实现上稍有不同
如果 InitFunc 接口的配置文件注册了多个实现类,那么这些注册的 InitFunc 实现类都会被 Sentinel 加载、实例化,且都会被使用,但 SlotChainBuilder 不同,如果注册多个实现类,Sentinel 只会加载和使用第一个。
Sentinel 在加载 SlotChainBuilder 时,只会获取第一个非默认(非 DefaultSlotChainBuilder)实现类的实例,如果接口配置文件中除了默认实现类没有注册别的实现类,则 Sentinel 会使用这个默认的 SlotChainBuilder。其实现源码在 SpiLoader 的 loadFirstInstanceOrDefault 方法中。
//加载第一个找到的 Provider 实例,如果没有找到,返回默认的 Provider 实例
public S loadFirstInstanceOrDefault() {
//加载provider实例
load();
for (Class<? extends S> clazz : classList) {
if (defaultClass == null || clazz != defaultClass) {
//加载找到的实例
return createInstance(clazz);
}
}
//加载默认实例
return loadDefaultInstance();
}
Sentinel 加载 InitFunc 则不同,因为 Sentinel 允许存在多个初始化方法。InitFunc 可用于初始化配置限流、熔断规则
Sentinel 使用 ServiceLoader 加载注册的 InitFunc 实现代码如下
public final class InitExecutor {
private static AtomicBoolean initialized = new AtomicBoolean(false);
/**
* 如果有一个InitFunc抛出异常,则 init 进程将立即中断,应用程序将退出。 初始化将只执行一次。
*/
public static void doInit() {
if (!initialized.compareAndSet(false, true)) {
return;
}
try {
// 加载配置
List<InitFunc> initFuncs = SpiLoader.of(InitFunc.class).loadInstanceListSorted();
List<OrderWrapper> initList = new ArrayList<OrderWrapper>();
for (InitFunc initFunc : initFuncs) {
// 插入数组并排序,同时将 InitFunc 包装为 OrderWrapper
insertSorted(initList, initFunc);
}
// 遍历调用 InitFunc 的初始化方法
for (OrderWrapper w : initList) {
w.func.init();
}
} catch (Exception ex) {
RecordLog.warn("[InitExecutor] WARN: Initialization failed", ex);
ex.printStackTrace();
} catch (Error error) {
RecordLog.warn("[InitExecutor] ERROR: Initialization failed with fatal error", error);
error.printStackTrace();
}
}
虽然 InitFunc 接口与 SlotChainBuilder 接口的配置文件在 sentinel-core 模块下,但我们不需要去修改 Sentinel 的源码,不需要修改 sentinel-core 模块下的接口配置文件,而只需要在当前项目的 /resource/META-INF/services 目录下创建一个与接口全名相同名称的配置文件,并在配置文件中添加接口的实现类即可。项目编译后不会覆盖 sentinel-core 模块下的相同名称的配置文件,而是将两个配置文件合并成一个配置文件。