Java編程技術之淺析SPI服務發現機制

来源:https://www.cnblogs.com/mazhilin/archive/2020/05/09/12854184.html
-Advertisement-
Play Games

SPI服務發現機制 SPI是Java JDK內部提供的一種服務發現機制。 SPI Service Provider Interface,服務提供介面,是Java JDK內置的一種服務發現機制 通過在ClassPath路徑下的META INF/services文件夾查找文件,自動載入文件里所定義的類 ...


SPI服務發現機制

SPI是Java JDK內部提供的一種服務發現機制。

  • SPI->Service Provider Interface,服務提供介面,是Java JDK內置的一種服務發現機制

  • 通過在ClassPath路徑下的META-INF/services文件夾查找文件,自動載入文件里所定義的類

[⚠️註意事項]:
面向對象的設計里,一般推薦模塊之間基於介面編程,模塊之間不對實現類進行編碼。如果涉及實現類就會違反可插拔的原則,針對於模塊裝配,Java SPI提供了為某個介面尋找服務的實現機制。

SPI規範

  • 使用約定:
    [1].編寫服務提供介面,可以是抽象介面和函數介面,JDK1.8之後推薦使用函數介面

[2].在jar包的META-INF/services/目錄里創建一個以服務介面命名的文件。其實就是實現該服務介面的具體實現類。

提供一個目錄:
META-INF/services/
放到ClassPath下麵

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

目錄下放置一個配置文件:
文件名是需要拓展的介面全限定名稱
文件內部為要實現的介面實現類
文件必須為UTF-8編碼

[4].尋找服務介面實現,不用在代碼中提供,而是利用JDK提供服務查找工具類:java.util.ServiceLoader類來載入使用:

ServiceLoader.load(xxx.class)
ServiceLoader<XXXInterface> loads = ServiceLoader.load(xxx.class)

SPI源碼分析

[1].ServiceLoader源碼:
YMEMlV.png

package java.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.AccessController;
import java.security.AccessControlContext;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

public final class ServiceLoader<S> implements Iterable<S>
{
    //[1].初始化定義全局配置文件路徑Path
    private static final String PREFIX = "META-INF/services/";
    //[2].初始化定義載入的服務類或介面
    private final Class<S> service;
    //[3].初始化定義類載入器
    private final ClassLoader loader;
    //[4].初始化定義訪問控制上下文
    private final AccessControlContext acc;
    //[5].初始化定義載入服務類的緩存集合 
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    //[6].初始化定義私有內部LazyIterator類,真正載入服務類的實現類
    private LazyIterator lookupIterator;
    
    //私有化有參構造-> ServiceLoader(Class<S> svc, ClassLoader cl)
    private ServiceLoader(Class<S> svc, ClassLoader cl) {   //[1].實例化服務介面->Class<S>
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    //[2].實例化類載入器->ClassLoader
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    //[3].實例化訪問控制上下文->AccessControlContext
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    //[4].回調函數->reload
    reload();
    }
    
    public void reload() {
    //[1].清空緩存實例集合
    providers.clear();
    //[2].實例化私有內部LazyIterator類->LazyIterator
    lookupIterator = new LazyIterator(service, loader);
    }
    
    public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader)
    {
     return new ServiceLoader<>(service, loader);
    }
    
    public static <S> ServiceLoader<S> load(Class<S> service) {
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      return ServiceLoader.load(service, cl);
    } 
    
 }

2.LazyIterator源碼:
YMeW11.png

  private class LazyIterator implements Iterator<S> {

    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;

    private LazyIterator(Class<S> service, ClassLoader loader) {
      this.service = service;
      this.loader = loader;
    }

    private boolean hasNextService() {
      if (nextName != null) {
        return true;
      }
      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;
        }
        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 {
        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
    }

    public boolean hasNext() {
      if (acc == null) {
        return hasNextService();
      } else {
        PrivilegedAction<Boolean> action =
            new PrivilegedAction<Boolean>() {
              public Boolean run() {
                return hasNextService();
              }
            };
        return AccessController.doPrivileged(action, acc);
      }
    }

    public S next() {
      if (acc == null) {
        return nextService();
      } else {
        PrivilegedAction<S> action =
            new PrivilegedAction<S>() {
              public S run() {
                return nextService();
              }
            };
        return AccessController.doPrivileged(action, acc);
      }
    }

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

使用舉例

[1].Dubbo SPI 機制:

META-INF/dubbo.internal/xxx=介面全限定名

Dubbo 並未使用 Java SPI,而是重新實現了一套功能更強的 SPI 機制。
Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以載入指定的實現類。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路徑下。
與Java SPI 實現類配置不同,Dubbo SPI 是通過鍵值對的方式進行配置,這樣我們可以按需載入指定的實現類。另外,在測試 Dubbo SPI 時,需要在 Robot 介面上標註 @SPI 註解。
[2].Cache SPI 機制:

  META-INF/service/javax.cache.spi.CachingProvider=xxx

[3]Spring SPI 機制:

META-INF/services/org.apache.commons.logging.LogFactory=xxx

[4].SpringBoot SPI機制:

META-INF/spring.factories/org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx

在springboot的自動裝配過程中,最終會載入META-INF/spring.factories文件,而載入的過程是由SpringFactoriesLoader載入的。從CLASSPATH下的每個Jar包中搜尋所有META-INF/spring.factories配置文件,然後將解析properties文件,找到指定名稱的配置後返回
源碼:

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// spring.factories文件的格式為:key=value1,value2,value3
// 從所有的jar包中找到META-INF/spring.factories文件
// 然後從文件中解析出key=factoryClass類名稱的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    // 取得資源文件的URL
    Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    List<String> result = new ArrayList<String>();
    // 遍歷所有的URL
    while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        // 根據資源文件URL解析properties文件,得到對應的一組@Configuration類
        Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
        String factoryClassNames = properties.getProperty(factoryClassName);
        // 組裝數據,並返回
        result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
    }
    return result;
}

[5].自定義序列化實現SPI:META-INF/services/xxx=介面全限定名

參考學習Java SPI 和Dubbo SPI機制源碼,自己動手實現序列化工具類等

版權聲明:本文為博主原創文章,遵循相關版權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源。


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

-Advertisement-
Play Games
更多相關文章
  • 效果圖 index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> <link rel="stylesheet" href="css/style.css"> </head ...
  • 一、安裝 npm 1、 https://www.cnblogs.com/lgx5/p/10732016.html 二、安裝nginx 1、 https://blog.csdn.net/zuoyigehaizei/article/details/93061002 2、配置Nginx多站點 打開配置文件 ...
  • 效果圖 index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> <link rel="stylesheet" href="css/style.css"> </head ...
  • 本文詳細闡述了Vue的watch、函數式組件、非同步更新、非同步組件、批量導入等高級特性 ...
  • 效果圖 index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> <link rel="stylesheet" href="css/reset.css"> <link ...
  • 問題原因:參考element-ui文檔: W3C 標準中有如下規定: When there is only one single-line text input field in a form, the user agent should accept Enter in that field as ...
  • 效果圖 index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> <link rel="stylesheet" href="css/style.css"> </head ...
  • 效果圖 index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> <link rel="stylesheet" href="https://cdn.bootcss.co ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...