JDK SPI機(jī)制以及自定義SPI類加載問(wèn)題
概述
介紹SPI之前,我們先了解一下為什么要用SPI
JDBC相信已經(jīng)不陌生了,JDBC 是一個(gè)標(biāo)準(zhǔn)。
不同的數(shù)據(jù)庫(kù)廠商(如,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)绻麤](méi)有寫(xiě)的這行代碼,也是可以讓com.mysql.jdbc.Driver正確加載的,即:
String url = "jdbc:///test"; Connection connection = = DriverManager.getConnection(url,"root","123456");
那么這是為什么呢?要知道DriverManager類是由啟動(dòng)類加載器加載,而且根據(jù)全盤(pán)負(fù)責(zé)委托機(jī)制,每個(gè)類都有自己的類加載器,那么負(fù)責(zé)加載當(dāng)前類的類加載器也會(huì)去加載當(dāng)前類中引用的其他類,前提是引用的類沒(méi)有被加載過(guò)。
例如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ī)制來(lái)加載驅(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();
}來(lái)得到具體的Driver實(shí)現(xiàn)類,那我們?cè)僮芬幌?strong>ServiceLoader是如何通過(guò)Driver.class接口來(lái)加載它具體的實(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)類,不過(guò)這個(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>
* 那么我們完全可以參照它的思想取仿寫(xiě)一個(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; /**
* 通過(guò)接口的{@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() {
//想要獲取誰(shuí)的實(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)容取出來(lá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)單在線聊天功能,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06
解決druid監(jiān)控頁(yè)面SQL不顯示的問(wèn)題
這篇文章主要介紹了解決druid監(jiān)控頁(yè)面SQL不顯示的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
15道非常經(jīng)典的Java面試題 附詳細(xì)答案
這篇文章主要為大家推薦了15道非常經(jīng)典的Java面試題,附詳細(xì)答案,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10
解決MyBatis-Plus使用動(dòng)態(tài)表名selectPage不生效的問(wèn)題
這篇文章主要介紹了如惡化解決MyBatis-Plus使用動(dòng)態(tài)表名selectPage不生效的問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-11-11
java程序中指定某個(gè)瀏覽器打開(kāi)的實(shí)現(xiàn)方法
最近工作中遇到一個(gè)需求,是要利用java打開(kāi)指定瀏覽器,整理后發(fā)現(xiàn)有四種解決的方法,所以想著分享出來(lái),下面這篇文章主要給大家介紹了java程序中指定某個(gè)瀏覽器打開(kāi)的實(shí)現(xiàn)方法,,需要的朋友可以參考下。2017-03-03
SpringBoot編譯target目錄下沒(méi)有resource下的文件踩坑記錄
這篇文章主要介紹了SpringBoot編譯target目錄下沒(méi)有resource下的文件踩坑記錄,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08

