07-Java SPI及SPI在Sentinel中的应用

原创 吴就业 134 0 2020-09-22

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/22c93b06bd73441792810121ec7f1861

作者:吴就业
链接:https://wujiuye.com/article/22c93b06bd73441792810121ec7f1861
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

深入理解Sentinel

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简介

我们先来个“Hello Word”级别的Demo学习一下Java SPI怎么使用,通过这个例子认识Java SPI。

第一步:定义接口

假设我们有多种登录方式,则创建一个LoginService接口。

public interface LoginService{
  void login(String username,String password);
}

第二步:编写接口实现类

假设一开始我们使用Shiro框架实现用户鉴权,提供了一个ShiroLoginService。

public class ShiroLoginService implements LoginService{
  public void login(String username,String password){
    // 实现登陆
  }
}

现在我们不想搞那么麻烦,比如我们可以直接使用Spring MVC的拦截器实现用户鉴权,那么可以提供一个SpringLoginService。

public class SpringLoginService implements LoginService{
  public void login(String username,String password){
    // 实现登陆
  }
}

第三步:通过配置使用SpringLoginService或ShiroLoginService

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

需要在resources目录下新建一个目录META-INF,并在META-INF目录下创建services目录,用来存放接口配置文件。

配置文件名为接口LoginService全类名,文件中写入使用的实现类的全类名。只要是在META-INF/services目录下,只要文件名是接口的全类名,那么编写配置文件内容的时候,IDEA就会自动提示有哪些实现类。

文件:/resources/META-INF/services/接口名称,填写的内容为接口的实现类,多个实现类使用换行分开。

com.wujiuye.spi.ShiroLoginService

第四步:编写main方法测试使用Java SPI加载LoginService。

public class JavaSPI{
  public static void main(String[] args){
    ServiceLoader<LoginService> serviceLoader = ServiceLoader.load(ServiceLoader.class);
    for(LoginService serviceImpl:serviceLoader){
      serviceImpl.login("wujiuye","123456");
    }
  }
}

ServiceLoader(服务加载器)是Java提供的SPI机制的实现,调用load方法传入接口名就能获取到一个ServiceLoader实例,此时配置文件中注册的实现类是还没有加载到JVM的,只有通过Iterator遍历获取的时候,才会去加载实现类与实例化实现类。

需要说明的是,例子中配置文件只配置了一个实现类,但其实我们是可以配置N多个的,并且iterator遍历的顺序就是配置文件中注册实现类的顺序。如果非要想一个注册多实现类的适用场景的话,责任链(拦截器、过滤器)模式这种可插拔的设计模式最适合不过。又或者一个画图程序,定义一个形状接口,实现类可以有矩形、三角形等,如果后期添加了圆形,只需要在形状接口的配置文件中注册圆形就能支持画圆形,完全不用修改任何代码。

ServiceLoader源码很容易理解,就是根据传入的接口获取接口的全类名,将前缀”/META-INF/services”与接口的全类名拼接定位到配置文件,读取配置文件中的字符串、解析字符串,将解析出来的实现类全类名添加到一个数组,返回一个ServiceLoader实例。只有在遍历迭代器的时候ServiceLoader才通过调用Class#forName方法加载类并且通过反射创建实例,如果不指定类加载器,就使用当前线程的上下文类加载器加载类。

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.metric.extension.MetricCallbackInit
  # Default slot chain builder
  com.alibaba.csp.sentinel.slots.DefaultSlotChainBuilder

ServiceLoader可加载接口配置文件中配置的所有实现类并且使用反射创建对象,但是否全部加载以及实例化还是由使用者自己决定。Sentinel的core模块使用Java SPI机制加载InitFunc与SlotChainBuilder的实现上稍有不同,如果InitFunc接口的配置文件注册了多个实现类,那么这些注册的InitFunc实现类都会被Sentinel加载、实例化,且都会被使用,但SlotChainBuilder不同,如果注册多个实现类,Sentinel只会加载和使用第一个。

调用ServiceLoader#load方法传入接口可获取到一个ServiceLoader实例,ServiceLoader实现了Iterable接口,所以可以使用forEach语法遍历,ServiceLoader使用layz方式实现迭代器(Iterator),只有被迭代器的next方法遍历到的类才会被加载和实例化。如果只想使用接口配置文件中注册的第一个实现类,那么可在使用迭代器遍历时,使用break跳出循环。

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

public final class SpiLoader {
	public static <T> T loadFirstInstanceOrDefault(Class<T> clazz, Class<? extends T> defaultClass) {
        try {
            // 缓存的实现省略...
            // 返回第一个类型不等于defaultClass的实例
            // ServiceLoader实现了Iterable接口
            for (T instance : serviceLoader) {
                // 获取第一个非默认类的实例
                if (instance.getClass() != defaultClass) {
                    return instance;
                }
            }
            // 没有则使用默认类的实例,反射创建对象
            return defaultClass.newInstance();
        } catch (Throwable t) {
            return null;
        }
    }
}

Sentinel加载InitFunc则不同,因为Sentinel允许存在多个初始化方法。InitFunc可用于初始化配置限流、熔断规则,但在Web项目中我们基本不会使用它,更多的是通过监听Spring容器刷新完成事件再去初始化为Sentinel配置规则,如果使用动态数据源还可在监听到动态配置改变事件时重新加载规则,所以InitFunc我们基本使用不上。

Sentinel使用ServiceLoader加载注册的InitFunc实现代码如下。

public final class InitExecutor {

    public static void doInit() {
        try {
            // 加载配置
            ServiceLoader<InitFunc> loader = ServiceLoaderUtil.getServiceLoader(InitFunc.class);
            List<OrderWrapper> initList = new ArrayList<OrderWrapper>();
            for (InitFunc initFunc : loader) {
               // 插入数组并排序,同时将InitFunc包装为OrderWrapper
                insertSorted(initList, initFunc);
            }
            // 遍历调用InitFunc的初始化方法
            for (OrderWrapper w : initList) {
                w.func.init();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        } catch (Error error) {
            error.printStackTrace();
        }
    }
}

虽然InitFunc接口与SlotChainBuilder接口的配置文件在sentinel-core模块下,但我们不需要去修改sentinel的源码,不需要修改sentinel-core模块下的接口配置文件,而只需要在当前项目的/resource/META-INF/services目录下创建一个与接口全名相同名称的配置文件,并在配置文件中添加接口的实现类即可。项目编译后不会覆盖sentinel-core模块下的相同名称的配置文件,而是将两个配置文件合并成一个配置文件。

自定义ProcessorSlotChain构造器

Sentinel使用SlotChainBuilder将多个ProcessorSlot构造成一个ProcessorSlotChain,由ProcessorSlotChain按照ProcessorSlot的注册顺序去调用这些ProcessorSlot。Sentinel使用Java SPI加载SlotChainBuilder支持使用者自定义SlotChainBuilder,相当于是提供了插件的功能。

Sentinel默认使用的SlotChainBuilder是DefaultSlotChainBuilder,其源码如下。

public class DefaultSlotChainBuilder implements SlotChainBuilder {

    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();
        chain.addLast(new NodeSelectorSlot());
        chain.addLast(new ClusterBuilderSlot());
        chain.addLast(new LogSlot());
        chain.addLast(new StatisticSlot());
        chain.addLast(new AuthoritySlot());
        chain.addLast(new SystemSlot());
        chain.addLast(new FlowSlot());
        chain.addLast(new DegradeSlot());
        return chain;
    }

}

DefaultSlotChainBuilder构造的ProcessorSlotChain注册了NodeSelectorSlot、ClusterBuilderSlot、LogSlot、StatisticSlot、AuthoritySlot、SystemSlot、FlowSlot、DegradeSlot,但这些ProcessorSlot并非都是必须的,如果注册的这些ProcessorSlot有些我们用不到,那么我们可以自己实现一个SlotChainBuilder,自己构造ProcessorSlotChain。例如,我们可以将LogSlot、AuthoritySlot、SystemSlot去掉。

第一步,编写MySlotChainBuilder,实现SlotChainBuilder接口,代码如下。

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;
    }

}

第二步,在当前项目的/resources/META-INF/services目录下添加名为com.alibaba.csp.sentinel.slotchain.SlotChainBuilder的接口配置文件,并在配置文件中注册MySlotChainBuilder。

com.wujiuye.sck.provider.config.MySlotChainBuilder

在构造ProcessorSlotChain时,需注意ProcessorSlot的注册顺序,例如,NodeSelectorSlot需作为ClusterBuilderSlot的前驱节点,ClusterBuilderSlot需作为StatisticSlot的前驱节点,否则Sentinel运行会出现bug。但你可以将DegradeSlot放在FlowSlot的前面,这就是我们上一篇说到的ProcessorSlot的排序。

总结

Sentinel使用Java SPI为我们提供了插件的功能,也类似于Spring Boot提供的自动配置类注册功能。我们可以直接替换Sentinel提供的默认SlotChainBuilder,使用自定义的SlotChainBuilder自己构造ProcessorSlotChain,以此实现修改ProcessorSlot排序顺序以及增加或移除ProcessorSlot。在Sentinel 1.7.2版本中,Sentinel支持使用SPI注册ProcessorSlot,并且支持排序。

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Spring Data R2DBC快速上手指南

本篇内容介绍如何使用r2dbc-mysql驱动程序包与mysql数据库建立连接、使用r2dbc-pool获取数据库连接、Spring-Data-R2DBC增删改查API、事务的使用,以及R2DBC Repository。

使用Spring WebFlux + R2DBC搭建消息推送服务

消息推送服务主要是处理同步给用户推送短信通知或是异步推送短信通知、微信模板消息通知等。本篇介绍如何使用Spring WebFlux + R2DBC搭建消息推送服务。

教你如何编写一个IDEA插件,并掌握核心知识点PSI

IDEA有着极强的扩展功能,它提供插件扩展支持,让开发者能够参与到IDEA生态建设中,为更多开发者提供便利、提高开发效率。我们常用的插件有Lombok、Mybatis插件,这些插件都大大提高了我们的开发效率。即便IDEA功能已经很强大,并且也已有很多的插件,但也不可能面面俱到,有时候我们需要自给自足。

Spring Boot实现加载自定义配置文件

本篇将介绍两种加载自定义配置文件的实现方式,并通过分析源码了解SpringBoot加载配置文件的流程,从而加深理解。

设计模式那些模糊不清的概念

23种设计模式属于结构型模式,而mvc模式等属于架构型模式。本篇要讨论的设计模式指的是结构型设计模式。

实现一个分布式调用链路追踪Java探针你可能会遇到的问题

Instrumentation之所以难驾驭,在于需要了解Java类加载机制以及字节码,一不小心就能遇到各种陌生的Exception。笔者在实现Java探针时就踩过不少坑,其中一类就是类加载相关的问题,也是本篇所要跟大家分享的。