SPI框架實現之旅二:整體設計
上一篇簡單的說了一下spi相關的東西, 接下來我們準備開動,本篇博文主要集中在一些術語,使用規(guī)范的約定和使用方式
設計思路
下圖圍繞 SpiLoader 為中心,描述了三個主要的流程:
- load所有的spi實現
- 初始化選擇器 selector
- 獲取spi實現類 (or一個實現類代理)

基礎類說明
主要介紹一下框架中涉及到的接口和注解,并指出需要注意的點
1. Selector 選擇器
為了最大程度的支持業(yè)務方對spi實現類的選擇,我們定義了一個選擇器的概念,用于獲取spi實現類
接口定義如下:
public interface ISelector<T> {
<K> K selector(Map<String, SpiImplWrapper<K>> map, T conf) throws NoSpiMatchException;
}
結合上面的接口定義,我們可以考慮下,選擇器應該如何工作?
- 根據傳入的條件,從所有的實現類中,找到一個最匹配的實現類返回
- 如果查不到,則拋一個異常
NoSpiMatchException出去
所以傳入的參數會是兩個, 一個是所有的實現類列表map(至于上面為什么用map,后續(xù)分析),一個是用于判斷的輸入條件conf
框架中會提供兩種基本的選擇器實現,
-
DefaultSelector, 對每個實現類賦予唯一的name,默認選擇器則表示根據name來查找實現類 -
ParamsSelector, 在實現類上加上@SpiConf注解,定義其中的params,當傳入的參數(conf), 能完全匹配定義的params,表示這個實現類就是你所需要的
自定義實現
自定義實現比較簡單,實現上面的接口即可
2. Spi 注解
要求所有的spi接口,都必須有這個注解;
定義如下
主要是有一個參數,用于指定是選擇器類型,定義spi接口的默認選擇器,
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Spi {
Class<? extends ISelector> selector() default DefaultSelector.class;
}
說明
在上一篇《SPI框架實現之旅一》中,使用jdk的spi方式中,并沒有使用注解依然可以正常工作,我們這里定義這個注解且要求必需有,出于下面幾個考慮
- 醒目,告訴開發(fā)者,這個接口是聲明的spi接口, 使用的時候注意下
- 加入選擇器參數,方便用戶擴展自己的選擇方式
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 基本上一模一樣,唯一的區(qū)別是一個放在類上,一個放在方法上,那么為什么這么考慮?
-
@Spi注解放在類上,更多的表名這個接口是我們定義的一個SPI接口,但是使用方式可以有兩種(靜態(tài) + 動態(tài)確認) -
@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);
}
4. SpiConf 注解
這個主鍵主要是用在實現類上(或實現類的方法上),里面存儲一些選擇條件,通常是和
Selector搭配使用
定義如下
定義了三個字段:
- name 唯一標識,用于
DefaultSelector; - params 參數條件, 用于
ParamsSelector; - order : 優(yōu)先級, 主要是為了解決多個實現類都滿足選擇條件時, 應該選擇哪一個 (談到這里就有個想法, 通過一個參數,來選擇是否讓滿足條件的全部返回)
@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 {};
/**
* 排序, 越小優(yōu)先級越高
*
* @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);
}
/**
* 實際的優(yōu)先級取 方法 和類上的最高優(yōu)先級, 實際為1;
* `ParamsSelector`選擇器時, 執(zhí)行該方法的條件等同于 `{"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注解,采取下面的規(guī)則- 方法注解聲明name時,兩個會同時生效,即想調用上面的echo方法, 通過傳入
ConsoleCode(類注解不顯示賦值時,采用類名代替) 和consoleEcho等價 - 方法注解未聲明name時,只能通過類注解上定義的name(or默認的類名)來選擇
- order,取最高優(yōu)先級,如上面的
write方法的優(yōu)先級是 1; 當未顯示定義order時,以定義的為準 - params: 取并集,即要求類上 + 方法上的條件都滿足
- 方法注解聲明name時,兩個會同時生效,即想調用上面的echo方法, 通過傳入
SPI加載器
spi加載器的主要業(yè)務邏輯集中在
SpiLoader類中,包含通過spi接口,獲取所有的實現類; 獲取spi接口對應的選擇器 (包括類對應的選擇器, 方法對應的選擇器); 返回Spi接口實現類(靜態(tài)確認的實現類,自適應的代理類)
從上面的簡述,基本上可以看出這個類劃分為三個功能點, 下面將逐一說明,本篇博文主要集中在邏輯的設計層,至于優(yōu)化(如懶加載,緩存優(yōu)化等) 放置下一篇博文單獨敘述
1. 加載spi實現類
這一塊比較簡單,我們直接利用了jdk的
ServiceLoader來根據接口,獲取所有的實現類;因此我們的spi實現,需要滿足jdk定義的這一套規(guī)范
具體的代碼業(yè)務邏輯非常簡單,大致流程如下
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的規(guī)范,我們其實完全可以自己定義標準的,只要能將這個接口的所有實現類找到, 怎么實現都可以由你定義
如使用spring框架后,可以考慮通過 applicationContext.getBeansOfAnnotaion(xxx ) 來獲取所有的特定注解的bean,這樣就可以不需要自己新建一個文件,來存儲spi接口和其實現類的映射關系了
構建spi實現的關系表
上面獲取了spi實現類,顯然我們的目標并不局限于簡單的獲取實現類,在獲取實現類之后,還需要解析其中的 @SpiConf 注解信息,用于表示要選擇這個實現,必須滿足什么樣的條件
SpiImplWrapper : spi實現類,以及定義的各種條件的封裝類
注解的解析過程流程如下:
- name: 注解定義時,采用定義的值; 否則采用簡單類名 (因此一個系統中不允許兩個實現類同名的情況)
- order: 優(yōu)先級, 注解定義時,采用定義的值;未定義時采用默認;
- 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. 初始化選擇器
我們的選擇器會區(qū)分為兩類,一個是類上定義的選擇器, 一個是方法上定義的選擇器; 在自適應的使用方式中,方法上定義的優(yōu)先級 > 類上定義
簡單來講,初始化選擇器,就是掃一遍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) {
// 優(yōu)先從選擇器緩存中獲取類型對應的選擇器
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)具體的實例化選擇器的方法從實現來看,優(yōu)先從選擇器緩存中獲取選擇器對象,這樣的目的是保證一個spi接口,每種類型的選擇器只有一個實例;因此在自定義選擇器中,你完全可以做一些選擇判斷的緩存邏輯,如
ParamsSelector中的spi實現類的有序緩存列表 -
currentSelector,currentMethodSelector,selectorInstanceCacheMapcurrentSelector: 對應的是類選擇器,每個SPI接口必然會有一個,作為打底的選擇器 currentMethodSelector: 方法選擇器映射關系表,key為方法名,value為該方法對應的選擇器; 所以spi接口中,不支持重載 selectorInstanceCacheMap: spi接口所有定義的選擇器映射關系表,key為選擇器類型,value是實例;用于保障每個spi接口中選擇器只會有一個實例
3. 獲取實現類
對使用者而言,最關注的就是這個接口,這里會返回我們需要的實現類(or代理);內部的邏輯也比較清楚,首先確定選擇器,然后通過選擇器便利所有的實現類,把滿足條件的返回即可
從上面的描述可以看到,主要分為兩步
- 獲取選擇器
- 根據選擇器,遍歷所有的實現類,找出匹配的返回
獲取選擇器
初始化選擇器之后,我們會有 currentSelector , currentMethodSelector 兩個緩存
- 靜態(tài)確定spi實現時,直接用
currentSelector即可 (spi接口中所有方法都公用類定義選擇器) - 動態(tài)適配時, 根據方法名在
currentMethodSelector中獲取選擇器,如果沒有,則表示該方法沒有@SpiAdaptive注解,直接使用類的選擇器currentMethodSelector即可
// 動態(tài)適配時,獲取方法對應對應的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;
}
選擇實現類
這個的主要邏輯就是遍歷所有的實現類,判斷是否滿足選擇器的條件,將第一個找到的返回即可,所有的業(yè)務邏輯都在 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();
}
}
流程說明
上面主要就各個點單獨的進行了說明,看起來可能比較分散,看完之后可能沒有一個清晰的流程,這里就整個實現的流程順一遍,主要從使用者的角度出發(fā),當定義了一個SPI接口后,到獲取spi實現的過程中,上面的這些步驟是怎樣串在一起的
流程圖
先拿簡單的靜態(tài)獲取SPI實現流程說明(動態(tài)的其實差不多,具體的差異下一篇說明),先看下這種用法的使用姿勢
@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);
這行代碼觸發(fā)的action 主要是初始化所有的選擇器, 如下圖
- 首先從緩存中查
- 是否已經初始化過了有則直接返回;
- 緩存中沒有,則進入new一個新的對象出來
- 解析類上注解
@Spi,初始化currentSelector - 解析所有方法的注解
@SpiAdaptive, 初始化currentMethodSelector
- 解析類上注解
- 塞入緩存,并返回

IPrint print = spiLoader.getService("ConsolePrint");
根據name獲取實現類,具體流程如下
- 判斷是否加載過所有實現類
spiImplClassCacheMap - 沒有加載,則重新加載所有的實現類
- 通過jdk的
ServiceLoader.load()方法獲取所有的實現類 - 遍歷實現類,根據
@SpiConf注解初始化參數,封裝SpiImplWrapper對象 - 保存封裝的
SpiImplWrapper對象到緩存
- 通過jdk的
- 執(zhí)行
currentSelector.select()方法,獲取匹配的實現類

其他
博客系列鏈接:
-SPI框架實現之旅一:背景介紹
-SPI框架實現之旅二:整體設計
-SPI框架實現之旅三:實現說明
-SPI框架實現之旅四:使用測試
源碼地址:
https://git.oschina.net/liuyueyi/quicksilver/tree/master/silver-spi