瞭解一下Java SPI的原理
1 爲何寫這篇文章?
近期,本人在學習dubbo相關的知識,可是在dubbo官網中有提到Java的 SPI,這個名詞以前未接觸過,因此就去看了看,感受仍是有不少地方有使用的,好比jdbc、log相關的技術上均有使用,仍是頗有用處的,就在這裏總結一下本身的學習內容!(本文有參考相關資料:好比dubbo官網、相關blog等)java
2 SPI是什麼?
Java SPI(Service Provider Interface)是JDK內置的一種動態加載擴展點的實現。在ClassPath的META-INF/services目錄下放置一個與接口同名的文本文件,文件的內容爲接口的實現類,多個實現類用換行符分隔。JDK中使用java.util.ServiceLoader來加載具體的實現。mysql
Java SPI 其實是「基於接口的編程+策略模式+配置文件」組合實現的動態加載機制。sql
3 自定義一個SPI
3.1 建立工程
建立dubbo-spi的工程,這裏展現一下完整的spi示例程序結構:數據庫
3.2 建立接口
在包top.flygrk.ishare.spi.service下建立接口: SPIService編程
package top.flygrk.ishare.spi.service; /** * @Package top.flygrk.ishare.spi.service * @Version V1.0 * @Description: SPIService 接口 */ public interface SPIService { /** * 接口方法: say() */ String say(); }
3.3 建立實現類: ASPIServiceImpl和BSPIServiceImpl
在包top.flygrk.ishare.spi.service.impl下建立ASPIServiceImpl和BSPIServiceImpl類,均實現SPIservice接口:緩存
- ASPIServiceImpl
package top.flygrk.ishare.spi.service.impl; import top.flygrk.ishare.spi.service.SPIService; /** * @Package top.flygrk.ishare.spi.service.impl * @Version V1.0 * @Description: SPIService 實現類 ASPIServiceImpl */ public class ASPIServiceImpl implements SPIService { @Override public String say() { return "ASPIServiceImpl"; } }
- BSPIServiceImpl
package top.flygrk.ishare.spi.service.impl; import top.flygrk.ishare.spi.service.SPIService; /** * @Package top.flygrk.ishare.spi.service.impl * @Version V1.0 * @Description: SPIService 實現類 BSPIServiceImpl */ public class BSPIServiceImpl implements SPIService { @Override public String say() { return "BSPIServiceImpl"; } }
3.4 建立文件top.flygrk.ishare.spi.service.SPIService
在resource目錄下,建立META-INF/services目錄,並在該目錄下建立top.flygrk.ishare.spi.service.SPIService文件(該文件名爲接口的全路徑,需保持一致),並在該文件中配置兩個實現類的全路徑:微信
top.flygrk.ishare.spi.service.impl.ASPIServiceImpl top.flygrk.ishare.spi.service.impl.BSPIServiceImpl
3.5 建立測試類TestSPIService
在包top.flygrk.ishare.demo下建立TestSPIService類,用於測試該SPI服務框架
package top.flygrk.ishare.demo; import top.flygrk.ishare.spi.service.SPIService; import java.util.Iterator; import java.util.ServiceLoader; /** * @Package top.flygrk.ishare.demo * @Version V1.0 * @Description: 測試 SPIService */ public class TestSPIService { public static void main(String[] args) { // ServiceLoader實現了Iterable接口,能夠遍歷出全部的服務實現者 ServiceLoader<SPIService> serviceLoaders = ServiceLoader.load(SPIService.class); /* * 方法1: 迭代器 */ Iterator<SPIService> spiServiceIterator = serviceLoaders.iterator(); while (spiServiceIterator != null && spiServiceIterator.hasNext()) { SPIService spiService = spiServiceIterator.next(); System.out.println(spiService.getClass().getName() + " : " + spiService.say()); } /* * 迭代方法2: foreach */ // for (SPIService spiService : serviceLoaders) { // System.out.println(spiService.getClass().getName() + " : " + spiService.say()); // } } }
3.6 測試類運行結果
top.flygrk.ishare.spi.service.impl.ASPIServiceImpl : ASPIServiceImpl top.flygrk.ishare.spi.service.impl.BSPIServiceImpl : BSPIServiceImpl
4 SPI原理分析
在咱們閱讀源碼前,咱們先提出如下幾個問題,而後咱們再去帶着問題去源碼中找答案:ide
-
- META-INF/services目錄下的文件有什麼用?爲何要用接口的全路徑命名?是否能夠更改接口名稱?裏面的內容爲何要用實現類的全路徑?
- 2) ServiceLoader 是如何獲取到SPIService的所有實現的?
- 3) 若是咱們只想取ASPIServiceImpl,並不想去操做BSPIServiceImpl,如何去操做?
4.1 ServiceLoader結構
咱們先看一下ServiceLoader類的結構:函數
進入ServiceLoader類的源碼,咱們能夠看到如下定義的一些常量:
各位確定注意到了一點: private static final String PREFIX = "META-INF/services/";
, 這個PREFIX後面的路徑不正是咱們在上述示例中建立和接口保持一致的文件的目錄嗎?還有services、loader、acc、lookupIterator和providers表達的意思在源碼上方的註釋中也進行了描述,下面我將各個屬性的釋義標註一下:
// 配置文件的目錄 private static final String PREFIX = "META-INF/services/"; // 要加載服務的類或者接口 // The class or interface representing the service being loaded private final Class<S> service; // 服務加載器 // The class loader used to locate, load, and instantiate providers private final ClassLoader loader; // 訪問控制上下文 // The access control context taken when the ServiceLoader is created private final AccessControlContext acc; // 服務實例的緩存 // Cached providers, in instantiation order private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // 懶加載的迭代器 // The current lazy-lookup iterator private LazyIterator lookupIterator;
4.2 ServiceLoader的加載過程
看完了上面ServiceLoader的結構,下面咱們再來看看ServiceLoader是如何一步步加載的。咱們在TestSPIService類上的main方法第一行打上斷點:
而後使用debug的方式調試,進入ServiceLoader的源碼,會依次進入如下幾個函數:
通過這些步驟以後,serviceLoader內部包含有一個Iterator迭代器,下面咱們來仔細看一下這個迭代器的做用!
4.3 迭代器lookupIterator的操做
在上述4.2步驟加載完成以後,serviceLoader內的lookupIterator的內容以下:
而後使用iterator()方法獲取Iterator迭代器時,執行以下的程序:
在通過上述過程以後,咱們拿到了Iterator迭代器,這時咱們看下spiServiceIterator的內容:
是否是很奇怪,仍是隻有SPIService,不要忘記了,他內部的迭代器但是懶加載的!咱們繼續跟進代碼,進入到hasNext()方法。
從上面能夠知道,acc一直爲null的,因此這時候,他進入了hasNextService()方法:
重頭戲來了,咱們能夠看到其中的 PREFIX, 這個內容就是咱們配置的文件。再仔細的跟進代碼,咱們會進入到parse()方法,該方法用於按照行讀取出文件中的內容,並保存到Iterator<String>
中。
故而,再經過 nextName = pending.next();
執行後,獲取到top.flygrk.ishare.spi.service.impl.ASPIServiceImpl
,繼而進行後續的next()方法操做。
而後進入到nextService()方法:
再nextService()方法裏,使用了反射的技術,根據前面從文件中讀取到的實現類全路徑top.flygrk.ishare.spi.service.impl.ASPIServiceImpl
獲取到該實現類的對象!走到這裏,也就基本上了解了SPI,可是咱們能只獲取ASPIServiceImpl,而不去獲取BSPIServiceImpl嗎?對不起,這裏不容許這樣,只能經過迭代器遍歷出全部的內容!除非人爲干預(外層循環比對完成以後退出循環)。接下來的步驟就和前面幾乎一致了,這裏再也不細述~
5 SPI 優缺點
咱們評價一門思想每每須要從其優缺點的方向進行考慮。SPI一樣也是有必定的優缺點存在的,下面咱們來仔細的看下它有哪些優缺點:
5.1 優勢
- 解耦:最大的優勢也就是解耦了,經過SPI可使第三方服務模塊的邏輯與業務代碼相分離,而不耦合在一塊兒。應用程序能夠根據實際業務進行擴展。
5.2 缺點
參考dubbo官方文檔
- 須要遍歷全部的實現,並實例化,而後咱們在循環中才能找到咱們須要的實現。
- 配置文件中只是簡單的列出了全部的擴展實現,而沒有給他們命名。致使在程序中很難去準確的引用它們。
- 擴展若是依賴其餘的擴展,作不到自動注入和裝配
- 不提供相似於Spring的IOC和AOP功能
- 擴展很難和其餘的框架集成,好比擴展裏面依賴了一個Spring bean,原生的Java SPI不支持
6 SPI案例分析
在咱們經常使用的框架中,有不少都是有使用SPI的方式,其中包括JDBC加載不一樣類型數據庫的驅動、SLF4J加載不一樣提供商的日誌實現類、Spring 框架、Dubbo框架。
這裏須要注意,dubbo框架的SPI是對原生的Java SPI 進行了擴展的。關於dubbo的SPI咱們將在後面詳細講解。如今,咱們來以JDBC加載的方式來簡單的看看其SPI的方式。
咱們先找到mysql的包,其結構以下:
在META-INF/services 目錄下,存在 文件 java.sql.Driver,其內容爲:
經過這個路徑,咱們也能夠找到 com.mysql.jdbc.Driver類,它實現了java.sql.Driver接口:
諸如Oracle,一樣也有此機制,這裏就再也不細述了,請自行驗證查看~