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其中:
└─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完成的工作:
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完成的工作:
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());1parse方法是憑借 參數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;12使用loader引用的類加載器來加載cn指向的接口實現類,并返回其Class對象(但是不初始化此類):
c = Class.forName(cn, false, loader);13.調用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;}1234564.將Provider實例放置于providers指向的HashMap中:
providers.put(cn, p);15.返回provider實例:
return p;1ServiceLoader類的小總結:
四、總結與評價
總結
以上是生活随笔為你收集整理的spi 动态加载、卸载_理解 ServiceLoader类与SPI机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql锁表更新_Mysql Inno
- 下一篇: 基线长度中误差的计算_电子战支援实施中的