JDK SPI機制以及自定義SPI類加載問題
概述
介紹SPI之前,我們先了解一下為什么要用SPI
JDBC相信已經(jīng)不陌生了,JDBC 是一個標(biāo)準(zhǔn)。
不同的數(shù)據(jù)庫廠商(如,mysql、oracle等)會根據(jù)這個標(biāo)準(zhǔn),有它們自己的實現(xiàn)。
既然,JDBC 是一個標(biāo)準(zhǔn),那么 JDBC 的接口,應(yīng)該就已經(jīng)存在于JDK 中了,以前我們在使用JDBC的時候,都是需要加載Driver驅(qū)動的,如:
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類是由啟動類加載器加載,而且根據(jù)全盤負(fù)責(zé)委托機制,每個類都有自己的類加載器,那么負(fù)責(zé)加載當(dāng)前類的類加載器也會去加載當(dāng)前類中引用的其他類,前提是引用的類沒有被加載過。
例如ClassA中有個變量 ClassB,那么加載ClassA的類加載器會去加載ClassB,如果找不到ClassB,則異常。
根據(jù)以上特性,那么JDK中的DriverManager啟動類加載器會嘗試去加載MySql的jar包,但明顯是找不到的,因為它根本不在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);
}
}
}
分析其中兩個地方:
1、這里使用了ServiceLoader機制來加載驅(qū)動,它是Java提供的一套 SPI(Service Provider Interface) 框架,用于實現(xiàn)服務(wù)提供方與服務(wù)使用方解耦
2、使用 jdbc.drivers 定義的驅(qū)動名加載驅(qū)動
3、ClassLoader.getSystemClassLoader() 就是應(yīng)用程序類加載器
規(guī)則
SPI機制是JDK提供接口,第三方Jar包實現(xiàn),接口由啟動類加載器加載,實現(xiàn)類不在JDK中,需要反向委派,由線程上下文加載器加載。它約定:在 jar 包的 META-INF/services 包下,以接口全限定名為文件名,文件內(nèi)容是實現(xiàn)類名稱

這樣便可以使用剛才loadInitialDrivers這個方法
ServiceLoader<接口類型> allImpls = ServiceLoader.load(接口類型.class);
Iterator<接口類型> iter = allImpls.iterator();
while(iter.hasNext()) {
iter.next();
}來得到具體的Driver實現(xiàn)類,那我們再追一下ServiceLoader是如何通過Driver.class接口來加載它具體的實現(xiàn)類的,現(xià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)用程序類加載器,那么這個方法中的load方法就會使用剛才拿到的線程上下文類加載器去加載目標(biāo)實現(xiàn)類,不過這個方法比較深,真正加載的具體代碼在 ServiceLoader 的內(nèi)部類 LazyIterator 的nextService方法中:

自定義實現(xiàn)
注解
package com.phz.prpc.extension;import java.lang.annotation.*;/**
* <p>
* {@code SPI}注解,可運行其他第三方實現(xiàn)的抽象接口需使用此注解
* </p>
* </br>
* <p>
* {@code JDK}的{@code SPI}機制是{@code JDK}提供接口,第三方{@code jar}包實現(xiàn),接口由啟動類加載器加載,實現(xiàn)類不在{@code JDK}中,需要反向委派,由線程上下文加載器加載。
* </p>
* </br>
* <p>
* 它約定:在 {@code jar} 包的 {@code META-INF/services} 包下,以接口全限定名為文件名,文件內(nèi)容是實現(xiàn)類名稱
* </p>
* </br>
* <p>
* 那么我們完全可以參照它的思想取仿寫一個
* </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>
* 自己實現(xiàn)一個擴展類加載器輔助類
* ,區(qū)別于{@code JDK}的{@code SPI}機制,我們預(yù)定好在 {@code jar} 包的 {@code META-INF/extensions} 目錄下方存放擴展類文件,文件內(nèi)容就為第三方實現(xiàn)的全路徑
* </p>
*
* @author PengHuanZhi
* @date 2022年01月16日 17:56
*/
@Slf4j
@Data
public final class ExtensionLoader<T> {
/**
* 約定第三方實現(xiàn)配置文件目錄
**/
private static final String SERVICE_DIRECTORY = "META-INF/extensions/"; /**
* 接口的類型,用于獲取此接口下的第三方實現(xiàn)
**/
private final Class<?> type; /**
* 通過接口的{@link Class}對象獲取其第三方實現(xiàn)類的加載器
*
* @param type 接口的類型
* @return ExtensionLoader<T> 返回一個指定接口類型的類加載器輔助類
**/
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Spi需要知道你想要找到哪個功能的第三方實現(xiàn)!");
}
if (!type.isInterface()) {
throw new IllegalArgumentException("只支持尋找接口類型的第三方實現(xiàn)!");
}
if (type.getAnnotation(Spi.class) == null) {
throw new IllegalArgumentException("目標(biāo)接口必須被@Spi注解標(biāo)注!");
}
return new ExtensionLoader<>(type);
} /**
* 獲取這個接口指定名稱的第三方實現(xiàn)對象
*
* @return T 返回目標(biāo)實現(xiàn)
**/
public T getExtension() {
// 加載到一個第三方實現(xiàn)
Class<T> clazz = loadExtensionFile();
if (clazz == null) {
return null;
}
try {
return clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("實例化失敗 : " + clazz);
}
} /**
* 加載約定好的目錄下方的名稱為接口全路徑的擴展文件
*
* @return Class<T> 返回目標(biāo)第三方實現(xiàn)的{@link Class}對象
**/
private Class<T> loadExtensionFile() {
//想要獲取誰的實現(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;
}
} /**
* 讀取擴展文件的內(nèi)容,找到第三方實現(xiàn)的全路徑,并獲得其{@link Class}對象
*
* @param classLoader 擴展類加載器輔助類的類加載器
* @param resourceUrl 文件在資源{@code URL}
* @return Class<T> 返回目標(biāo){@link Class}對象
**/
@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('#');
//如果是第一個位置,則這一行都可以不用解析了
if (ci == 0) {
continue;
} else if (ci > 0) {
//如果非第一個位置,需要將注釋前面的內(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;
}
}測試
參考如下方式:


代碼中體現(xiàn)(因為自定義的SPI機制用于筆者自己的項目下方,所以讀者可以僅關(guān)注代碼中的11行即可):
/**
* 使用負(fù)載均衡算法從服務(wù)集合中選取一個服務(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);
}以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Springboot基于websocket實現(xiàn)簡單在線聊天功能
這篇文章主要介紹了Springboot基于websocket實現(xiàn)簡單在線聊天功能,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-06-06
15道非常經(jīng)典的Java面試題 附詳細(xì)答案
這篇文章主要為大家推薦了15道非常經(jīng)典的Java面試題,附詳細(xì)答案,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10
解決MyBatis-Plus使用動態(tài)表名selectPage不生效的問題
這篇文章主要介紹了如惡化解決MyBatis-Plus使用動態(tài)表名selectPage不生效的問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-11-11
SpringBoot編譯target目錄下沒有resource下的文件踩坑記錄
這篇文章主要介紹了SpringBoot編譯target目錄下沒有resource下的文件踩坑記錄,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08

