- 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值,最终,返回,得到指定的对象。
- 先看getExtensionLoader方法
- 以上,便是首次获取指定SPI对象的过程
- 如果是已经获取过一次对象,那么,下次获取的时候,直接通过Class获取到ExtensionLoader,并通过ExtensionLoader的成员变量获取到缓存对象。即,可以知道,多次通过SPI获取的对象是同一个。
- 先走一遍代码:运行ExtensionLoader.getExtensionLoader(JdbcSPI.class).getJoin("mysql");
- 代码成员变量含义
//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策略选择
- 负载均衡算法选择
- 限流算法选择
- 追踪器选择