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ī)制詳解