淺談SPI機(jī)制
前言
這段時間在研究一個開源框架,發(fā)現(xiàn)其中有一些以SPI命名的包,經(jīng)過搜索、整理以及思考之后,將學(xué)習(xí)的筆記、心得整理出來,供日后復(fù)習(xí)使用。
SPI
SPI全稱是Service Provider Interface,翻譯過來是服務(wù)提供者接口,這個翻譯其實(shí)不那么形象,理解起來也不是很好理解,至少不那么見名知意。
其實(shí)SPI是一種機(jī)制,一種類似于服務(wù)發(fā)現(xiàn)的機(jī)制,什么叫做服務(wù)發(fā)現(xiàn)呢,就是能夠根據(jù)情況發(fā)現(xiàn)已有服務(wù)的機(jī)制,好像說了跟沒說一樣,對吧,下面我們逐個來理解。
首先是服務(wù),英文叫做Service,服務(wù)可以理解為就是某一種或者某幾種功能,比如日常生活中的醫(yī)生,提供看病的服務(wù);家政公司,提供家政服務(wù);房產(chǎn)中介公司,提供,這樣子的話,關(guān)于服務(wù),應(yīng)該是理清楚了。
接下來是服務(wù)的發(fā)現(xiàn),英文是Service Discovery,理解了服務(wù),那么服務(wù)的發(fā)現(xiàn)就應(yīng)該很好理解了,用大白話講就是具有某種能力,可以發(fā)現(xiàn)某些服務(wù),比如生活中的房產(chǎn)中介公司(服務(wù)發(fā)現(xiàn)),他們就能夠發(fā)現(xiàn)很多的擁有空閑房子并且愿意出租的人(服務(wù))。
SPI機(jī)制的作用就是服務(wù)發(fā)現(xiàn),也就是說,我們有一些服務(wù),然后通過SPI機(jī)制,就能讓這些服務(wù)被需要的人所使用,而我們這些服務(wù)被發(fā)現(xiàn)的過程就是SPI的任務(wù)了。
說到這里,可能你還是不太理解SPI是什么,接下來我們通過具體的例子分析來理解SPI。
在JDBC4.0之前,我們使用JDBC去連接數(shù)據(jù)庫的時候,通常會經(jīng)過如下的步驟
- 將對應(yīng)數(shù)據(jù)庫的驅(qū)動加到類路徑中
- 通過
Class.forName()注冊所要使用的驅(qū)動,如Class.forName(com.mysql.jdbc.Driver) - 使用驅(qū)動管理器
DriverManager來獲取連接 - 后面的內(nèi)容我們不關(guān)心了。
這種方式有個缺點(diǎn),加載驅(qū)動是由用戶來操作的,這樣就很容易出現(xiàn)加載錯驅(qū)動或者更換驅(qū)動的時候,忘記更改加載的類了。
在JDBC4.0,現(xiàn)在我們使用的時候,上面的第二步就不需要了,并且能夠正常使用,這個就是SPI的功勞了。
接下來我們先來看下為什么不需要第二步。
熟悉反射的同學(xué)應(yīng)該知道,第二步其實(shí)就是將對應(yīng)的驅(qū)動類加載到虛擬機(jī)中,也就是說,現(xiàn)在我們沒有手動加載,那么對應(yīng)的驅(qū)動類是如何加載到虛擬機(jī)中的呢,我們通過DriverManger的源碼的了解SPI是如何實(shí)現(xiàn)這個功能的。
DriverManager.java
在DriverManager中,有一段靜態(tài)代碼(靜態(tài)代碼在類被加載的時候就會執(zhí)行)
static {
// 在這里加載對應(yīng)的驅(qū)動類
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
接下來我們來具體看下其內(nèi)容
loadInitialDrivers()
private static void loadInitialDrivers() {
String drivers;
try {
// 先獲取系統(tǒng)變量
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// SPI機(jī)制加載驅(qū)動類
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 通過ServiceLoader.load進(jìn)行查找,我們的重點(diǎn)也是這里,后面分析
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 獲取迭代器,也請注意這里
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
// 遍歷迭代器
// 這里需要這么做,是因?yàn)镾erviceLoader默認(rèn)是延遲加載
// 只是找到對應(yīng)的class,但是不加載
// 所以這里在調(diào)用next的時候,其實(shí)就是實(shí)例化了對應(yīng)的對象了
// 請注意這里 -------------------------------------------------------------------- 1
while(driversIterator.hasNext()) {
// 真正實(shí)例化的邏輯,詳見后面分析
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
// 同時加載系統(tǒng)變量中找到的驅(qū)動類
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 由于是系統(tǒng)變量,所以使用系統(tǒng)類加載器,而不是應(yīng)用類加載器
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
從上面的代碼中并沒有找到對應(yīng)的操作邏輯,唯一的一個突破點(diǎn)就是ServiceLoader.load(Driver.class)方法,該方法其實(shí)就是SPI的核心啦
接下來我們來分析這個類的代碼(代碼可能有點(diǎn)長哦,要有心理準(zhǔn)備)
ServiceLoader.java
public final class ServiceLoader<S>
implements Iterable<S>
{
/**
* 由于是調(diào)用ServiceLoader.load(Driver.class)方法,所以我們先從該方法分析
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
// 獲取當(dāng)前的上下文線程
// 默認(rèn)情況下是應(yīng)用類加載器,具體的內(nèi)容稍后分析
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 調(diào)用帶加載器的加載方法
return ServiceLoader.load(service, cl);
}
/**
* 帶類加載器的加載方法
*/
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// 只是返回一哥ServiceLoader對象,調(diào)用自己的構(gòu)造函數(shù)嘛
return new ServiceLoader<>(service, loader);
}
/**
* 私有構(gòu)造函數(shù)
*/
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// 目標(biāo)加載類不能為null
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 獲取類加載器,如果cl是null,則使用系統(tǒng)類加載器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
// 調(diào)用reload方法
reload();
}
// 用于緩存加載的服務(wù)提供者
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 真正查找邏輯的實(shí)現(xiàn)
private LazyIterator lookupIterator;
/**
* reload方法
*/
public void reload() {
// 先清空內(nèi)容
providers.clear();
// 初始化lookupIterator
lookupIterator = new LazyIterator(service, loader);
}
}
LazyIterator.class
LazyIterator是ServiceLoader的私有內(nèi)部類
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
/**
* 私有構(gòu)造函數(shù),用于初始化參數(shù)
*/
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
}
到了上面的內(nèi)容,其實(shí)ServiceLoader.load()方法就結(jié)束了,并沒有實(shí)際上去查找具體的實(shí)現(xiàn)類,那么什么時候才去查找以及加載呢,還記得上面的Iterator<Driver> driversIterator = loadedDrivers.iterator();這一行代碼嗎,這一行代碼用于獲取一個迭代器,這里同樣也沒有進(jìn)行加載,但是,其后面還有遍歷迭代器的代碼,上面標(biāo)注為1的部分。
迭代器以及遍歷迭代器的過程如下所示
ServiceLoader.java
public Iterator<S> iterator() {
return new Iterator<S>() {
// 注意這里的providers,這里就是上面提到的用于緩存
// 已經(jīng)加載的服務(wù)提供者的容器。
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
// 底層其實(shí)委托給了providers
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
// 如果沒有緩存,則查找及加載
return lookupIterator.hasNext();
}
// 同上
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
上面已經(jīng)分析過了,ServiceLoader.load()方法執(zhí)行到LazyIterator的初始化之后就結(jié)束了,真正地查找直到調(diào)用lookupIterator.hasNext()才開始。
LazyIterator.java
// 希望你還記得他
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
//檢查 AccessControlContext,這個我們不關(guān)系
// 關(guān)鍵的核心是都調(diào)用了hasNextService()方法
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);
}
}
private boolean hasNextService() {
// 第一次加載
if (nextName != null) {
return true;
}
// 第一次加載
if (configs == null) {
try {
// 注意這里,獲取了的完整名稱
// PREFIX定義在ServiceLoader中
// private static final String PREFIX = "META-INF/services/"
// 這里可以看到,完整的類名稱就是 META-INF/services/CLASS_FULL_NAME
// 比如這里的 Driver.class,完整的路徑就是
// META-INF/services/java.sql.Driver,注意這個只是文件名,不是具體的類哈
String fullName = PREFIX + service.getName();
// 如果類加載器為null,則使用系統(tǒng)類加載器進(jìn)行加載
// 類加載會加載指定路徑下的所有類
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else // 使用傳入的類加載器進(jìn)行加載,其實(shí)就是應(yīng)用類加載器
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 如果pending為null或者沒有內(nèi)容,則進(jìn)行加載,一次只加載一個文件的一行
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 解析讀取到的每個文件,高潮來了
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
/**
* 解析讀取到的每個文件
*/
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
// utf-8編碼
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
// 一行一行地讀取數(shù)據(jù)
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
// 返回迭代器
return names.iterator();
}
// 解析一行行的數(shù)據(jù)
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
String ln = r.readLine();
if (ln == null) {
return -1;
}
// 查找是否存在#
// 如果存在,則剪取#前面的內(nèi)容
// 目的是防止讀取到#及后面的內(nèi)容
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
// 不能包含空格及制表符\t
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
// 檢查第一個字符是否是Java語法規(guī)范的單詞
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
// 檢查每個字符
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
// 如果緩存中沒有,并且當(dāng)前列表中也沒有,則加入列表。
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
/**
* 上面解析完文件之后,就開始加載文件的內(nèi)容了
*/
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 {
// 實(shí)例化并且將其轉(zhuǎn)化為對應(yīng)的接口或者父類
S p = service.cast(c.newInstance());
// 將其放入緩存中
providers.put(cn, p);
// 返回當(dāng)前實(shí)例
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
}
到此,解析的步驟就完成了,在一開始的DriverManager中,我們也看到了在DriveirManager中一直在調(diào)用next方法,也就是持續(xù)地加載找到的所有的Driver的實(shí)現(xiàn)類了,比如MySQL的驅(qū)動類,Oracle的驅(qū)動類啦。
這個例子有點(diǎn)長,但我們收獲還是很多,我們知道了JDBC4不用手動加載驅(qū)動類的實(shí)現(xiàn)原理,其實(shí)就是通過ServiceLoader去查找當(dāng)前類加載器能訪問到的目錄下的WEB-INF/services/FULL_CLASS_NAME文件中的所有內(nèi)容,而這些內(nèi)容由一定的規(guī)范,如下
- 每行只能寫一個全類名
-
#作為注釋 - 只能使用utf-8及其兼容的編碼
- 每個實(shí)現(xiàn)類必須提供一個無參構(gòu)造函數(shù),因?yàn)槭侵苯邮褂?code>class.newInstance()來創(chuàng)建實(shí)例的嘛
由此我們也明白了SPI機(jī)制的工作原理,那么這個東西有什么用呢,其實(shí)JDBC就是個最好的例子啦,這樣用戶就不需要知道到底是要加載哪個實(shí)現(xiàn)類,一方面是簡化了操作,另一方面避免了操作的錯誤,當(dāng)然,這種一般是用于寫框架之類的用途,用于向框架使用者提供更加便利的操作,比如上面的引導(dǎo)我看到SPI的例子,其實(shí)是來自一個RPC框架,通過SPI機(jī)制,讓我們可以直接編寫自定義的序列化方式,然后由框架來負(fù)責(zé)加載即可。
SPI實(shí)戰(zhàn)小案例
上面學(xué)習(xí)完了SPI的例子,也學(xué)習(xí)完了JDBC是如何實(shí)現(xiàn)的,接下來我們來通過一個小案例,來動手實(shí)踐一下SPI是如何工作的。
新建一個接口,內(nèi)容隨便啦
HelloServie.java
public interface HelloService {
void sayHello();
}
然后編寫其實(shí)現(xiàn)類
HelloServiceImpl.java
public class HelloServiceImpl implements HelloService {
@Override
public void sayHello() {
System.out.println("hello world");
}
}
關(guān)鍵點(diǎn)來了,既然是學(xué)習(xí)SPI,那么我們肯定不是手動new一個實(shí)現(xiàn)類啦,而是通過SPI的機(jī)制來加載,如果認(rèn)真地看完上面的分析,那么下面的內(nèi)容應(yīng)該很容易看懂啦,如果沒看懂,再回去看一下啦。
-
在實(shí)現(xiàn)類所在項(xiàng)目(這里是同個項(xiàng)目哈)的類路徑下,如果是maven項(xiàng)目,則是在resources目錄下
- 建立目錄
META-INF/services - 建立文件cn.xuhuanfeng.spi.HelloService(接口的全限定名哈)
- 建立目錄
內(nèi)容是實(shí)現(xiàn)類的類名:cn.xuhuanfeng.spi.impl.HelloServiceImpl(注意這里我們直接放在同個項(xiàng)目,不是同個項(xiàng)目也可以的!?。?
-
自定義一個加載的類,并且通過ServiceLoader.load()方法進(jìn)行加載,如下所示
public class HelloServiceFactory { public HelloService getHelloService() { ServiceLoader<HelloService> load = ServiceLoader.load(HelloService.class); return load.iterator().next(); } } 測試一下,enjoy :)
如果你有興趣的話,可以嘗試將實(shí)現(xiàn)放在另一個項(xiàng)目中,然后打包成jar包,再放置在測試項(xiàng)目的classpath中,enjoy :)
總結(jié)
本小節(jié)我們主要學(xué)習(xí)了SPI,主要包括了SPI是什么,JDBC4中不需要手動加載驅(qū)動類的原理,并且詳細(xì)看了DriverManager中的代碼實(shí)現(xiàn),最后,通過一個簡單的小案例來實(shí)現(xiàn)我們自己的SPI服務(wù),通過這個小節(jié),應(yīng)該說,SPI的大部分內(nèi)容我們是掌握了,當(dāng)然,里面管理類加載器部分我們還沒有學(xué)習(xí),這里先挖個坑,后面有時間再分析一下。