1.SPI 是什么?
SPI 的全稱是 Service Provider Interface, 即提供服務(wù)接口;是一種服務(wù)發(fā)現(xiàn)機(jī)制,SPI 的本質(zhì)是將接口實(shí)現(xiàn)類的全限定名配置在文件中,并由服務(wù)加載器讀取配置文件,加載實(shí)現(xiàn)類。這樣可以在運(yùn)行時(shí),動(dòng)態(tài)為接口替換實(shí)現(xiàn)類。正因此特性,我們可以很容易的通過(guò) SPI 機(jī)制為我們的程序提供拓展功能。 如下圖:
系統(tǒng)設(shè)計(jì)的各個(gè)抽象,往往有很多不同的實(shí)現(xiàn)方案,在面對(duì)象設(shè)計(jì)里,一般推薦模塊之間基于接口編程,模塊之間不對(duì)實(shí)現(xiàn)硬編碼,一旦代碼涉及具體的實(shí)現(xiàn)類,就違反了可插拔的原則。Java SPI 就是提供這樣的一個(gè)機(jī)制,為某一個(gè)接口尋找服務(wù)的實(shí)現(xiàn),有點(diǎn)類似 IOC 的思想,把裝配的控制權(quán)移到程序之外,在模塊化涉及里面這個(gè)各尤為重要。與其說(shuō) SPI 是 java 提供的一種服務(wù)發(fā)現(xiàn)機(jī)制,倒不如說(shuō)是一種解耦思想。
2. 使用場(chǎng)景
數(shù)據(jù)庫(kù)驅(qū)動(dòng)加載接口實(shí)現(xiàn)類的加載;如:JDBC 加載 Mysql,Oracle...
日志門面接口實(shí)現(xiàn)類加載,如:SLF4J 對(duì) log4j、logback 的支持
Spring 中大量使用了 SPI,特別是 spring-boot 中自動(dòng)化配置的實(shí)現(xiàn)
Dubbo 也是大量使用 SPI 的方式實(shí)現(xiàn)框架的擴(kuò)展,它是對(duì)原生的 SPI 做了封裝,允許用戶擴(kuò)展實(shí)現(xiàn) Filter 接口。
3. 使用介紹
要使用 Java SPI,需要遵循以下約定:
當(dāng)服務(wù)提供者提供了接口的一種具體實(shí)現(xiàn)后,需要在 JAR 包的 META-INF/services 目錄下創(chuàng)建一個(gè)以 “接口全限制定名” 為命名的文件,內(nèi)容為實(shí)現(xiàn)類的全限定名;
接口實(shí)現(xiàn)類所在的 JAR 放在主程序的 classpath 下,也就是引入依賴。
主程序通過(guò) java.util.ServiceLoder 動(dòng)態(tài)加載實(shí)現(xiàn)模塊,它會(huì)通過(guò)掃描 META-INF/services 目錄下的文件找到實(shí)現(xiàn)類的全限定名,把類加載值 JVM, 并實(shí)例化它;
SPI 的實(shí)現(xiàn)類必須攜帶一個(gè)不帶參數(shù)的構(gòu)造方法。
示例:
spi-interface 模塊定義
定義一組接口:public interface MyDriver
spi-jd-driver
spi-ali-driver
實(shí)現(xiàn)為:public class JdDriver implements MyDriver public class AliDriver implements MyDriver
在 src/main/resources/ 下建立 /META-INF/services 目錄, 新增一個(gè)以接口命名的文件 (org.MyDriver 文件)
內(nèi)容是要應(yīng)用的實(shí)現(xiàn)類分別 com.jd.JdDriver 和 com.ali.AliDriver
spi-core
一般都是平臺(tái)提供的核心包,包含加載使用實(shí)現(xiàn)類的策略等等,我們這邊就簡(jiǎn)單實(shí)現(xiàn)一下邏輯:a. 沒(méi)有找到具體實(shí)現(xiàn)拋出異常 b. 如果發(fā)現(xiàn)多個(gè)實(shí)現(xiàn),分別打印
public void invoker(){ ServiceLoaderserviceLoader = ServiceLoader.load(MyDriver.class); Iterator drivers = serviceLoader.iterator(); boolean isNotFound = true; while (drivers.hasNext()){ isNotFound = false; drivers.next().load(); } if(isNotFound){ throw new RuntimeException("一個(gè)驅(qū)動(dòng)實(shí)現(xiàn)類都不存在"); } }
spi-test
public class App { public static void main( String[] args ) { DriverFactory factory = new DriverFactory(); factory.invoker(); } }
1. 引入 spi-core 包,執(zhí)行結(jié)果
2. 引入 spi-core,spi-jd-driver 包
3. 引入 spi-core,spi-jd-driver,spi-ali-driver
4. 原理解析
看看我們剛剛是怎么拿到具體的實(shí)現(xiàn)類的? 就兩行代碼:
ServiceLoaderserviceLoader = ServiceLoader.load(MyDriver.class); Iterator drivers = serviceLoader.iterator();
所以,首先我們看 ServiceLoader 類:
public final class ServiceLoaderimplements Iterable{ //配置文件的路徑 private static final String PREFIX = "META-INF/services/"; // 代表被加載的類或者接口 private final Classservice; // 用于定位,加載和實(shí)例化providers的類加載器 private final ClassLoader loader; // 創(chuàng)建ServiceLoader時(shí)采用的訪問(wèn)控制上下文 private final AccessControlContext acc; // 緩存providers,按實(shí)例化的順序排列 private LinkedHashMapproviders = new LinkedHashMap<>(); // 懶查找迭代器,真正加載服務(wù)的類 private LazyIterator lookupIterator; //服務(wù)提供者查找的迭代器 private class LazyIterator implements Iterator { ..... private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { //全限定名:com.xxxx.xxx String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; } private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class> c = null; try { //通過(guò)反射獲取 c = Class.forName(cn, false, loader); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } } ........
大概的流程就是下面這張圖:
應(yīng)用程序調(diào)用 ServiceLoader.load 方法
應(yīng)用程序通過(guò)迭代器獲取對(duì)象實(shí)例,會(huì)先判斷 providers 對(duì)象中是否已經(jīng)有緩存的示例對(duì)象,如果存在直接返回
如果沒(méi)有存在,執(zhí)行類轉(zhuǎn)載讀取 META-INF/services 下的配置文件,獲取所有能被實(shí)例化的類的名稱,可以跨越 JAR 獲取配置文件通過(guò)反射方法 Class.forName () 加載對(duì)象并用 Instance () 方法示例化類將實(shí)例化類緩存至 providers 對(duì)象中,同步返回。
5. 總結(jié)
優(yōu)點(diǎn):解耦
SPI 的使用,使得第三方服務(wù)模塊的裝配控制邏輯與調(diào)用者的業(yè)務(wù)代碼分離,不會(huì)耦合在一起,應(yīng)用程序可以根據(jù)實(shí)際業(yè)務(wù)情況來(lái)啟用框架擴(kuò)展和替換框架組件。
SPI 的使用,使得無(wú)須通過(guò)下面幾種方式獲取實(shí)現(xiàn)類
代碼硬編碼 import 導(dǎo)入
指定類全限定名反射獲取,例如 JDBC4.0 之前;Class.forName("com.mysql.jdbc.Driver")
缺點(diǎn):
雖然 ServiceLoader 也算是使用的延遲加載,但是基本只能通過(guò)遍歷全部獲取,也就是接口的實(shí)現(xiàn)類全部加載并實(shí)例化一遍。如果你并不想用某些實(shí)現(xiàn)類,它也被加載并實(shí)例化了,這就造成了浪費(fèi)。獲取某個(gè)實(shí)現(xiàn)類的方式不夠靈活,只能通過(guò) Iterator 形式獲取,不能根據(jù)某個(gè)參數(shù)來(lái)獲取對(duì)應(yīng)的實(shí)現(xiàn)類。
6. 對(duì)比
審核編輯:劉清
-
JAVA
+關(guān)注
關(guān)注
19文章
2973瀏覽量
104907 -
SPI
+關(guān)注
關(guān)注
17文章
1717瀏覽量
91842 -
JDBC
+關(guān)注
關(guān)注
0文章
25瀏覽量
13414
原文標(biāo)題:可插拔組件設(shè)計(jì)機(jī)制 —SPI
文章出處:【微信號(hào):OSC開(kāi)源社區(qū),微信公眾號(hào):OSC開(kāi)源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論