SPI框架實現之旅二:總體設計

SPI框架實現之旅二:總體設計

上一篇簡單的說了一下spi相關的東西, 接下來咱們準備開動,本篇博文主要集中在一些術語,使用規範的約定和使用方式java

設計思路

下圖圍繞 SpiLoader 爲中心,描述了三個主要的流程:git

  1. load全部的spi實現
  2. 初始化選擇器 selector
  3. 獲取spi實現類 (or一個實現類代理)

https://static.oschina.net/uploads/img/201705/26185143_ULnL.png


基礎類說明

主要介紹一下框架中涉及到的接口和註解,並指出須要注意的點spring

1. Selector 選擇器

爲了最大程度的支持業務方對spi實現類的選擇,咱們定義了一個選擇器的概念,用於獲取spi實現類緩存

接口定義以下:

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

結合上面的接口定義,咱們能夠考慮下,選擇器應該如何工做?

  • 根據傳入的條件,從全部的實現類中,找到一個最匹配的實現類返回
  • 若是查不到,則拋一個異常NoSpiMatchException出去

因此傳入的參數會是兩個, 一個是全部的實現類列表map(至於上面爲何用map,後續分析),一個是用於判斷的輸入條件confapp

框架中會提供兩種基本的選擇器實現,

  • DefaultSelector , 對每一個實現類賦予惟一的name,默認選擇器則表示根據name來查找實現類
  • ParamsSelector, 在實現類上加上 @SpiConf 註解,定義其中的 params,當傳入的參數(conf), 能徹底匹配定義的params,表示這個實現類就是你所須要的

自定義實現

自定義實現比較簡單,實現上面的接口便可框架

2. Spi 註解

要求全部的spi接口,都必須有這個註解;ide

定義以下

主要是有一個參數,用於指定是選擇器類型,定義spi接口的默認選擇器,測試

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

說明

在上一篇《SPI框架實現之旅一》中,使用jdk的spi方式中,並無使用註解依然能夠正常工做,咱們這裏定義這個註解且要求必需有,出於下面幾個考慮優化

  • 醒目,告訴開發者,這個接口是聲明的spi接口, 使用的時候注意下
  • 加入選擇器參數,方便用戶擴展本身的選擇方式

3. 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 來確認具體的實現類;而 writepp 方法則是採用 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);
}

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 {
    @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註解,採起下面的規則
    • 方法註解聲明name時,兩個會同時生效,即想調用上面的echo方法, 經過傳入 ConsoleCode(類註解不顯示賦值時,採用類名代替) 和 consoleEcho 等價
    • 方法註解未聲明name時,只能經過類註解上定義的name(or默認的類名)來選擇
    • order,取最高優先級,如上面的 write 方法的優先級是 1; 當未顯示定義order時,以定義的爲準
    • params: 取並集,即要求類上 + 方法上的條件都知足

SPI加載器

spi加載器的主要業務邏輯集中在 SpiLoader 類中,包含經過spi接口,獲取全部的實現類; 獲取spi接口對應的選擇器 (包括類對應的選擇器, 方法對應的選擇器); 返回Spi接口實現類(靜態確認的實現類,自適應的代理類)

從上面的簡述,基本上能夠看出這個類劃分爲三個功能點, 下面將逐一說明,本篇博文主要集中在邏輯的設計層,至於優化(如懶加載,緩存優化等) 放置下一篇博文單獨敘述

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 類型, 不支持抽象類和類的方式

拓展

雖然這裏直接使用了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;
    }

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);
   }
}

說明

  1. SeectorWrapper 選擇器封裝類

    這裏咱們在獲取選擇器時,特地定義了一個封裝類,其中包含具體的選擇器對象,以及所匹配的參數類型,所以能夠在下一步經過選擇器獲取實現類時,保證傳入的參數類型合法

  2. private SelectorWrapper initSelector(Class<? extends ISelector> clz) 具體的實例化選擇器的方法

    從實現來看,優先從選擇器緩存中獲取選擇器對象,這樣的目的是保證一個spi接口,每種類型的選擇器只有一個實例;所以在自定義選擇器中,你徹底能夠作一些選擇判斷的緩存邏輯,如 ParamsSelector 中的spi實現類的有序緩存列表

  3. currentSelector , currentMethodSelector, selectorInstanceCacheMap

    currentSelector:   對應的是類選擇器,每一個SPI接口必然會有一個,做爲打底的選擇器
     currentMethodSelector:  方法選擇器映射關係表,key爲方法名,value爲該方法對應的選擇器; 因此spi接口中,不支持重載
     selectorInstanceCacheMap: spi接口全部定義的選擇器映射關係表,key爲選擇器類型,value是實例;用於保障每一個spi接口中選擇器只會有一個實例

3. 獲取實現類

對使用者而言,最關注的就是這個接口,這裏會返回咱們須要的實現類(or代理);內部的邏輯也比較清楚,首先肯定選擇器,而後經過選擇器便利全部的實現類,把知足條件的返回便可

從上面的描述能夠看到,主要分爲兩步

  1. 獲取選擇器
  2. 根據選擇器,遍歷全部的實現類,找出匹配的返回

獲取選擇器

初始化選擇器以後,咱們會有 currentSelector , currentMethodSelector 兩個緩存

  • 靜態肯定spi實現時,直接用 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 主要是初始化全部的選擇器, 以下圖

  • 首先從緩存中查
  • 是否已經初始化過了有則直接返回;
  • 緩存中沒有,則進入new一個新的對象出來
    • 解析類上註解 @Spi,初始化 currentSelector
    • 解析全部方法的註解 @SpiAdaptive , 初始化 currentMethodSelector
  • 塞入緩存,並返回

https://static.oschina.net/uploads/img/201705/27140821_19ee.png

IPrint print = spiLoader.getService("ConsolePrint");

根據name獲取實現類,具體流程以下

  • 判斷是否加載過全部實現類 spiImplClassCacheMap
  • 沒有加載,則從新加載全部的實現類
    • 經過jdk的 ServiceLoader.load() 方法獲取全部的實現類
    • 遍歷實現類,根據 @SpiConf 註解初始化參數,封裝 SpiImplWrapper 對象
    • 保存封裝的 SpiImplWrapper對象到緩存
  • 執行 currentSelector.select() 方法,獲取匹配的實現類

https://static.oschina.net/uploads/img/201705/27150620_EOUL.png

其餘

博客系列連接:

源碼地址:

https://git.oschina.net/liuyueyi/quicksilver/tree/master/silver-spi

相關文章
相關標籤/搜索