上一篇簡單的說了一下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