1.美图

2.概述

参考:添加链接描述

看原文去,原文是一个系列

SPI的设计思路,下图围绕 SpiLoader 为中心,描述了三个主要的流程:

  1. load所有的spi实现
  2. 初始化选择器 selector
  3. 获取spi实现类 (or一个实现类代理)

3.基础类

3.1 Selector 选择器

Seclector是SPI的一个选择器的概念,用于获取spi实现类

为了最大程度的支持业务方对spi实现类的选择,我们定义了一个选择器的概念,用于获取spi实现类

接口定义如下:

public interface ISelector<T> {<K> K selector(Map<String, SpiImplWrapper<K>> map, T conf) throws NoSpiMatchException;
}

结合上面的接口定义,我们可以考虑下,选择器应该如何工作?

  • 根据传入的条件,从所有的实现类中,找到一个最匹配的实现类返回
  • 如果查不到,则抛一个异常NoSpiMatchException出去

所以传入的参数会是两个, 一个是所有的实现类列表map(至于上面为什么用map,后续分析),一个是用于判断的输入条件conf

框架中会提供两种基本的选择器实现,

  • DefaultSelector , 对每个实现类赋予唯一的name,默认选择器则表示根据name来查找实现类

  • ParamsSelector, 在实现类上加上 @SpiConf 注解,定义其中的 params,当传入的参数(conf), 能完全匹配定义的params,表示这个实现类就是你所需要的

自定义实现

自定义实现比较简单,实现上面的接口即可

3.2. Spi 注解

要求所有的spi接口,都必须有这个注解;

定义如下

主要是有一个参数,用于指定是选择器类型,定义spi接口的默认选择器,

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Spi {Class<? extends ISelector> selector() default DefaultSelector.class;
}

说明
在上一篇《SPI框架实现之旅一》中,使用jdk的spi方式中,并没有使用注解依然可以正常工作,我们这里定义这个注解且要求必需有,出于下面几个考虑

  • 醒目,告诉开发者,这个接口是声明的spi接口, 使用的时候注意下
  • 加入选择器参数,方便用户扩展自己的选择方式

3.3 SpiAdaptive 注解

对需要自适应的场景,为了满足一个spi接口,应用多重不同的选择器场景,可以加上这个注解;
如果不加这个注解,则表示采用默认的选择器来自适应

接口说明

/*** SPI 自适应注解, 表示该方法会用到spi实现* <p/>* Created by yihui on 2017/5/24.*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface SpiAdaptive {Class<? extends ISelector> selector() default DefaultSelector.class;
}

说明
这个注解内容和 @Spi 基本上一模一样,唯一的区别是一个放在类上,一个放在方法上,那么为什么这么考虑?

@Spi 注解放在类上,更多的表名这个接口是我们定义的一个SPI接口,但是使用方式可以有两种(静态 + 动态确认)
@SpiAdaptive 只能在自适应的场景下使用,用于额外指定spi接口中某个方法的选择器 (如果一个spi接口全部只需要一个选择器即可,那么可以不使用这个注解)
如下面的这个例子,print方法和 echo方法其实是等价的,都是采用 DefaultSelector 来确认具体的实现类;而 write 和 pp 方法则是采用 ParamsSelector 选择器;

/*** Created by yihui on 2017/5/25.*/
@Spi
public interface ICode {void print(String name, String contet);@SpiAdaptivevoid echo(String name, String content);@SpiAdaptive(selector = ParamsSelector.class)void write(Context context, String content);@SpiAdaptive(selector = ParamsSelector.class)void pp(Context context, String content);
}

3.4 4. SpiConf 注解

这个主键主要是用在实现类上(或实现类的方法上),里面存储一些选择条件,通常是和Selector搭配使用

定义如下
定义了三个字段:

name 唯一标识,用于 DefaultSelector;
params 参数条件, 用于 ParamsSelector;
order : 优先级, 主要是为了解决多个实现类都满足选择条件时, 应该选择哪一个 (谈到这里就有个想法, 通过一个参数,来选择是否让满足条件的全部返回)

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface SpiConf {/*** 唯一标识** @return*/String name() default "";/*** 参数过滤, 单独一个元素,表示参数必须包含; 用英文分号,左边为参数名,右边为参数值,表示参数的值必须是右边的* <p/>* 形如  {"a", "a:12", "b:TAG"}** @return*/String[] params() default {};/*** 排序, 越小优先级越高** @return*/int order() default -1;
}

说明
SpiConf 注解可以修饰类,也可以修饰方法,因此当一个实现类中,类和方法都有这个注解时, 怎么处理 ?

以下面的这个测试类进行说明

/*** Created by yihui on 2017/5/25.*/
@SpiConf(params = "code", order = 1)
public class ConsoleCode implements ICode {@Overridepublic void print(String name, String contet) {System.out.println("console print:--->" + contet);}/*** 显示指定了name, 因此可以直接通过 consoleEcho 来确定调用本实现方法* @param name* @param content*/@Override@SpiConf(name = "consoleEcho")public void echo(String name, String content) {System.out.println("console echo:---->" + content);}/*** 实际的优先级取 方法 和类上的最高优先级, 实际为1; * `ParamsSelector`选择器时, 执行该方法的条件等同于  `{"code", "type:console"}`* @param context* @param content*/@Override@SpiConf(params = {"type:console"}, order = 3)public void write(Context context, String content) {System.out.println("console write:---->" + content);}
}

在设计中,遵循下面几个原则:

类上的SpiConf注解, 默认适用与类中的所有方法
方法上有SpiConf注解,采取下面的规则
方法注解声明name时,两个会同时生效,即想调用上面的echo方法, 通过传入 ConsoleCode(类注解不显示赋值时,采用类名代替) 和 consoleEcho 等价
方法注解未声明name时,只能通过类注解上定义的name(or默认的类名)来选择
order,取最高优先级,如上面的 write 方法的优先级是 1; 当未显示定义order时,以定义的为准
params: 取并集,即要求类上 + 方法上的条件都满足

4. SPI加载器

spi加载器的主要业务逻辑集中在 SpiLoader 类中,包含通过spi接口,获取所有的实现类; 获取spi接口对应的选择器 (包括类对应的选择器, 方法对应的选择器); 返回Spi接口实现类(静态确认的实现类,自适应的代理类)

从上面的简述,基本上可以看出这个类划分为三个功能点, 下面将逐一说明,本篇博文主要集中在逻辑的设计层,至于优化(如懒加载,缓存优化等) 放置下一篇博文单独叙述

4.1. 加载spi实现类

这一块比较简单,我们直接利用了jdk的 ServiceLoader 来根据接口,获取所有的实现类;因此我们的spi实现,需要满足jdk定义的这一套规范

具体的代码业务逻辑非常简单,大致流程如下

 if (null == spiInterfaceType) {throw new IllegalArgumentException("common cannot be null...");
}if (!spiInterfaceType.isInterface()) {throw new IllegalArgumentException("common class:" + spiInterfaceType + " must be interface!");
}if (!withSpiAnnotation(spiInterfaceType)) {throw new IllegalArgumentException("common class:" + spiInterfaceType + " must have the annotation of @Spi");
}ServiceLoader<T> serviceLoader = ServiceLoader.load(spiInterfaceType);
for(T spiImpl: serviceLoader) {// xxx
}

注意
因为使用了jdk的标准,因此每定义一个spi接口,必须在 META_INF.services 下新建一个文件, 文件名为包含包路径的spi接口名, 内部为包含包路径的实现类名
每个spi接口,要求必须有 @Spi 注解
Spi接口必须是 interface 类型, 不支持抽象类和类的方式

4.1.1 拓展

虽然这里直接使用了spi的规范,我们其实完全可以自己定义标准的,只要能将这个接口的所有实现类找到, 怎么实现都可以由你定义

如使用spring框架后,可以考虑通过 applicationContext.getBeansOfAnnotaion(xxx ) 来获取所有的特定注解的bean,这样就可以不需要自己新建一个文件,来存储spi接口和其实现类的映射关系了

构建spi实现的关系表
上面获取了spi实现类,显然我们的目标并不局限于简单的获取实现类,在获取实现类之后,还需要解析其中的 @SpiConf 注解信息,用于表示要选择这个实现,必须满足什么样的条件

SpiImplWrapper : spi实现类,以及定义的各种条件的封装类

注解的解析过程流程如下:

name: 注解定义时,采用定义的值; 否则采用简单类名 (因此一个系统中不允许两个实现类同名的情况)
order: 优先级, 注解定义时,采用定义的值;未定义时采用默认;
params: 参数约束条件, 会取类上和方法上的并集(原则上要求类上的约束和方法上的约束不能冲突)

List<SpiImplWrapper<T>> spiServiceList = new ArrayList<>();// 解析注解
spiConf = t.getClass().getAnnotation(SpiConf.class);Map<String, String> map;if (spiConf == null) { // 没有添加注解时, 采用默认的方案implName = t.getClass().getSimpleName();implOrder = SpiImplWrapper.DEFAULT_ORDER;// 参数选择器时, 要求spi实现类必须有 @SpiConf 注解, 否则选择器无法获取校验条件参数if (currentSelector.getSelector() instanceof ParamsSelector) {throw new IllegalStateException("spiImpl must contain annotation @SpiConf!");}map = Collections.emptyMap();} else {implName = spiConf.name();if (StringUtils.isBlank(implName)) {implName = t.getClass().getSimpleName();}implOrder = spiConf.order() < 0 ? SpiImplWrapper.DEFAULT_ORDER : spiConf.order();map = parseParms(spiConf.params());}// 添加一个类级别的封装类spiServiceList.add(new SpiImplWrapper<>(t, implOrder, implName, map));// ------------// 解析参数的方法private Map<String, String> parseParms(String[] params) {if (params.length == 0) {return Collections.emptyMap();}Map<String, String> map = new HashMap<>(params.length);String[] strs;for (String param : params) {strs = StringUtils.split(param, ":");if (strs.length >= 2) {map.put(strs[0].trim(), strs[1].trim());} else if (strs.length == 1) {map.put(strs[0].trim(), null);}}return map;}

4.1.2 初始化选择器

我们的选择器会区分为两类,一个是类上定义的选择器, 一个是方法上定义的选择器; 在自适应的使用方式中,方法上定义的优先级 > 类上定义

简单来讲,初始化选择器,就是扫一遍SPI接口中的注解,实例化选择器后,缓存住对应的结果, 实现如下

 /**
* 选择器, 根据条件, 选择具体的 SpiImpl;
*/
private SelectorWrapper currentSelector;/**
* 自适应时, 方法对应的选择器
*/
private Map<String, SelectorWrapper> currentMethodSelector;/**
* 每一个 SpiLoader 中, 每种类型的选择器, 只保存一个实例
* 因此可以在选择器中, 如{@link ParamsSelector} 对spiImplMap进行处理并缓存结果
*/
private ConcurrentHashMap<Class, SelectorWrapper> selectorInstanceCacheMap = new ConcurrentHashMap<>();private void initSelector() {Spi ano = spiInterfaceType.getAnnotation(Spi.class);if (ano == null) {currentSelector = initSelector(DefaultSelector.class);} else {currentSelector = initSelector(ano.selector());}Method[] methods = this.spiInterfaceType.getMethods();currentMethodSelector = new ConcurrentHashMap<>();SelectorWrapper temp;for (Method method : methods) {if (!method.isAnnotationPresent(SpiAdaptive.class)) {continue;}temp = initSelector(method.getAnnotation(SpiAdaptive.class).selector());if (temp == null) {continue;}currentMethodSelector.put(method.getName(), temp);}
}private SelectorWrapper initSelector(Class<? extends ISelector> clz) {// 优先从选择器缓存中获取类型对应的选择器if (selectorInstanceCacheMap.containsKey(clz)) {return selectorInstanceCacheMap.get(clz);}try {ISelector selector = clz.newInstance();Class paramClz = null;Type[] types = clz.getGenericInterfaces();for (Type t : types) {if (t instanceof ParameterizedType) {paramClz = (Class) ((ParameterizedType) t).getActualTypeArguments()[0];break;}}Assert.check(paramClz != null);SelectorWrapper wrapper = new SelectorWrapper(selector, paramClz);selectorInstanceCacheMap.putIfAbsent(clz, wrapper);return wrapper;} catch (Exception e) {throw new IllegalArgumentException("illegal selector defined! yous:" + clz);}
}

说明
SeectorWrapper 选择器封装类

这里我们在获取选择器时,特意定义了一个封装类,其中包含具体的选择器对象,以及所匹配的参数类型,因此可以在下一步通过选择器获取实现类时,保证传入的参数类型合法

private SelectorWrapper initSelector(Class<? extends ISelector> clz) 具体的实例化选择器的方法

从实现来看,优先从选择器缓存中获取选择器对象,这样的目的是保证一个spi接口,每种类型的选择器只有一个实例;因此在自定义选择器中,你完全可以做一些选择判断的缓存逻辑,如 ParamsSelector 中的spi实现类的有序缓存列表

currentSelector , currentMethodSelector, selectorInstanceCacheMap

currentSelector: 对应的是类选择器,每个SPI接口必然会有一个,作为打底的选择器
currentMethodSelector: 方法选择器映射关系表,key为方法名,value为该方法对应的选择器; 所以spi接口中,不支持重载
selectorInstanceCacheMap: spi接口所有定义的选择器映射关系表,key为选择器类型,value是实例;用于保障每个spi接口中选择器只会有一个实例

4.3 获取实现类

对使用者而言,最关注的就是这个接口,这里会返回我们需要的实现类(or代理);内部的逻辑也比较清楚,首先确定选择器,然后通过选择器便利所有的实现类,把满足条件的返回即可
从上面的描述可以看到,主要分为两步

获取选择器
根据选择器,遍历所有的实现类,找出匹配的返回
获取选择器
初始化选择器之后,我们会有 currentSelector , currentMethodSelector 两个缓存

静态确定spi实现时,直接用 currentSelector 即可 (spi接口中所有方法都公用类定义选择器)
动态适配时, 根据方法名在 currentMethodSelector 中获取选择器,如果没有,则表示该方法没有@SpiAdaptive注解,直接使用类的选择器 currentMethodSelector 即可

M.参考

参考:添加链接描述

【java】java基础之SPI框架实现-整体设计相关推荐

  1. 【SPI】java基础之SPI框架实现

    1.美图 2.概述 SPI的全名为Service Provider Interface,简单的总结下java spi机制的思想.我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案, ...

  2. java设计模式--基础思想总结--抽象类与架构设计思想

    抽象类?这个东西我感觉没什么卵用啊,又不能拿来new对象,没有具体的对象的抽象类,有什么实际的意义呢?这是很多刚刚接触java抽象类语法时的第一反应(当然,包括我).确实,很多刚刚接触抽象类这个概念的 ...

  3. ibatis源码学习(一)整体设计和核心流程

    本文主要从ibatis框架的基本代码骨架进行切入,理解ibatis框架的整体设计思路,各组件的实现细节将在后文进行分析. [b][size=large]背景[/size][/b] 介绍ibatis实现 ...

  4. Thinking in java基础之集合框架

    Thinking in java基础之集合框架 大家都知道我的习惯,先上图说话. 集合简介(容器) 把具有相同性质的一类东西,汇聚成一个整体,就可以称为集合,例如这里有20个苹果,我们把每一个苹果当成 ...

  5. java把map值放入vector_Thinking in java基础之集合框架

    Thinking in java基础之集合框架 大家都知道我的习惯,先上图说话. 集合简介(容器) 把具有相同性质的一类东西,汇聚成一个整体,就可以称为集合,例如这里有20个苹果,我们把每一个苹果当成 ...

  6. Java基础知识第二讲:Java开发手册/JVM/集合框架/异常体系/Java反射/语法知识/Java IO

    Java基础知识第二讲(Java编程规范/JVM/集合框架/异常体系/Java反射/语法知识/Java IO/码出高效) 分享在java学习及工作中,常使用的一些基础知识,本文从JVM出发,讲解了JV ...

  7. JAVA教程-JAVA语言基础框架知识学习点-JAVA精通必看

    JAVA教程中文版在线代码示例 1. JAVA语言基础 1. 1. 导言( 17 ) 1. 9. 变量( 6 ) 1. 2. Java关键词( 1 ) 1. 10. 变量范围( 2 ) 1. 3. J ...

  8. Netty框架-java网络基础-1

    1,java网络基础 计算机网络体系结构 OSI 七层模型 1,物理层 : 物理层并不是物理媒体本身,他只是开放系统中利用物理媒体实现物理连接的功能描述和执行连接的规范: DTE , DCE(物理光猫 ...

  9. java gui中文变方块_150道Java面试基础题(含答案)

    1)Java 中能创建 volatile 数组吗? 能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组.我的意思是,如果改变引用指向的数组,将会受到 vo ...

最新文章

  1. 一个QT比较好的博客资源
  2. Linux深入理解Socket异常
  3. linux ruby gem 安装目录,linux 安装 gem
  4. LruCache缓存bitmap(一)
  5. sockaddr_in 结构体
  6. 网页文字无法免费复制的几种解决方法
  7. SPT20 协议_【笔试时间有变】关于国家电网三方协议的那些事!
  8. java 数组协变_java 数组协变
  9. 20165333 2017-2018-2《Java程序设计》课程总结
  10. 大年三十整理的asp.net资料!(不得不收藏)
  11. FastAPI系列(1):FastAPI简介
  12. regedit是什么意思_regedit用法_regedit参数_reg的注册与反注册
  13. 智能车心得分享(五)-- 电磁排布
  14. 计算机打字的基础知识,学习电脑打字基础知识
  15. php 微信 语音,微信语音的上传与下载功能实现详解
  16. C语言程序设计卢萍,卢萍
  17. TTD 专题 (第一篇):C# 那些短命线程都在干什么?
  18. C# CheckBox/CheckedListBox 复选框/复选框列表
  19. HBase2.x(六)HBase API DML的操作
  20. 手把手识别125K读卡模块的设计方案

热门文章

  1. 北京移动联合中兴通讯率先完成SON 4/5G全制式规模部署
  2. 中芯国际能靠14nm工艺翻身么?
  3. 首批国产特斯拉车主,被“割了韭菜”
  4. “我在B站有套房”成真:云蹦迪、不停学还健身
  5. 一加8系列新机有望亮相CES 2020:全系支持5G网络
  6. 降级无门!苹果关闭iOS 13.2.2验证通道
  7. 宁德时代811电芯初现真容 搭配宝马X1混动汽车能量密度提升近6成
  8. python 装饰器
  9. mysql bin 恢复工具_基于binlog恢复工具mysqlbinlog_flashback
  10. 程序员交接文档_一个.NET程序员 2019 跳槽3次的悲惨故事