SPI 在Sentinel中 的应用
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 模块下的相同名称的配置文件,而是将两个配置文件合并成一个配置文件。
作者:小林酱子
链接:https://juejin.cn/post/7025618832219701278