写点什么

Java SPI 在 Sentinel 中是如何应用的?

  • 2023-06-10
    湖南
  • 本文字数:3724 字

    阅读完需:约 12 分钟

SPI 机制在阿里巴巴集团开源的项目中被广泛使用,如 Dubbo、RocketMQ 与 Sentinel 都使用了 SPI 机制。除 Dubbo 外,RocketMQ 与 Sentinel 使用的都是 Java 提供的 SPI 机制。


Dubbo 使用的是自实现的一套 SPI——Dubbo SPI,与 Java SPI 的配置方式不同,DubboSPI 使用 Key-Value 方式配置,目的是实现自适应扩展机制。

Java SPI 实现原理与适用场景

SPI(Service Provider Interface,服务提供者接口)是一种服务发现机制,是 Java 的一个内置标准,可以保障不同的开发者实现某个特定的服务。


SPI 的本质是将接口实现类的全限定名配置在文件中,由服务加载器读取配置文件、加载实现类并创建实例。使用 SPI 机制能够实现运行时从配置文件中读取接口的实现类并创建实例。


我们以实现动态切换登录方式为例讲解如何使用 Java SPI,虽然这不是一个很好的使用案例,但是通过此案例能更直观地认识 Java SPI。


1. 定义登录接口

定义登录接口 LoginService,该接口提供了 login 方法,login 方法可以接收用户名和密码并返回登录结果。接口定义的代码如下:

2. 编写接口实现类

如果想使用 Shiro 框架实现用户鉴权,那么需要提供一个 LoginService 的实现类 ShiroLoginService。使用 ShiroLoginService 类的代码如下:

如果想直接使用 Spring MVC 的拦截器实现用户鉴权,那么需要提供一个 LoginService 的实现类 SpringLoginService。使用 SpringLoginService 类的代码如下:

3. 通过配置使用 SpringLoginService 类或 ShiroLoginService 类

当我们想通过修改配置文件的方式而不是修改代码的方式实现权限验证框架的切换时,就可以使用 Java SPI,具体做法是运行时从配置文件中读取 LoginService 的实现类,然后加载并使用配置的实现类。


首先,在项目的 resources 目录下创建 META-INF 目录,并在 META-INF 目录下创建 services 目录;然后,在 services 目录下创建名称为 LoginService 的配置文件(LoginService 是接口的全类名);最后,在配置文件中写入使用的 LoginService 接口实现类的全类名。


提示:只要在 META-INF/services 目录下,且文件名是接口的全类名,在编写配置文件内容时,IDEA 就会自动提示有哪些实现类。


在配置文件中,填写的内容为接口的实现类,多个实现类使用换行的方式分开。在此案例中,如果想使用 ShiroLoginService 类,则配置如下:

4. 使用 Java SPI 加载 LoginService

编写 main 方法,测试使用 ServiceLoader 加载 LoginService,代码如下:

ServiceLoader 是 Java 提供的服务加载器,用于实现 SPI 机制。ServiceLoader 提供 load 静态方法,该方法接收一个接口并返回一个 ServiceLoader 实例。通过遍历 ServiceLoader 实例的迭代器(Iterator),我们可以获取接口对应的配置文件中配置的所有实现类实例。


在调用 ServiceLoader#load 方法后,此时配置文件中注册的实现类还没有被加载到 JVM 中,只有通过迭代器遍历获取时,才会加载实现类及实例化实现类,并且遍历的顺序就是配置文件中注册实现类的顺序。


在本例中,我们通过 forEach 语法遍历 ServiceLoader 时,使用了 break 语句,因为在登录场景下,不可能同时使用两种 LoginService,所以也不应该在 SPI 配置文件中配置多个 LoginService 的实现类。

ServiceLoader 实现原理

在调用 ServiceLoader#load 方法时,ServiceLoader 根据参数传入的接口获取接口的全类名,将前缀/META-INF/services 与接口的全类名拼接定位到配置文件,然后读取配置文件中的字符串并解析字符串,将解析出来的实现类全类名添加到一个数组中,并返回一个 ServiceLoader 实例。


ServiceLoader 实现了 Iterable 接口,所以可以使用 forEach 语法遍历。ServiceLoader 使用 lazy 方式(“懒加载”或“延迟加载”)实现迭代器,只有被迭代器的 next 方法遍历到的类才会被加载和实例化。如果只想使用接口配置文件中注册的第一个实现类,那么在使用迭代器遍历时,可以使用 break 语句跳出循环。


在使用迭代器遍历时,ServiceLoader 通过调用 Class#forName 方法加载类并且通过反射创建实例。如果不指定加载实现类使用的类加载器,ServiceLoader 就会使用当前线程的上下文类加载器加载。

SPI 机制的适用场景

适合使用策略模式、责任链模式的场景都可以使用 SPI 机制。


例如,将 SPI 机制用在绘制形状的场景:定义一个形状接口,实现矩形、三角形等的绘制,如果想要添加圆形,只需要在形状接口的配置文件中注册圆形即可支持绘制圆形,完全不用修改任何代码。

Java SPI 在 Sentinel 中的应用

Sentinel 使用 Java SPI 为我们提供了插件注册的功能,类似于 Spring Boot 提供的自动配置类注册功能。


我们可以直接替换 Sentinel 提供的默认 SlotChainBuilder,使用自定义的 SlotChainBuilder 为资源构造自己的 ProcessorSlotChain,以实现修改 ProcessorSlot 排列顺序、增加或移除 ProcessorSlot 的功能。


提示:在 Sentinel 1.7.2 版本中,Sentinel 支持使用 SPI 注册 ProcessorSlot,并且支持排序。


在 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.slotchain.SlotChainBuilder 文件的默认配置如下:

com.alibaba.csp.sentinel.init.InitFunc 文件的默认配置如下:

ServiceLoader 可加载接口配置文件中配置的所有实现类,并且使用反射创建对象,但是是否全部加载及实例化仍然由使用者自己决定。


sentinel-core 模块在使用 Java SPI 机制加载 InitFunc 与 SlotChainBuilder 时,会在实现上稍有不同。如果 InitFunc 接口的配置文件注册了多个实现类,那么这些注册的 InitFunc 实现类都会被 Sentinel 加载并实例化,且都会被使用。但是如果 SlotChainBuilder 接口的配置文件注册了多个实现类,那么 Sentinel 只会加载和使用第一个实现类。


Sentinel 在加载 SlotChainBuilder 时,只会获取第一个非默认实现类的实例,如果接口配置文件中只有默认实现类而没有注册其他的实现类,那么 Sentinel 会使用这个默认的 SlotChainBuilder。实现源码在 SpiLoader 的 loadFirstInstanceOrDefault 方法中,代码如下:

  1. 获取接口的 ServiceLoader 实例。

  2. 遍历加载接口的实现类并获取实例,若获取的实例类型与指定的默认实现类不同,则使用该实例。

  3. 使用默认实现类的实例,使用反射创建默认实现类的实例。


因为 Sentinel 允许存在多个初始化方法,所以 Sentinel 加载 InitFunc 与 SlotChainBuilder 的方式会有所不同。Sentinel 使用 ServiceLoader 加载注册的 InitFunc 实现类实例的代码如下:

  1. 遍历获取注册的所有接口实现类的实例。

  2. 实现排序及包装实例。

  3. 遍历调用每个 InitFunc 实例的初始化方法。


InitFunc 可用于初始化配置限流、熔断规则,但在 Web 项目中基本不会使用它,更多的是先通过监听 Spring 容器刷新完成事件,再初始化 Sentinel 配置规则。如果使用 Sentinel 提供的动态数据源还可以在监听到动态配置改变事件时重新加载规则,则基本使用不到 InitFunc。


虽然 InitFunc 接口与 SlotChainBuilder 接口的配置文件在 sentinel-core 模块下,但是并不需要修改 Sentinel 的源码,也不需要修改 sentinel-core 模块下的接口配置文件,而只需要在当前项目的 resource/META-INF/services 目录下创建一个与接口名称相同的配置文件,并在配置文件中添加接口的实现类即可。ServiceLoader 会遍历项目依赖的每个 jar 包下的与接口名称相同的配置文件。


ServiceLoader 会遍历项目依赖的每个 jar 包下的与接口名称相同的配置文件,同一个文件中注册的实现类是按注册顺序遍历的,但多个文件的遍历顺序则是不确定的,所以不要依赖注册顺序实现排序。

自定义组装 ProcessorSlotChain

Sentinel 使用 SlotChainBuilder 将多个 ProcessorSlot 构造成一个 ProcessorSlotChain,由 ProcessorSlotChain 按照 ProcessorSlot 的注册顺序调度这些 ProcessorSlot。


Sentinel 使用 Java SPI 加载 SlotChainBuilder,支持使用者自定义 SlotChainBuilder,相当于提供了插件的功能。


默认使用的 SlotChainBuilder 是 DefaultSlotChainBuilder。DefaultSlotChainBuilder 构造 ProcessorSlotChain 的源码如下:

build 方法:用来创建 DefaultProcessorSlotChain 实例,并注册 ProcessorSlot,如 NodeSelectorSlot、ClusterBuilderSlot、LogSlot、StatisticSlot、AuthoritySlot、SystemSlot、FlowSlot、DegradeSlot。

DefaultSlotChainBuilder 注册的 ProcessorSlot 并非都是必需的,如果注册的 ProcessorSlot 中有些用不到,那么可以自己实现一个 SlotChainBuilder,自己构造 ProcessorSlotChain。


例如,可以将 LogSlot、AuthoritySlot 和 SystemSlot 去掉,实现这个操作只需要两步。

(1)编写 MySlotChainBuilder 类,实现 SlotChainBuilder 接口,代码如下:

(2)在当前项目的 resources/META-INF/services 目录下添加名称为 com.alibaba.csp.sentinel.

slotchain.SlotChainBuilder 的接口配置文件,并在配置文件中注册 MySlotChainBuilder 类,配置代码如下:

需要注意的是 ProcessorSlot 的注册顺序,NodeSelectorSlot 需要作为 ClusterBuilderSlot 的前驱节点,ClusterBuilderSlot 需要作为 StatisticSlot 的前驱节点,否则 Sentinel 运行会出现 Bug。但可以将 DegradeSlot 放在 FlowSlot 的前面。

用户头像

加VX:bjmsb02 凭截图即可获取 2020-06-14 加入

公众号:程序员高级码农

评论

发布
暂无评论
Java SPI 在 Sentinel 中是如何应用的?_Java_互联网架构师小马_InfoQ写作社区