Java SPI用法案例詳解
1.什么是SPI
SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現(xiàn)或者擴展的接口,它可以用來啟用框架擴展和替換組件。 SPI的作用就是為這些被擴展的API尋找服務實現(xiàn)。
2.SPI和API的使用場景
API (Application Programming Interface)在大多數(shù)情況下,都是實現(xiàn)方制定接口并完成對接口的實現(xiàn),調(diào)用方僅僅依賴接口調(diào)用,且無權選擇不同實現(xiàn)。 從使用人員上來說,API 直接被應用開發(fā)人員使用。
SPI (Service Provider Interface)是調(diào)用方來制定接口規(guī)范,提供給外部來實現(xiàn),調(diào)用方在調(diào)用時則選擇自己需要的外部實現(xiàn)。 從使用人員上來說,SPI 被框架擴展人員使用。
3.SPI的簡單實現(xiàn)
下面我們來簡單實現(xiàn)一個jdk的SPI的簡單實現(xiàn)。
首先第一步,定義一組接口:
public interface UploadCDN {
void upload(String url);
}
這個接口分別有兩個實現(xiàn):
public class QiyiCDN implements UploadCDN { //上傳愛奇藝cdn
@Override
public void upload(String url) {
System.out.println("upload to qiyi cdn");
}
}
public class ChinaNetCDN implements UploadCDN {//上傳網(wǎng)宿cdn
@Override
public void upload(String url) {
System.out.println("upload to chinaNet cdn");
}
}
然后需要在resources目錄下新建META-INF/services目錄,并且在這個目錄下新建一個與上述接口的全限定名一致的文件,在這個文件中寫入接口的實現(xiàn)類的全限定名:


這時,通過serviceLoader加載實現(xiàn)類并調(diào)用:
public static void main(String[] args) {
ServiceLoader<UploadCDN> uploadCDN = ServiceLoader.load(UploadCDN.class);
for (UploadCDN u : uploadCDN) {
u.upload("filePath");
}
}
輸出如下:

這樣一個簡單的spi的demo就完成了??梢钥吹狡渲凶顬楹诵牡木褪峭ㄟ^ServiceLoader這個類來加載具體的實現(xiàn)類的。
4. SPI原理解析
通過上面簡單的demo,可以看到最關鍵的實現(xiàn)就是ServiceLoader這個類,可以看下這個類的源碼,如下:
public final class ServiceLoader<S> implements Iterable<S> {
//掃描目錄前綴
private static final String PREFIX = "META-INF/services/";
// 被加載的類或接口
private final Class<S> service;
// 用于定位、加載和實例化實現(xiàn)方實現(xiàn)的類的類加載器
private final ClassLoader loader;
// 上下文對象
private final AccessControlContext acc;
// 按照實例化的順序緩存已經(jīng)實例化的類
private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
// 懶查找迭代器
private java.util.ServiceLoader.LazyIterator lookupIterator;
// 私有內(nèi)部類,提供對所有的service的類的加載與實例化
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
String nextName = null;
//...
private boolean hasNextService() {
if (configs == null) {
try {
//獲取目錄下所有的類
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
//...
}
//....
}
}
private S nextService() {
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//反射加載類
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
}
try {
//實例化
S p = service.cast(c.newInstance());
//放進緩存
providers.put(cn, p);
return p;
} catch (Throwable x) {
//..
}
//..
}
}
}
上面的代碼只貼出了部分關鍵的實現(xiàn),有興趣的讀者可以自己去研究,下面貼出比較直觀的spi加載的主要流程供參考:

5.dubbo SPI
dubbo作為一個高度可擴展的rpc框架,也依賴于java的spi,并且dubbo對java原生的spi機制作出了一定的擴展,使得其功能更加強大。
首先,從上面的java spi的原理中可以了解到,java的spi機制有著如下的弊端:
- 只能遍歷所有的實現(xiàn),并全部實例化。
- 配置文件中只是簡單的列出了所有的擴展實現(xiàn),而沒有給他們命名。導致在程序中很難去準確的引用它們。
- 擴展如果依賴其他的擴展,做不到自動注入和裝配。
- 擴展很難和其他的框架集成,比如擴展里面依賴了一個Spring bean,原生的Java SPI不支持。
dubbo的spi有如下幾個概念:
(1)擴展點:一個接口。
(2)擴展:擴展(接口)的實現(xiàn)。
(3)擴展自適應實例:其實就是一個Extension的代理,它實現(xiàn)了擴展點接口。在調(diào)用擴展點的接口方法時,會根據(jù)實際的參數(shù)來決定要使用哪個擴展。dubbo會根據(jù)接口中的參數(shù),自動地決定選擇哪個實現(xiàn)。
(4)@SPI:該注解作用于擴展點的接口上,表明該接口是一個擴展點。
(5)@Adaptive:@Adaptive注解用在擴展接口的方法上。表示該方法是一個自適應方法。Dubbo在為擴展點生成自適應實例時,如果方法有@Adaptive注解,會為該方法生成對應的代碼。
dubbo的spi也會從某些固定的路徑下去加載配置文件,并且配置的格式與java原生的不一樣,類似于property文件的格式:

下面將基于dubbo去實現(xiàn)一個簡單的擴展實現(xiàn)。首先,要實現(xiàn)LoadBalance這個接口,當然這個接口是被注解標注的可以擴展的:
@SPI("random")
public interface LoadBalance {
@Adaptive({"loadbalance"})
<T> Invoker<T> select(List<Invoker<T>> var1, URL var2, Invocation var3) throws RpcException;
}
public class DemoLoadBalance implements LoadBalance {
@Override
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
System.out.println("my demo loadBalance is used, hahahahh");
return invokers.get(0);//選擇第一個
}
}
然后,需要在duboo SPI的掃描目錄下,添加配置文件,注意配置文件的名稱要和擴展點的接口名稱對應起來:

還需要在dubbo的spring配置中顯式的聲明,使用上面自己實現(xiàn)的負載均衡策略:
<dubbo:reference id="helloService" interface="com.dubbo.spi.demo.api.IHelloService" loadbalance="demo" />
然后,啟動dubbo,調(diào)用service,就可以發(fā)現(xiàn)確實是使用了自定義的負載策略:

至此,dubbo的spi的demo也完成了。
dubbo spi的原理和jdk的實現(xiàn)稍有不同,大概流程如下圖,具體的實現(xiàn)讀者可以自己了解下源碼。

6.總結(jié)
關于spi的詳解到此就結(jié)束了,總結(jié)下spi能帶來的好處:
- 不需要改動源碼就可以實現(xiàn)擴展,解耦。
- 實現(xiàn)擴展對原來的代碼幾乎沒有侵入性。
- 只需要添加配置就可以實現(xiàn)擴展,符合開閉原則。
到此這篇關于Java SPI用法案例詳解的文章就介紹到這了,更多相關Java SPI用法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
java發(fā)送http請求并獲取狀態(tài)碼的簡單實例
下面小編就為大家?guī)硪黄猨ava發(fā)送http請求并獲取狀態(tài)碼的簡單實例。小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-05-05
詳解JAVA生成將圖片存入數(shù)據(jù)庫的sql語句實現(xiàn)方法
這篇文章主要介紹了詳解JAVA生成將圖片存入數(shù)據(jù)庫的sql語句實現(xiàn)方法的相關資料,這里就是實現(xiàn)java生成圖片并存入數(shù)據(jù)庫的實例,需要的朋友可以參考下2017-08-08
Jenkins節(jié)點配置實現(xiàn)原理及過程解析
這篇文章主要介紹了Jenkins節(jié)點配置實現(xiàn)原理及過程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-09-09
Java Integer.valueOf()和Integer.parseInt()的區(qū)別說明
這篇文章主要介紹了Java Integer.valueOf()和Integer.parseInt()的區(qū)別說明,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08
關于scanner.nextInt()等next()和scanner.nextIine()連用注意事項
這篇文章主要介紹了關于scanner.nextInt()等next()和scanner.nextIine()連用注意事項,具有很好的參考價值,希望對大家有所幫助。2023-04-04
SpringBoot實現(xiàn)防止XSS攻擊的示例詳解
這篇文章主要為大家詳細介紹了SpringBoot如何實現(xiàn)防止XSS攻擊,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下2024-03-03
java 實現(xiàn)單鏈表逆轉(zhuǎn)詳解及實例代碼
這篇文章主要介紹了java 實現(xiàn)單鏈表逆轉(zhuǎn)實例代碼的相關資料,需要的朋友可以參考下2017-02-02

