了解一下Java SPI的原理


了解一下Java SPI的原理

1 為什么寫這篇文章?

近期,本人在學習dubbo相關的知識,但是在dubbo官網中有提到Java的 SPI,這個名詞之前未接觸過,所以就去看了看,感覺還是有很多地方有使用的,比如jdbc、log相關的技術上均有使用,還是很有用處的,就在這里總結一下自己的學習內容!(本文有參考相關資料:比如dubbo官網、相關blog等)

2 SPI是什么?

Java SPI(Service Provider Interface)是JDK內置的一種動態加載擴展點的實現。在ClassPath的META-INF/services目錄下放置一個與接口同名的文本文件,文件的內容為接口的實現類,多個實現類用換行符分隔。JDK中使用java.util.ServiceLoader來加載具體的實現。

Java SPI 實際上是“基於接口的編程+策略模式+配置文件”組合實現的動態加載機制。

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原理分析

在我們閱讀源碼前,我們先提出以下幾個問題,然后我們再去帶着問題去源碼中找答案:

    1. 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迭代器時,執行如下的程序:

file

在經過上述過程之后,我們拿到了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,同樣也有此機制,這里就不再細述了,請自行驗證查看~


Blog:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM