上一篇简单的说了一下spi相关的东西, 接下来咱们准备开动,本篇博文主要集中在一些术语,使用规范的约定和使用方式java
下图围绕 SpiLoader
为中心,描述了三个主要的流程:git
主要介绍一下框架中涉及到的接口和注解,并指出须要注意的点spring
Selector
选择器为了最大程度的支持业务方对spi实现类的选择,咱们定义了一个选择器的概念,用于获取spi实现类缓存
public interface ISelector<T> { <K> K selector(Map<String, SpiImplWrapper<K>> map, T conf) throws NoSpiMatchException; }
NoSpiMatchException
出去因此传入的参数会是两个, 一个是全部的实现类列表map
(至于上面为何用map,后续分析),一个是用于判断的输入条件conf
app
DefaultSelector
, 对每一个实现类赋予惟一的name,默认选择器则表示根据name来查找实现类ParamsSelector
, 在实现类上加上 @SpiConf
注解,定义其中的 params
,当传入的参数(conf
), 能彻底匹配定义的params,表示这个实现类就是你所须要的自定义实现比较简单,实现上面的接口便可框架
Spi
注解要求全部的spi接口,都必须有这个注解;ide
主要是有一个参数,用于指定是选择器类型,定义spi接口的默认选择器,测试
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Spi { Class<? extends ISelector> selector() default DefaultSelector.class; }
在上一篇《SPI框架实现之旅一》中,使用jdk的spi方式中,并无使用注解依然能够正常工做,咱们这里定义这个注解且要求必需有,出于下面几个考虑优化
SpiAdaptive
注解对须要自适应的场景,为了知足一个spi接口,应用多重不一样的选择器场景,能够加上这个注解; 若是不加这个注解,则表示采用默认的选择器来自适应ui
/** * 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); @SpiAdaptive void 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); }
SpiConf
注解这个主键主要是用在实现类上(或实现类的方法上),里面存储一些选择条件,一般是和
Selector
搭配使用
定义了三个字段:
DefaultSelector
;ParamsSelector
;@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 { @Override public 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
注解,采起下面的规则
ConsoleCode
(类注解不显示赋值时,采用类名代替) 和 consoleEcho
等价write
方法的优先级是 1; 当未显示定义order时,以定义的为准spi加载器的主要业务逻辑集中在
SpiLoader
类中,包含经过spi接口,获取全部的实现类; 获取spi接口对应的选择器 (包括类对应的选择器, 方法对应的选择器); 返回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 }
META_INF.services
下新建一个文件, 文件名为包含包路径的spi接口名, 内部为包含包路径的实现类名@Spi
注解interface
类型, 不支持抽象类和类的方式虽然这里直接使用了spi的规范,咱们其实彻底能够本身定义标准的,只要能将这个接口的全部实现类找到, 怎么实现均可以由你定义
如使用spring框架后,能够考虑经过 applicationContext.getBeansOfAnnotaion(xxx )
来获取全部的特定注解的bean,这样就能够不须要本身新建一个文件,来存储spi接口和其实现类的映射关系了
上面获取了spi实现类,显然咱们的目标并不局限于简单的获取实现类,在获取实现类以后,还须要解析其中的 @SpiConf
注解信息,用于表示要选择这个实现,必须知足什么样的条件
SpiImplWrapper
: spi实现类,以及定义的各类条件的封装类
注解的解析过程流程以下:
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; }
咱们的选择器会区分为两类,一个是类上定义的选择器, 一个是方法上定义的选择器; 在自适应的使用方式中,方法上定义的优先级 > 类上定义
简单来说,初始化选择器,就是扫一遍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接口中选择器只会有一个实例
对使用者而言,最关注的就是这个接口,这里会返回咱们须要的实现类(or代理);内部的逻辑也比较清楚,首先肯定选择器,而后经过选择器便利全部的实现类,把知足条件的返回便可
从上面的描述能够看到,主要分为两步
初始化选择器以后,咱们会有 currentSelector
, currentMethodSelector
两个缓存
currentSelector
便可 (spi接口中全部方法都公用类定义选择器)currentMethodSelector
中获取选择器,若是没有,则表示该方法没有@SpiAdaptive
注解,直接使用类的选择器 currentMethodSelector
便可// 动态适配时,获取方法对应对应的selector实现逻辑 SelectorWrapper selector = currentMethodSelector.get(methodName); if (selector == null) { // 自适应方法上未定义选择器, 则默认继承类的 selector = currentSelector; currentMethodSelector.putIfAbsent(methodName, selector); } if (!selector.getConditionType().isAssignableFrom(conf.getClass())) { // 选择器类型校验 if (!(conf instanceof String)) { throw new IllegalArgumentException("conf spiInterfaceType should be sub class of [" + currentSelector.getConditionType() + "] but yours:" + conf.getClass()); } // 参数不匹配时,且传入的参数为String类型, 则尝试使用默认选择器进行兼容(不建议在实现时,出现这种场景) selector = DEFAULT_SELECTOR; }
这个的主要逻辑就是遍历全部的实现类,判断是否知足选择器的条件,将第一个找到的返回便可,全部的业务逻辑都在 ISelector
中实现,以下面给出的默认选择器,根据name来获取实现类
/** * 默认的根据name 获取具体的实现类 * <p/> * Created by yihui on 2017/5/24. */ public class DefaultSelector implements ISelector<String> { @Override public <K> K selector(Map<String, SpiImplWrapper<K>> map, String name) throws NoSpiMatchException { if (StringUtils.isBlank(name)) { throw new IllegalArgumentException("spiName should not be empty!"); } if (map == null || map.size() == 0) { throw new IllegalArgumentException("no impl spi!"); } if (!map.containsKey(name)) { throw new NoSpiMatchException("no spiImpl match the name you choose! your choose is: " + name); } return map.get(name).getSpiImpl(); } }
上面主要就各个点单独的进行了说明,看起来可能比较分散,看完以后可能没有一个清晰的流程,这里就整个实现的流程顺一遍,主要从使用者的角度出发,当定义了一个SPI接口后,到获取spi实现的过程当中,上面的这些步骤是怎样串在一块儿的
先拿简单的静态获取SPI实现流程说明(动态的其实差很少,具体的差别下一篇说明),先看下这种用法的使用姿式
@Spi public interface IPrint { void print(String str); } public class FilePrint implements IPrint { @Override public void print(String str) { System.out.println("file print: " + str); } } public class ConsolePrint implements IPrint { @Override public void print(String str) { System.out.println("console print: " + str); } } @Test public void testPrint() throws NoSpiMatchException { SpiLoader<IPrint> spiLoader = SpiLoader.load(IPrint.class); IPrint print = spiLoader.getService("ConsolePrint"); print.print("console---->"); }
SpiLoader<IPrint> spiLoader = SpiLoader.load(IPrint.class);
这行代码触发的action 主要是初始化全部的选择器, 以下图
@Spi
,初始化 currentSelector
@SpiAdaptive
, 初始化 currentMethodSelector
IPrint print = spiLoader.getService("ConsolePrint");
根据name获取实现类,具体流程以下
spiImplClassCacheMap
ServiceLoader.load()
方法获取全部的实现类@SpiConf
注解初始化参数,封装 SpiImplWrapper
对象SpiImplWrapper
对象到缓存currentSelector.select()
方法,获取匹配的实现类博客系列连接:
源码地址:
https://git.oschina.net/liuyueyi/quicksilver/tree/master/silver-spi