日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

spi 动态加载、卸载_理解 ServiceLoader类与SPI机制

發布時間:2024/9/27 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 spi 动态加载、卸载_理解 ServiceLoader类与SPI机制 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

對于Java中的Service類和SPI機制的透徹理解,也算是對Java類加載模型的掌握的不錯的一個反映。

了解一個不太熟悉的類,那么從使用案例出發,讀懂源代碼以及代碼內部執行邏輯是一個不錯的學習方式。


一、使用案例

通常情況下,使用ServiceLoader來實現SPI機制。 SPI 全稱為 (Service Provider Interface) ,是JDK內置的一種服務提供發現機制。SPI是一種動態替換發現的機制, 比如有個接口,想運行時動態的給它添加實現,你只需要添加一個實現。

SPI機制可以歸納為如下的圖:


起始這樣說起來還是比較抽象,那么下面舉一個具體的例子,案例為JDBC的調用例子:

案例如下:


JDBC中的接口即為:java.sql.Driver

SPI機制的實現核心類為:java.util.ServiceLoader

Provider則為:com.mysql.jdbc.Driver

外層調用則是我們進行增刪改查JDBC操作所在的代碼塊,但是對于那些現在還沒有學過JDBC的小伙伴來說(不難學~),這可能會有點難理理解,所以我這里就舉一個使用案例:

按照上圖的SPI執行邏輯,我們需要寫一個接口、至少一個接口的實現類、以及外層調用的測試類。

但是要求以這樣的目錄書結構來定義項目文件,否則SPI機制無法實現(類加載機制相關,之后會講):

E:.│ MyTest.java│├─com│ └─fisherman│ └─spi│ │ HelloInterface.java│ ││ └─impl│ HelloJava.java│ HelloWorld.java│└─META-INF └─services com.fisherman.spi.HelloInterface123456789101112131415

其中:

  • MyTest.java為測試java文件,負責外層調用;
  • HelloInterface.java為接口文件,等待其他類將其實現;
  • HelloJava.java 以及 HelloWorld.java 為接口的實現類;
  • META-INF
    └─services
    com.fisherman.spi.HelloInterface 為配置文件,負責類加載過程中的路徑值。
  • 首先給出接口的邏輯:

    public interface HelloInterface { void sayHello();}123

    其次,兩個實現類的代碼:

    public class HelloJava implements HelloInterface { @Override public void sayHello() { System.out.println("HelloJava."); }}123456public class HelloWorld implements HelloInterface { @Override public void sayHello() { System.out.println("HelloWorld."); }}123456

    然后,配置文件:com.fisherman.spi.HelloInterface

    com.fisherman.spi.impl.HelloWorldcom.fisherman.spi.impl.HelloJava12

    最后測試文件:

    public class MyTest26 { public static void main(String[] args) { ServiceLoader loaders = ServiceLoader.load(HelloInterface.class); for (HelloInterface in : loaders) { in.sayHello(); } }}12345678910111213

    測試文件運行后的控制臺輸出:

    HelloWorld.HelloJava.12

    我們從控制臺的打印信息可知我們成功地實現了SPI機制,通過 ServiceLoader 類實現了等待實現的接口和實現其接口的類之間的聯系。

    下面我們來深入探討以下,SPI機制的內部實現邏輯。


    二、ServiceLoader類的內部實現邏輯

    Service類的構造方法是私有的,所以我們只能通過掉用靜態方法的方式來返回一個ServiceLoader的實例:

    方法的參數為被實現結構的Class對象。

    ServiceLoader loaders = ServiceLoader.load(HelloInterface.class); 1

    其內部實現邏輯如所示,不妨按調用步驟來分步講述:

    1.上述load方法的源代碼:

    public static ServiceLoader load(Class service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl);}1234

    完成的工作:

  • 得到當前線程的上下文加載器,用于后續加載實現了接口的類
  • 調用另一個load方法的重載版本(多了一個類加載器的引用參數)
  • 2.被調用的另一個load重載方法的源代碼:

    public static ServiceLoader load(Class service, ClassLoader loader) { return new ServiceLoader<>(service, loader); }123456

    完成的工作:

    • 調用了類ServiceLoader的私有構造器

    3.私有構造器的源代碼:

    private ServiceLoader(Class 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();}123456

    完成的工作:

  • 空指針和安全性的一些判斷以及處理;
  • 并對兩個重要要的私有實例變量進行了賦值:private final Class service; private final ClassLoader loader; 12
  • reload()方法來迭代器的清空并重新賦值
  • SercviceLoader的初始化跑完如上代碼就結束了。但是實際上聯系待實現接口和實現接口的類之間的關系并不只是在構造ServiceLoader類的過程中完成的,而是在迭代器的方法hasNext()中實現的。

    這個聯系通過動態調用的方式實現,其代碼分析就見下一節吧:


    三、動態調用的實現

    在使用案例中寫的forEach語句內部邏輯就是迭代器,迭代器的重要方法就是hasNext():

    ServiceLoader是一個實現了接口Iterable接口的類。

    hasNext()方法的源代碼:

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

    拋出復雜的確保安全的操作,可以將上述代碼看作就是調用了方法:hasNextService.

    hasNextService()方法的源代碼:

    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;}123456789101112131415161718192021222324

    上述代碼中比較重要的代碼塊是:

    String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName);123

    此處PREFIX(前綴)是一個常量字符串(用于規定配置文件放置的目錄,使用相對路徑,說明其上層目錄為以項目名為名的文件夾):

    private static final String PREFIX = "META-INF/services/";1

    那么fullName會被賦值為:"META-INF/services/com.fisherman.spi.HelloInterface"

    然后調用方法getSystemResources或getResources將fullName參數視作為URL,返回配置文件的URL集合 。

    pending = parse(service, configs.nextElement());1

    parse方法是憑借 參數1:接口的Class對象 和 參數2:配置文件的URL來解析配置文件,返回值是含有配置文件里面的內容,也就是實現類的全名(包名+類名)字符串的迭代器;

    最后調用下面的代碼,得到下面要加載的類的完成類路徑字符串,相對路徑。在使用案例中,此值就可以為:

    com.fisherman.spi.impl.HelloWorld和com.fisherman.spi.impl.HelloJava

    nextName = pending.next();1

    這僅僅是迭代器判斷是否還有下一個迭代元素的方法,而獲取每輪迭代元素的方法為:nextService()方法。

    nextService()方法源碼:

    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}123456789101112131415161718192021222324252627

    拋出一些負責安全以及處理異常的代碼,核心代碼為:

    1.得到接口實現類的完整類路徑字符串:

    String cn = nextName;1

    2使用loader引用的類加載器來加載cn指向的接口實現類,并返回其Class對象(但是不初始化此類):

    c = Class.forName(cn, false, loader);1

    3.調用Class對象的newInstance()方法來調用無參構造方法,返回Provider實例:

    S p = service.cast(c.newInstance());1//cast方法只是在null和類型檢測通過的情況下進行了簡單的強制類型轉換public T cast(Object obj) { if (obj != null && !isInstance(obj)) throw new ClassCastException(cannotCastMsg(obj)); return (T) obj;}123456

    4.將Provider實例放置于providers指向的HashMap中:

    providers.put(cn, p);1

    5.返回provider實例:

    return p;1

    ServiceLoader類的小總結:

  • 利用創建ServiceLoader類的線程對象得到上下文類加載器,然后將此加載器用于加載provider類;
  • 利用反射機制來得到provider的類對象,再通過類對象的newInstance方法得到provider的實例;
  • ServiceLoader負責provider類加載的過程數據類的動態加載;
  • provider類的相對路徑保存于配置文件中,需要完整的包名,如:com.fisherman.spi.impl.HelloWorld
  • 四、總結與評價

  • SPI的理念:通過動態加載機制實現面向接口編程,提高了框架和底層實現的分離;
  • ServiceLoader 類提供的 SPI 實現方法只能通過遍歷迭代的方法實現獲得Provider的實例對象,如果要注冊了多個接口的實現類,那么顯得效率不高;
  • 雖然通過靜態方法返回,但是每一次Service.load方法的調用都會產生一個ServiceLoader實例,不屬于單例設計模式;
  • ServiceLoader與ClassLoader是類似的,都可以負責一定的類加載工作,但是前者只是單純地加載特定的類,即要求實現了Service接口的特定實現類;而后者幾乎是可以加載所有Java類;
  • 對于SPi機制的理解有兩個要點:理解動態加載的過程,知道配置文件是如何被利用,最終找到相關路徑下的類文件,并加載的;理解 SPI 的設計模式:接口框架 和底層實現代碼分離
  • 之所以將ServiceLoader類內部的迭代器對象稱為LazyInterator,是因為在ServiceLoader對象創建完畢時,迭代器內部并沒有相關元素引用,只有真正迭代的時候,才會去解析、加載、最終返回相關類(迭代的元素);
  • 創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

    總結

    以上是生活随笔為你收集整理的spi 动态加载、卸载_理解 ServiceLoader类与SPI机制的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。