Soul网关SPI的运用

  • SPI
    全称:service provider interface:服务提供接口,是一种思想,java SPI是对SPI的一种实现。
    顾名思义,它是用作服务提供的,类似于,用户自己定义了一种实现,按照提供服务接口的规则,将定义的实现注册到服务提供接口中,该接口不管用户任何实现的细节,当用户后续使用的时候传入某种标识,调用服务提供接口,获取到自己所定义的规则。
    比如在Slf4J 、javaSPI、DriverManager中,各个厂商提供自己的规则实现,但是调用的时候,根据传入的标识等,调用同一个接口,能够获得自己定义的内容。
  • JDK的实现
    • 在META-INF/services/目录下创建以接口权限定名为文件名的文件
    • 在文件中写入实现类的权限定名
    • ServiceLoader.load(class)便可以获得实现类
  • Soul实现
    • 写接口和实现类,接口上添加@SPI注解,实现类上添加@join注解
    • 在META-INF/soul/目录下创建以接口权限定名为文件名的文件
    • 在文件中以key=value的形式写实现类,key为任何自定义的字符串,用来获取实现类,value为实现类的全限定名
    • 调用ExtensionLoader.getExtensionLoader(**.class).getJoin(key),获得实现类
  • 实现代码
    • soul网关自定义扩展加载器:ExtensionLoader 
    • 测试:
               定义接口:JdbcSPI
      • META-INF/soul下建立文件org.dromara.soul.spi.fixture.JdbcSPI(接口全限定名)

      • 文件内容是实现类

      • 在junit中运行测试代码:JdbcSPI jdbcSPI =  = ExtensionLoader.getExtensionLoader(JdbcSPI.class).getJoin("mysql");返回的正是MysqlSPI类的对象
  • 规则要求
    • 接口标注@SPI,一定要是接口,否则使用getExtensionLoader(class)会报错,@SPI注解可以增加value值,标识的是默认的join类key值
    • 实现类实现接口,需要增加@Join注解,否则会报错
    • 文件、目录、文件内容
  • 实现分析
    • 先走一遍代码:运行ExtensionLoader.getExtensionLoader(JdbcSPI.class).getJoin("mysql");
      • 先看getExtensionLoader方法


        可以看到,直接判断了传入的class是不是为空,先去LOADERS中拿,看有没有ExtensionLoader,如果没有,则新建
      • 新建了一个ExtensionLoader,赋值成员变量clazz,并执行ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getExtensionClasses();代码,
        重新执行getExtensionLoader方法,创建ExtensionLoader,并赋值ExtensionFactory.class为key,ExtensionLoader为value到LOADERS当中,执行getExtensionClasses方法,将ExtensionFactory全限定名为文件,在META-INF/soul下的文件做了一次检索,其实就相当于在首次进入获取扩展加载器方法时,默认额外加载了一个ExtensionFactory类的自定义扩展加载器,并使用soul的规则,去加载它的SPI实现类,并缓存起来。加载完毕后,JdbcSPI.class new出来的扩展加载器也放置到了LOADERS当中,并返回。
      • 接下来进入到了getJoin("mysql")方法的步骤,之前一步仅仅是获取了加载器,创建一个加载器,记录了接口class,并缓存起来了。进入getJoin方法则是去搜寻以及实例化指定的对象


        先回去cachedInstances缓存中拿,看有没有实例对象,如果没有,则新增一个Holder内部类,用来盛装以后的对象,判断objectHolder.getValue()的值是不是null,如果是的话,那么,添加synchorized代码块,锁住cachedInstances,方式在并发中,产生cachedInstances重复操作的问题,之后再次进行value判断,类似双重加锁单例模式,接下来代码进入到value = createExtension(name);
      • createExtension(name);作用是,创建扩展类实例,在该方法,进入到getExtensionClasses()方法
      • getExtensionClasses,该方法作用是获取扩展的所有@Join实现类


        方法先会从cachedClasses中拿取对象,如果没有,则再去加载,此处,也进行加锁操作,防止重复操作,浪费性能。
      • 代码进入到loadExtensionClass();方法当中,


        获取成员变量clazz的SPI注解,该 变量为之前获取加载器的时候注入。获取SPI注解上的value值,用以设定cachedDefaultName,即DefaultJoin key值,并创建一个初始化大小为16的hashmap用来存放spi实现的class
      • 进入到loadDirectory(classes);方法当中


        扫描META-INF/soul/ + clazz.getname即接口全限定名,获得文件的路径加名称获取类加载器,获取到的是sun.misc.Launcher$AppClassLoader 应用程序加载器,如果为空,则直接使用ClassLoader加载系统资源文件,加载文件后,开始读取文件内容
      • 进入 loadResources(classes, url);方法,使用Properties加载输入流,以键值对进行循环

      • 进入loadClass(classes, name, classPath);方法,加载Class


        使用Class.forName(classPath);获取到class,判断该实现类是否实现接口,是否添加@join注解,并将key 和Class设置到map当中,如果文件中key值存在相同的,则抛出异常。返回包含 key class 的map
      • 获取到所有的Class后,返回。此时,成员变量 cachedClasses 的value值为 返回的map,
        返回到createExtension方法,并通过name值获取到对应的class,并以Class为key值,对象实例为value值,设置到成员变量joinInstances当中。
      • 返回到getJoin方法中,此时,cachedInstance,中缓存值为 key为name值,value为对象的Holder值,最终,返回,得到指定的对象。
    • 以上,便是首次获取指定SPI对象的过程
    • 如果是已经获取过一次对象,那么,下次获取的时候,直接通过Class获取到ExtensionLoader,并通过ExtensionLoader的成员变量获取到缓存对象。即,可以知道,多次通过SPI获取的对象是同一个。

       
  • 代码成员变量含义
     
        
        //Soul SPI加载的目录
        private static final String SOUL_DIRECTORY = "META-INF/soul/";
        //(k,v)->(Class, ExtensionLoader) 缓存class的soul加载器
        private static final Map<Class<?>, ExtensionLoader<?>> LOADERS = new ConcurrentHashMap<>();
        //获取指定接口加载器的接口class,为添加SPI注解的接口class
        private final Class<T> clazz;
        //在文件当中加载到的所有class
        private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
        //缓存的实例对象,key为name value为实例,之后名称相同的对象直接从这个缓存取用,由于是getJoin存放的,存放的数量可能不是所有的clazz
        private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
        //缓存的实例对象,key值为加载到的class,value值为实例对对象
        private final Map<Class<?>, Object> joinInstances = new ConcurrentHashMap<>();
        //接口@SPI指定的name名,没指定则为空
        private String cachedDefaultName;

  • getJoin时,给定的名字没有,则会去重新加载文件,找不到报错 name is error,有则缓存,后续调用加载,都从缓存中取用。

  • SOUL网关自定义SPI,使用场景
    • 客户端数据注册方式,接口数据发送至souladmin的方式
    • 服务端接收数据接收方式,souladmin接收数据类选择
    • 规则匹配and or策略选择
    • 负载均衡算法选择
    • 限流算法选择
    • 追踪器选择
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值