SPI機制
SPI的全名為Service Provider Interface.大多數(shù)開發(fā)人員可能不熟悉悬槽,因為這個是針對廠商或者插件的。在java.util.ServiceLoader
的文檔里有比較詳細的介紹。簡單的總結(jié)下java spi機制的思想光稼。我們系統(tǒng)里抽象的各個模塊枷踏,往往有很多不同的實現(xiàn)方案,比如日志模塊的方案惑折,xml解析模塊、jdbc模塊的方案等枯跑。面向的對象的設(shè)計里惨驶,我們一般推薦模塊之間基于接口編程,模塊之間不對實現(xiàn)類進行硬編碼敛助。一旦代碼里涉及具體的實現(xiàn)類粗卜,就違反了可拔插的原則,如果需要替換一種實現(xiàn)纳击,就需要修改代碼续扔。為了實現(xiàn)在模塊裝配的時候能不在程序里動態(tài)指明,這就需要一種服務(wù)發(fā)現(xiàn)機制评疗。 java spi就是提供這樣的一個機制:為某個接口尋找服務(wù)實現(xiàn)的機制测砂。有點類似IOC的思想,就是將裝配的控制權(quán)移到程序之外百匆,在模塊化設(shè)計中這個機制尤其重要砌些。
SPI具體約定
java spi的具體約定為:當服務(wù)的提供者,提供了服務(wù)接口的一種實現(xiàn)之后,在jar包的META-INF/services/目錄里同時創(chuàng)建一個以服務(wù)接口命名的文件存璃。該文件里就是實現(xiàn)該服務(wù)接口的具體實現(xiàn)類仑荐。而當外部程序裝配這個模塊的時候,就能通過該jar包META-INF/services/里的配置文件找到具體的實現(xiàn)類名纵东,并裝載實例化粘招,完成模塊的注入。 基于這樣一個約定就能很好的找到服務(wù)接口的實現(xiàn)類偎球,而不需要再代碼里制定洒扎。jdk提供服務(wù)實現(xiàn)查找的一個工具類:java.util.ServiceLoader
應(yīng)用場景
- common-logging:apache最早提供的日志的門面接口。只有接口衰絮,沒有實現(xiàn)袍冷。具體方案由各提供商實現(xiàn), 發(fā)現(xiàn)日志提供商是通過掃描
META-INF/services/org.apache.commons.logging.LogFactory
配置文件猫牡,通過讀取該文件的內(nèi)容找到日志提工商實現(xiàn)類胡诗。只要我們的日志實現(xiàn)里包含了這個文件,并在文件里制定LogFactory
工廠接口的實現(xiàn)類即可淌友。 - JDBC:jdbc4.0以前煌恢, 開發(fā)人員還需要基于
Class.forName("xxx")
的方式來裝載驅(qū)動,jdbc4也基于spi的機制來發(fā)現(xiàn)驅(qū)動提供商了震庭,可以通過META-INF/services/java.sql.Driver
文件里指定實現(xiàn)類的方式來暴露驅(qū)動提供者.
SPI的使用
首先瑰抵,創(chuàng)建一個緩存數(shù)據(jù)源接口:
public interface CacheDataSource {
String getDataSource();
}
接下來創(chuàng)建了兩個實現(xiàn)類:
public class DatabaseCacheDataSource implements CacheDataSource {
@Override
public String getDataSource() {
System.out.println(this.getClass().getClassLoader());
System.out.println("DatabaseCacheDataSource");
return null;
}
}
public class MemoryCacheDataSource implements CacheDataSource {
@Override
public String getDataSource() {
System.out.println(this.getClass().getClassLoader());
System.out.println("MemoryCacheDataSource");
return null;
}
}
最后我們在META-INF/services目錄下新建一個文件,名稱為接口的全限定名归薛,即包名加類名谍憔,內(nèi)容為實現(xiàn)類的全限定名。例如本例中的文件名稱為cn.ideabuffer.spi.CacheDataSource
主籍,內(nèi)容如下:
cn.ideabuffer.spi.MemoryCacheDataSource
新建一個測試類:
public class Main {
public static void main(String[] args) {
ServiceLoader<CacheDataSource> s = ServiceLoader.load(CacheDataSource.class);
Iterator<CacheDataSource> it = s.iterator();
for (;it.hasNext();){
CacheDataSource cacheDataSource = it.next();
cacheDataSource.getDataSource();
}
}
}
這里通過ServiceLoader來獲取具體的實現(xiàn)類,執(zhí)行后的輸出如下:
sun.misc.Launcher$AppClassLoader@2503dbd3
MemoryCacheDataSource
我們查看一下ServiceLoader中的load方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
這里使用了Context Class Loader逛球,為什么要這么使用呢千元,為什么不直接使用系統(tǒng)類加載器呢?接下來我們具體分析一下颤绕。
線程上下文類加載器
線程上下文類加載器(context class loader)是從JDK 1.2開始引入的幸海。類java.lang.Thread
中的方法 getContextClassLoader()
和 setContextClassLoader(ClassLoader cl)
用來獲取和設(shè)置線程的上下文類加載器。
如果沒有通過 setContextClassLoader(ClassLoader cl)
方法進行設(shè)置的話奥务,線程將繼承其父線程的上下文類加載器物独。Java 應(yīng)用運行的初始線程的上下文類加載器是系統(tǒng)類加載器。在線程中運行的代碼可以通過此類加載器來加載類和資源氯葬。
為了加載類挡篓,Java還提供了很多服務(wù)提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實現(xiàn)官研。那類加載就會存在問題:SPI 的接口是 Java 核心庫的一部分秽澳,是由引導類加載器來加載的;SPI 實現(xiàn)的 Java 類一般是由系統(tǒng)類加載器來加載的戏羽。引導類加載器是無法找到 SPI 的實現(xiàn)類的担神,因為它只加載 Java 的核心庫。在 SPI 接口的代碼中使用線程上下文類加載器始花,就可以成功的加載到 SPI 實現(xiàn)的類妄讯。java的雙親委托類加載機制(ClassLoader A -> System class loader -> Extension class loader -> Bootstrap class loader)可以保證核心類的正常安全加載。但是右邊的 Bootstrap class loader 所加載的代碼需要反過來去找委派鏈靠左邊的 ClassLoader A 去加載東西的時候酷宵,就需要委派鏈左邊的 ClassLoader 設(shè)置為線程的上下文加載器即可捞挥。
線程上下文類加載器的應(yīng)用
查看mysql-connector中的Driver方法如下:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
mysql-connector中的Driver實現(xiàn)了java.sql.Driver
接口,那么查看mysql-connector中的META-INF/services
目錄可以看到忧吟,有一個名為java.sql.Driver
的文件砌函,文件的內(nèi)容為:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
下面看一下java.sql.DriverManager
,在Driver的靜態(tài)代碼塊中將Driver注冊到了DriverManager中溜族,DriverManager依賴了Driver讹俊,但通過斷點查看,DriverManager是通過Bootstrap類加載器加載的煌抒,所以要加載Driver的實現(xiàn)類必須通過其他的類加載器仍劈,那么這時,Context ClassLoader的出現(xiàn)寡壮,解決了這一問題贩疙,也就是上文說過的ServiceLoader中的load方法。