JDK SPI機(jī)制以及自定義SPI類加載問題
概述
介紹SPI之前,我們先了解一下為什么要用SPI
JDBC相信已經(jīng)不陌生了,JDBC 是一個(gè)標(biāo)準(zhǔn)。
不同的數(shù)據(jù)庫廠商(如,mysql、oracle等)會(huì)根據(jù)這個(gè)標(biāo)準(zhǔn),有它們自己的實(shí)現(xiàn)。
既然,JDBC 是一個(gè)標(biāo)準(zhǔn),那么 JDBC 的接口,應(yīng)該就已經(jīng)存在于JDK 中了,以前我們?cè)谑褂肑DBC的時(shí)候,都是需要加載Driver驅(qū)動(dòng)的,如:
Class.forName("com.mysql.jdbc.Driver"); String url = "jdbc:mysql:///test"; Connection connection = = DriverManager.getConnection(url,"root","123456");
但是我們?nèi)绻麤]有寫的這行代碼,也是可以讓com.mysql.jdbc.Driver正確加載的,即:
String url = "jdbc:///test"; Connection connection = = DriverManager.getConnection(url,"root","123456");
那么這是為什么呢?要知道DriverManager類是由啟動(dòng)類加載器加載,而且根據(jù)全盤負(fù)責(zé)委托機(jī)制,每個(gè)類都有自己的類加載器,那么負(fù)責(zé)加載當(dāng)前類的類加載器也會(huì)去加載當(dāng)前類中引用的其他類,前提是引用的類沒有被加載過。
例如ClassA中有個(gè)變量 ClassB,那么加載ClassA的類加載器會(huì)去加載ClassB,如果找不到ClassB,則異常。
根據(jù)以上特性,那么JDK中的DriverManager啟動(dòng)類加載器會(huì)嘗試去加載MySql的jar包,但明顯是找不到的,因?yàn)樗静辉?strong>JDK中
那我們不妨看一下DriverManager的源碼:
繼續(xù)查看一下其中的 loadInitialDrivers() 方法:
private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // 1 AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); println("DriverManager.initialize: jdbc.drivers = " + drivers); // 2 if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); // 3 Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } }
分析其中兩個(gè)地方:
1、這里使用了ServiceLoader機(jī)制來加載驅(qū)動(dòng),它是Java提供的一套 SPI(Service Provider Interface) 框架,用于實(shí)現(xiàn)服務(wù)提供方與服務(wù)使用方解耦
2、使用 jdbc.drivers 定義的驅(qū)動(dòng)名加載驅(qū)動(dòng)
3、ClassLoader.getSystemClassLoader() 就是應(yīng)用程序類加載器
規(guī)則
SPI機(jī)制是JDK提供接口,第三方Jar包實(shí)現(xiàn),接口由啟動(dòng)類加載器加載,實(shí)現(xiàn)類不在JDK中,需要反向委派,由線程上下文加載器加載。它約定:在 jar 包的 META-INF/services 包下,以接口全限定名為文件名,文件內(nèi)容是實(shí)現(xiàn)類名稱
這樣便可以使用剛才loadInitialDrivers這個(gè)方法
ServiceLoader<接口類型> allImpls = ServiceLoader.load(接口類型.class); Iterator<接口類型> iter = allImpls.iterator(); while(iter.hasNext()) { iter.next(); }
來得到具體的Driver實(shí)現(xiàn)類,那我們?cè)僮芬幌?strong>ServiceLoader是如何通過Driver.class接口來加載它具體的實(shí)現(xiàn)類的,現(xiàn)在進(jìn)入 load() 方法:
public static <S> ServiceLoader<S> load(Class<S> service) { //獲取到了線程上下文類加載器 ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
線程上下文類加載器是當(dāng)前線程使用的類加載器,默認(rèn)就是應(yīng)用程序類加載器,那么這個(gè)方法中的load方法就會(huì)使用剛才拿到的線程上下文類加載器去加載目標(biāo)實(shí)現(xiàn)類,不過這個(gè)方法比較深,真正加載的具體代碼在 ServiceLoader 的內(nèi)部類 LazyIterator 的nextService方法中:
自定義實(shí)現(xiàn)
注解
package com.phz.prpc.extension;import java.lang.annotation.*;/** * <p> * {@code SPI}注解,可運(yùn)行其他第三方實(shí)現(xiàn)的抽象接口需使用此注解 * </p> * </br> * <p> * {@code JDK}的{@code SPI}機(jī)制是{@code JDK}提供接口,第三方{@code jar}包實(shí)現(xiàn),接口由啟動(dòng)類加載器加載,實(shí)現(xiàn)類不在{@code JDK}中,需要反向委派,由線程上下文加載器加載。 * </p> * </br> * <p> * 它約定:在 {@code jar} 包的 {@code META-INF/services} 包下,以接口全限定名為文件名,文件內(nèi)容是實(shí)現(xiàn)類名稱 * </p> * </br> * <p> * 那么我們完全可以參照它的思想取仿寫一個(gè) * </p> * * @author PengHuanZhi * @date 2022年01月16日 17:50 */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Spi { }
基于SPI的偽類加載器
package com.phz.prpc.extension;import lombok.Data; import lombok.extern.slf4j.Slf4j;import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.util.Enumeration;import static java.nio.charset.StandardCharsets.UTF_8;/** * <p> * 自己實(shí)現(xiàn)一個(gè)擴(kuò)展類加載器輔助類 * ,區(qū)別于{@code JDK}的{@code SPI}機(jī)制,我們預(yù)定好在 {@code jar} 包的 {@code META-INF/extensions} 目錄下方存放擴(kuò)展類文件,文件內(nèi)容就為第三方實(shí)現(xiàn)的全路徑 * </p> * * @author PengHuanZhi * @date 2022年01月16日 17:56 */ @Slf4j @Data public final class ExtensionLoader<T> { /** * 約定第三方實(shí)現(xiàn)配置文件目錄 **/ private static final String SERVICE_DIRECTORY = "META-INF/extensions/"; /** * 接口的類型,用于獲取此接口下的第三方實(shí)現(xiàn) **/ private final Class<?> type; /** * 通過接口的{@link Class}對(duì)象獲取其第三方實(shí)現(xiàn)類的加載器 * * @param type 接口的類型 * @return ExtensionLoader<T> 返回一個(gè)指定接口類型的類加載器輔助類 **/ public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) { if (type == null) { throw new IllegalArgumentException("Spi需要知道你想要找到哪個(gè)功能的第三方實(shí)現(xiàn)!"); } if (!type.isInterface()) { throw new IllegalArgumentException("只支持尋找接口類型的第三方實(shí)現(xiàn)!"); } if (type.getAnnotation(Spi.class) == null) { throw new IllegalArgumentException("目標(biāo)接口必須被@Spi注解標(biāo)注!"); } return new ExtensionLoader<>(type); } /** * 獲取這個(gè)接口指定名稱的第三方實(shí)現(xiàn)對(duì)象 * * @return T 返回目標(biāo)實(shí)現(xiàn) **/ public T getExtension() { // 加載到一個(gè)第三方實(shí)現(xiàn) Class<T> clazz = loadExtensionFile(); if (clazz == null) { return null; } try { return clazz.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException("實(shí)例化失敗 : " + clazz); } } /** * 加載約定好的目錄下方的名稱為接口全路徑的擴(kuò)展文件 * * @return Class<T> 返回目標(biāo)第三方實(shí)現(xiàn)的{@link Class}對(duì)象 **/ private Class<T> loadExtensionFile() { //想要獲取誰的實(shí)現(xiàn)類 String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName(); try { Enumeration<URL> urls; ClassLoader classLoader = ExtensionLoader.class.getClassLoader(); urls = classLoader.getResources(fileName); if (urls != null) { URL resourceUrl = urls.nextElement(); return loadResource(classLoader, resourceUrl); } return null; } catch (IOException e) { log.error(e.getMessage()); return null; } } /** * 讀取擴(kuò)展文件的內(nèi)容,找到第三方實(shí)現(xiàn)的全路徑,并獲得其{@link Class}對(duì)象 * * @param classLoader 擴(kuò)展類加載器輔助類的類加載器 * @param resourceUrl 文件在資源{@code URL} * @return Class<T> 返回目標(biāo){@link Class}對(duì)象 **/ @SuppressWarnings("unchecked") private Class<T> loadResource(ClassLoader classLoader, URL resourceUrl) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), UTF_8))) { String line; while ((line = reader.readLine()) != null) { // 可能是注釋 final int ci = line.indexOf('#'); //如果是第一個(gè)位置,則這一行都可以不用解析了 if (ci == 0) { continue; } else if (ci > 0) { //如果非第一個(gè)位置,需要將注釋前面的內(nèi)容取出來,也就是將注釋后面的內(nèi)容截取 line = line.substring(0, ci); } return (Class<T>) classLoader.loadClass(line.trim()); } } catch (IOException | ClassNotFoundException e) { log.error(e.getMessage()); return null; } return null; } }
測(cè)試
參考如下方式:
代碼中體現(xiàn)(因?yàn)樽远x的SPI機(jī)制用于筆者自己的項(xiàng)目下方,所以讀者可以僅關(guān)注代碼中的11行即可):
/** * 使用負(fù)載均衡算法從服務(wù)集合中選取一個(gè)服務(wù) * * @param serviceInstances 服務(wù)集合 * @return InetSocketAddress 選取的服務(wù) **/ public InetSocketAddress doChoice(List<InetSocketAddress> serviceInstances) { String loadBalanceAlgorithm = prpcProperties.getLoadBalanceAlgorithm(); LoadBalance loadBalance; try { loadBalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(); if (loadBalance == null) { loadBalance = LoadBalanceAlgorithm.valueOf(loadBalanceAlgorithm); } } catch (IllegalArgumentException e) { log.error("未知的負(fù)載均衡算法:{},異常信息為:{}", loadBalanceAlgorithm, e.getMessage()); throw new PrpcException(ErrorMsg.UNKNOWN_LOAD_BALANCE_ALGORITHM); } return loadBalance.doChoice(serviceInstances); }
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Springboot基于websocket實(shí)現(xiàn)簡(jiǎn)單在線聊天功能
這篇文章主要介紹了Springboot基于websocket實(shí)現(xiàn)簡(jiǎn)單在線聊天功能,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-0615道非常經(jīng)典的Java面試題 附詳細(xì)答案
這篇文章主要為大家推薦了15道非常經(jīng)典的Java面試題,附詳細(xì)答案,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10解決MyBatis-Plus使用動(dòng)態(tài)表名selectPage不生效的問題
這篇文章主要介紹了如惡化解決MyBatis-Plus使用動(dòng)態(tài)表名selectPage不生效的問題,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-11-11java程序中指定某個(gè)瀏覽器打開的實(shí)現(xiàn)方法
最近工作中遇到一個(gè)需求,是要利用java打開指定瀏覽器,整理后發(fā)現(xiàn)有四種解決的方法,所以想著分享出來,下面這篇文章主要給大家介紹了java程序中指定某個(gè)瀏覽器打開的實(shí)現(xiàn)方法,,需要的朋友可以參考下。2017-03-03SpringBoot編譯target目錄下沒有resource下的文件踩坑記錄
這篇文章主要介紹了SpringBoot編譯target目錄下沒有resource下的文件踩坑記錄,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08