【Java】深入理解Java中的spi機制

来源:https://www.cnblogs.com/xcmelody/archive/2019/05/13/10859704.html
-Advertisement-
Play Games

深入理解Java中的spi機制 全名為 是JDK內置的一種服務提供發現機制,是Java提供的一套用來被第三方實現或者擴展的API,它可以用來啟用框架擴展和替換組件。 = 基於介面的編程+策略模式+配置文件 的動態載入機制 Java SPI的具體約定如下: 當服務的提供者,提供了服務介面的一種實現之後 ...


深入理解Java中的spi機制

SPI全名為Service Provider Interface是JDK內置的一種服務提供發現機制,是Java提供的一套用來被第三方實現或者擴展的API,它可以用來啟用框架擴展和替換組件。

JAVA SPI = 基於介面的編程+策略模式+配置文件 的動態載入機制

Java SPI的具體約定如下:

當服務的提供者,提供了服務介面的一種實現之後,在jar包的META-INF/services/目錄里同時創建一個以服務介面命名的文件。該文件里就是實現該服務介面的具體實現類。

而當外部程式裝配這個模塊的時候,就能通過該jarMETA-INF/services/里的配置文件找到具體的實現類名,並裝載實例化,完成模塊的註入。

根據SPI的規範我們的服務實現類必須有一個無參構造方法

為什麼一定要在classes中的META-INF/services下呢?

JDK提供服務實現查找的一個工具類:java.util.ServiceLoader

在這個類裡面已經寫死

// 預設會去這裡尋找相關信息
private static final String PREFIX = "META-INF/services/";

常見的使用場景:

  • JDBC載入不同類型的資料庫驅動
  • 日誌門面介面實現類載入,SLF4J載入不同提供商的日誌實現類
  • Spring中大量使用了SPI,
    • servlet3.0規範
    • ServletContainerInitializer的實現
    • 自動類型轉換Type Conversion SPI(Converter SPI、Formatter SPI)
  • Dubbo裡面有很多個組件,每個組件在框架中都是以介面的形成抽象出來!具體的實現又分很多種,在程式執行時根據用戶的配置來按需取介面的實現

簡單的spi實例

整體包結構如下

└─main
    ├─java
    │  └─com
    │      └─xinchen
    │          └─spi
    │              └─App.java
    │              └─IService.java
    │              └─ServiceImplA.java
    │              └─ServiceImplB.java            
    └─resources
        └─META-INF
            └─services
                └─com.xinchen.spi.IService

SPI介面

public interface IService {
    void say(String word);
}

具體實現類

public class ServiceImplA implements IService {
    @Override
    public void say(String word) {
        System.out.println(this.getClass().toString() + " say: " + word);
    }
}

public class ServiceImplB implements IService {
    @Override
    public void say(String word) {
        System.out.println(this.getClass().toString() + " say: " + word);
    }
}

/resource/META-INF/services/com.xinchen.spi.IService

com.xinchen.spi.ServiceImplA
com.xinchen.spi.ServiceImplB

Client類

public class App {
    static ServiceLoader<IService> services = ServiceLoader.load(IService.class);

    public static void main(String[] args) {
        for (IService service:services){
            service.say("Hello World!");
        }
    }
}

//    結果:
//    class com.xinchen.spi.ServiceImplA say: Hello World!
//    class com.xinchen.spi.ServiceImplB say: Hello World!

源碼解析

java.util.ServiceLoader中的Fied區域

    // 載入具體實現類信息的首碼
    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;

    // 創建ServiceLoader時採用的訪問控制上下文
    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;

    // 用於緩存已經載入的介面實現類,其中key為實現類的完整類名
    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 用於延遲載入介面的實現類
    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;

ServiceLoader.load(IService.class)進入源碼中

    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 獲取當前線程上下文的類載入器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

ServiceLoader.load(service, cl)

    public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
        // 返回ServiceLoader的實例
        return new ServiceLoader<>(service, loader);
    }

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

    public void reload() {
        // 清空已經緩存的載入的介面實現類
        providers.clear();
        // 創建新的延遲載入迭代器
        lookupIterator = new LazyIterator(service, loader);
    }   
    
    private LazyIterator(Class<S> service, ClassLoader loader) {
        // 指定this類中的 需要載入的介面service和類載入器loader
        this.service = service;
        this.loader = loader;
    }         

當我們通過迭代器獲取對象實例的時候,首先在成員變數providers中查找是否有緩存的實例對象

如果存在則直接返回,否則則調用lookupIterator延遲載入迭代器進行載入

迭代器判斷的代碼如下

public Iterator<S> iterator() {
        // 返回迭代器
        return new Iterator<S>() {
            // 查詢緩存中是否存在實例對象
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();

            public boolean hasNext() {
                // 如果緩存中已經存在返回true
                if (knownProviders.hasNext())
                    return true;
                // 如果不存在則使用延遲載入迭代器進行判斷是否存在
                return lookupIterator.hasNext();
            }

            public S next() {
                // 如果緩存中存在則直接返回
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                // 調用延遲載入迭代器進行返回
                return lookupIterator.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }

LazyIterator的類載入

        // 判斷是否擁有下一個實例
        private boolean hasNextService() {
            // 如果擁有直接返回true
            if (nextName != null) {
                return true;
            }

            // 具體實現類的全名 ,Enumeration<URL> config
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                // 轉換config中的元素,或者具體實現類的真實包結構
                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 {
                // 載入類對象
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                // 通過c.newInstance()實例化
                S p = service.cast(c.newInstance());
                // 將實現類加入緩存
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

總結

優點

使用Java SPI機制的優勢是實現解耦,使得第三方服務模塊的裝配控制的邏輯與調用者的業務代碼分離,而不是耦合在一起。應用程式可以根據實際業務情況啟用框架擴展或替換框架組件。

缺點

  • 多個併發多線程使用ServiceLoader類的實例是不安全的

  • 雖然ServiceLoader也算是使用的延遲載入,但是基本只能通過遍歷全部獲取,也就是介面的實現類全部載入並實例化一遍。

參考

The Java™ Tutorials

聊聊Dubbo(五):核心源碼-SPI擴展

深入理解Java SPI機制


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 第1題:閱讀下麵的代碼,默讀出A0,A1至An的最終值。 默讀代碼類的題目,相對來說是比較簡單的。重點去研究列表解析,之後你就可以輕鬆的回答這些問題嘍~ 第2題:如何提高python的運行效率? 1. 數據結構一定要選對 能用字典就不用列表:字典在索引查找和排序方面遠遠高於列表。 2. 多用pyth ...
  • 內容簡介 本書面向所有對機器學習與數據挖掘的實踐及競賽感興趣的讀者,從零開始,以Python編程語言為基礎,在不涉及大量數學模型與複雜編程知識的前提下,逐步帶領讀者熟悉並且掌握當下最流行的機器學習、數據挖掘與自然語言處理工具,如Scikitlearn、NLTK、Pandas、gensim、XGBo ...
  • Java9的日誌級別: ALL 最低級別,系統會輸出所有的日誌信息,會生成大量的·、冗餘的日誌 TRACE 輸出系統的各種跟蹤信息,會生成大量的·、冗餘的日誌 DEBUG 輸出調試信息,會生成較多的日誌 INFO 輸出系統提示用戶的信息 WARNING 輸出警告信息 ERROR 輸出錯誤信息 OFF ...
  • 筆記 7天學完Java基礎之0/7 1.常用命令提示符(cmd) 啟動: Win+R,輸入cmd​ :twisted_rightwards_arrows: cmd 切換盤符 盤符名稱+:(冒號為英文輸入法下的冒號) 進入指定文件夾 cd +文件夾名稱 註意只能夠進入文件夾,不能夠進入文件哦 查看當前 ...
  • 國際化是指應用程式運行時,可根據客戶端OS的國家/地區、語言的不同而顯示不同的界面,比如客戶端OS的語言環境為大陸的簡體中文,程式就顯示為簡體中文,客戶端OS的語言環境為美國——英語,程式就顯示美式英語。 OS的語言環境可在控制面板中手動設置。 國際化的英文單詞是Internationalizati ...
  • Python進階之網路編程,內容包括 網路通信,socket,udp,tcp。其中,網路通信 包括 使用網路的目的,ip地址,埠;socket 包括 socket的概念,創建socket;udp 包括 udp發送數據,udp接收數據,udp接發數據總結,埠綁定的問題,單工半雙工全雙工;tcp 包... ...
  • "概要" "開發" "web 框架" "資料庫" "認證" "日誌" "配置" "靜態文件服務" "上傳/下載" "發佈" "docker 打包" "部署中遇到的問題" "時區問題" 概要 輕量的基於 golang 的 web 開發實踐. golang 上手簡單, 第三方庫豐富, 對於業務沒那麼複雜 ...
  • 1、正則表達式語法: ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...