一、背景介紹
在看spring源碼和dubbo源碼的時候,發現兩者都用採用了JDK中spi的技術,發現都有大作用,所以就來分析下JDK中的SPI的使用方式及源碼實現。
二、什麼是SPI
SPI的全稱是 Service Provider Interface。 一種從特定路徑下,將實現了某些特定接口的類加載到內存中的方式(爲什麼會如此說,請看後面分析)。提供了另外一種方式加載實現類,也降低了代碼的耦合程度,提升了代碼的可擴展性。
實現SPI的地方主要有以下3處。主要的類和方法分別是:
- JDK
- java.util.ServiceLoader#load
- Spring
- org.springframework.core.io.support.SpringFactoriesLoader#loadFactories
- Dubbo
- org.apache.dubbo.common.extension.ExtensionLoader#
三、舉例說明JDK SPI的使用方式
1. 自定義實現類, 實現數據庫驅動 Driver.class
package com.fattyca1.driver;
import java.sql.*;
import java.util.Properties;
import java.util.logging.Logger;
/**
* <br>自定義數據庫操作</br>
*
* @author fattyca1
*/
public abstract class CustomziedDriver implements Driver {
public Connection connect(String url, Properties info) throws SQLException {
return null;
}
public boolean acceptsURL(String url) throws SQLException {
return false;
}
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
return new DriverPropertyInfo[0];
}
public int getMajorVersion() {
return 0;
}
public int getMinorVersion() {
return 0;
}
public boolean jdbcCompliant() {
return false;
}
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
// 自定義方法
protected abstract void outDbName();
}
package com.fattyca1.driver;
/**
* <br>自定義實現數據庫驅動</br>
*
* @author fattyca1
*/
public class Fattyca1Driver extends CustomziedDriver {
@Override
public void outDbName() {
System.out.println("fattyca1 db driver init ... ");
}
}
2. 在resources下創建META-INF/services文件夾,並在目錄中創建文件。文件名稱爲實現接口名稱,文件中內容爲接口實現類。
3. 建立一個main程序來測試結果
import com.fattyca1.driver.Fattyca1Driver;
import java.sql.Driver;
import java.util.ServiceLoader;
/**
* <br>spi 測試客戶端</br>
*
* @author fattyca1
*/
public class SPIClient {
public static void main(String[] args) {
ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
for (Driver driver : drivers) {
if(driver instanceof Fattyca1Driver) {
((Fattyca1Driver) driver).outDbName();
}
}
}
}
4. 實驗結果。如下圖:
以上方式就是JDK中的SPI的實現方式,通過此方法,我們實現了自定義的Driver,可以使用自己定義的方式連接數據庫。
四、JDK中ServiceLoader的源碼分析
通過看上面例子, 大概瞭解到如果使用JDK中的SPI的實現方式,但是我們還不知道JDK是如何操作的。 接下來,我們就分析分析JDK的實現方式。
1. 從構造函數入口,發現其構造方法是私有,無法被外部初始化,所以我們直接從提供的靜態方法入手
// ServiceLoader的構造函數 private ServiceLoader(Class<S> svc, ClassLoader cl) { ... }
2. 我們從ServiceLoader#load方法開始,一步步點進去,發現其最後調用的私有構造方法,構造中的核心方法是reload()。
private ServiceLoader(Class<S> svc, ClassLoader cl) { ... reload(); // 主要實現方法 }
3. 查看reload()方法的具體實現,發現並無多餘代碼,主要是清除了cacheMap, 實例化了lookupIterator,。
public void reload() { providers.clear(); // 清除緩存中的對象 lookupIterator = new LazyIterator(service, loader); // 初始化迭代器,此迭代器被調用時,纔會加載類。 }
4.分析LazyIterator. 從名字可以看出,這是懶加載的類(命名清晰的好處)。此代碼實現起來比較簡單。 通過給定的class名稱, 讀取資源文件, 然後加載文件中的實現類。通過反射,生成實現類,放入cacheMap中。
private class LazyIterator implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
...
}
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;
}
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
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
五、SPI使用場景
- jdk中數據庫驅動的加載
- spring中各種組件的插拔
- dubbo中自定義rpc協議,序列化方式,過濾器等
六、總結
至此,我們分析完JDK源碼中的SPI的實現(代碼實現簡單,沒有很仔細),發現實現簡單,功能d強大,大家是否學習到了呢? 我們在自己實現代碼的時候,可以多考慮學習此方式,也可以給代碼松耦合,提升自己的代碼質量。spring和dubbo中的源碼和JDK中的源碼都十分相似,實現起來大同小異,大家有時間可以自己比較比較。