SPI簡(jiǎn)介
SPI,Service Provider Interface,主要是被框架的開(kāi)發(fā)人員使用,比如java.sql.Driver接口,其他不同廠商可以針對(duì)同一接口做出不同的實(shí)現(xiàn),mysql和postgresql都有不同的實(shí)現(xiàn)提供給用戶,而Java的SPI機(jī)制可以為某個(gè)接口尋找服務(wù)實(shí)現(xiàn)。
當(dāng)服務(wù)的提供者提供了一種接口的實(shí)現(xiàn)之后,需要在classpath下的META-INF/services/目錄里創(chuàng)建一個(gè)以服務(wù)接口命名的文件,這個(gè)文件里的內(nèi)容就是這個(gè)接口的具體的實(shí)現(xiàn)類。當(dāng)其他的程序需要這個(gè)服務(wù)的時(shí)候,就可以通過(guò)查找這個(gè)jar包(一般都是以jar包做依賴)的META-INF/services/中的配置文件,配置文件中有接口的具體實(shí)現(xiàn)類名,可以根據(jù)這個(gè)類名進(jìn)行加載實(shí)例化,就可以使用該服務(wù)了。JDK中查找服務(wù)的實(shí)現(xiàn)的工具類是:java.util.ServiceLoader。
SPI可以認(rèn)為是一個(gè)規(guī)范,按照此規(guī)范實(shí)施,就能被服務(wù)正確識(shí)別自定義的功能。
Dubbo,
如何使用SPI
使用SPI的大致流程如下:
- 有關(guān)組織或者公司定義標(biāo)準(zhǔn)。
- 具體廠商或者框架開(kāi)發(fā)者實(shí)現(xiàn)。
- 程序猿使用。
詳細(xì)來(lái)講,就是:
標(biāo)準(zhǔn)制定者的工作:
- 使用
ServiceLoader.load(Class class);動(dòng)態(tài)加載Service接口的實(shí)現(xiàn)類。
具體實(shí)施者的工作:
- 在META-INF/services/目錄中創(chuàng)建以Service接口全限定名命名的文件,該文件內(nèi)容為Service接口具體實(shí)現(xiàn)類的全限定名,文件編碼必須為UTF-8。
- 如SPI的實(shí)現(xiàn)類為jar,則需要將其放在當(dāng)前程序的classpath下。
- Service的具體實(shí)現(xiàn)類必須有一個(gè)不帶參數(shù)的構(gòu)造方法。
應(yīng)用舉例
這里以JDBC為例
在JDBC4.0之前,我們開(kāi)發(fā)有連接數(shù)據(jù)庫(kù)的時(shí)候,通常會(huì)用Class.forName("com.mysql.jdbc.Driver")這句先加載數(shù)據(jù)庫(kù)相關(guān)的驅(qū)動(dòng),然后再進(jìn)行獲取連接等的操作。而JDBC4.0之后不需要用Class.forName("com.mysql.jdbc.Driver")來(lái)加載驅(qū)動(dòng),直接使用DriverManager.getConnection(String url)就可以了。
注:
在以前,操作數(shù)據(jù)庫(kù)的時(shí)候,需要先加載驅(qū)動(dòng)。不同的數(shù)據(jù)庫(kù)需要不同的驅(qū)動(dòng)。如:
- mysql -
Class.forName("com.mysql.jdbc.Driver").newInstance()- oracle -
Class.forName("oracle.jdbc.driver.OracleDriver").newInstance()- sql server -
Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver").newInstance()- postgreSQL -
Class.forNaem("org.postgresql.Driver").newInstance()- db2 -
Class.froName("com.ibm.db2.jdbc.app.DB2Driver").newInstance()如果要更改數(shù)據(jù)庫(kù),就需要更改驅(qū)動(dòng),也就是說(shuō)要修改代碼。修改代碼是個(gè)很麻煩的工作。使用SPI可以使我們無(wú)需修改代碼就可以更換數(shù)據(jù)庫(kù)。
通過(guò)SPI是如何實(shí)現(xiàn)的呢?
1. 組織方制定接口
這里的組織方是JDK組織。JDK中有定義驅(qū)動(dòng)接口Driver:
public interface Driver{
//接口方法略。。。
}
2. 實(shí)現(xiàn)方根據(jù)SPI規(guī)范實(shí)現(xiàn)接口
這里的實(shí)現(xiàn)方是各數(shù)據(jù)庫(kù)廠商
msyql
對(duì)于MySQL,在mysql-connector-java.jar下,有META-INF/services目錄,該目錄下有SPI需要的文件java.sql.Driver:

文件內(nèi)容為:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
postgreSQL
同理,對(duì)于postgreSQL,在postgresql.jar中,META-INF/services目錄下也有名為java.sql.Driver的文件:

文件內(nèi)容為:
org.postgresql.Driver
其他數(shù)據(jù)庫(kù)與之類似。具體接口實(shí)現(xiàn)略過(guò)。
3. 組織方加載實(shí)現(xiàn)類
各個(gè)數(shù)據(jù)庫(kù)驅(qū)動(dòng)的加載是在JDK的DriverManager類中實(shí)現(xiàn)的。該類中有如下一段靜態(tài)代碼,會(huì)在類初始化的時(shí)候執(zhí)行:
class DriverManager{
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
loadInitialDrivers()函數(shù)的內(nèi)容為:
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;
}
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;
}
});
//下面代碼略。。。
}
這段代碼中,ServiceLoader.load()會(huì)查找classpath以及jar包中,META-INF/services目錄下的所有java.sql.Driver的實(shí)現(xiàn)類,并實(shí)例化。
使用SPI的第一步操作如下。這里沒(méi)有去META-INF/services目錄下查找配置文件,也沒(méi)有加載具體實(shí)現(xiàn)類,做的事情就是封裝了我們的接口類型和類加載器,并初始化了一個(gè)迭代器。
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
第二步操作為:遍歷使用SPI獲取到的具體實(shí)現(xiàn),實(shí)例化各個(gè)實(shí)現(xiàn)類
//獲取迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍歷所有的驅(qū)動(dòng)實(shí)現(xiàn)
while(driversIterator.hasNext()) {
driversIterator.next();
}
在遍歷的時(shí)候,首先調(diào)用driversIterator.hasNext()方法,這里會(huì)搜索classpath下以及jar包中所有的META-INF/services目錄下的java.sql.Driver文件,并找到文件中的實(shí)現(xiàn)類的名字,此時(shí)并沒(méi)有實(shí)例化具體的實(shí)現(xiàn)類。
第三步為:實(shí)例化各個(gè)驅(qū)動(dòng)。關(guān)鍵代碼為:
driversIterator.next();
到此為止,classpath下的所有數(shù)據(jù)庫(kù)驅(qū)動(dòng)都被加載并實(shí)例化了。
在測(cè)試項(xiàng)目中添加了兩個(gè)jar包,mysql-connector-java-6.0.6.jar和postgresql-42.0.0.0.jar,跟蹤到DriverManager中之后:

可以看到此時(shí)迭代器中有兩個(gè)驅(qū)動(dòng),mysql和postgresql的都被加載了。有關(guān)兩個(gè)驅(qū)動(dòng)都加載了,具體使用哪個(gè)驅(qū)動(dòng),請(qǐng)自行深入jdbc的源碼。這里不做過(guò)多解析。
誰(shuí)使用了SPI
有很多的SPI擴(kuò)展機(jī)制應(yīng)用的實(shí)例,比如大名鼎鼎的slf4j, common-logging, JDBC, tomcat等等
SPI的思想
SPI重要的不是其原理,而是其思想。
Dubbo就借鑒了SPI的思想,實(shí)現(xiàn)了自動(dòng)擴(kuò)展。詳情可參考下面這篇文章。
Dubbo中SPI擴(kuò)展機(jī)制詳解