詳解Java中的SPI技術(shù)以及在架構(gòu)設(shè)計的運用
衡量一個架構(gòu)設(shè)計的好壞,其中一個標(biāo)準就是看這個架構(gòu)是否具有可擴展性,架構(gòu)設(shè)計中有很多常用的實現(xiàn)擴展性的技術(shù),這次我們就來探討一下比較常見的 SPI 技術(shù)。
我們首先了解一下什么是 SPI,然后講一講 JDK 是如何基于 SPI 機制來獲取到具體的數(shù)據(jù)庫驅(qū)動實現(xiàn)的。接下來,我們分析 JDK SPI 機制的不足之處。最后,概要講解一下 Apache Dubbo 對 SPI 進行了哪些改進,以及 Apache Dubbo 是如何基于增強 SPI 實現(xiàn) Dubbo 框架的可擴展性的。
服務(wù)提供者接口(Service provider interface,SPI),是指被第三方實現(xiàn)或者擴展的接口,它可以用來實現(xiàn)框架的擴展性和實現(xiàn)組件的可替換性。這里服務(wù)提供者接口中的服務(wù)是指一組接口和抽象類,服務(wù)提供者基于服務(wù)提供者接口來實現(xiàn)具體的服務(wù)。
JDK 中的 SPI 機制
了解了 SPI 的基本定義,我們接下來看一下 SPI 是如何在 JDK 中使用的。在 Java 開發(fā)中,我們經(jīng)常使用下面這段代碼來獲取一個數(shù)據(jù)庫連接。
DriverManager.getConnection("a database url of the form jdbc:subprotocol:subnam");
比如獲取 MySQL 數(shù)據(jù)庫連接,我們可以用如下代碼來操作:
DriverManager.getConnection("jdbc:mysql://localhost:3306/testDb?useUnicode=true&characterEncoding=UTF-8");
或者要獲取 Oracle 數(shù)據(jù)庫連接,對應(yīng)代碼如下所示:
DriverManager.getConnection("jdbc:oracle:thin:@localhost:3306:testDb");
獲取完數(shù)據(jù)庫連接后,我們該怎么用呢?
基于 MySQL 數(shù)據(jù)庫的示例,下面這段代碼就展示了如何基于連接做數(shù)據(jù)庫表的操作:
public static void main(String[] argc) throws SQLException {
Connection con = null;
try {
//1. 獲取 MySQL 數(shù)據(jù)庫連接
con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/testDb?useUnicode=true&characterEncoding=UTF-8");
//2. 執(zhí)行 SQL 語句
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM table");
//3. 處理結(jié)果集數(shù)據(jù)
while (rs.next()) {
String name = rs.getString("name");
String desc = rs.getString("desc");
System.out.println(name + ", " + desc);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
//4. 關(guān)閉連接
if (con != null) {
con.close();
}
}
我們先獲取到 MySQL 數(shù)據(jù)庫的一個連接,然后基于連接執(zhí)行查詢操作,接著處理查詢操作返回的數(shù)據(jù)集,處理完畢后關(guān)閉連接。
從上面示例可知,DriverManager.getConnection 方法根據(jù)傳遞的 database url 不同,可以獲取不同數(shù)據(jù)庫的連接,也就是說 DriverManager.getConnection 方法是與具體的數(shù)據(jù)庫驅(qū)動實現(xiàn)無關(guān)的。這是一個很好的設(shè)計,那么它是如何實現(xiàn)的呢?
首先,我們來剖析下 DriverManager.getConnection 的實現(xiàn)機制,我們列出來 DriverManager.getConnection 相關(guān)的類圖模型:
java.sql.Driver 文件的內(nèi)容如下圖:
圖片
Oracle 驅(qū)動包中 META-INF/services/java.sql.Driver 文件的內(nèi)容如下所示:
圖片
從 META-INF/services/java.sql.Driver 文件找到具體驅(qū)動的實現(xiàn)類的名稱后,會調(diào)用 ServiceLoader 內(nèi)的 nextService 方法,使用 Class.forName(“驅(qū)動實現(xiàn)類名稱”…)來創(chuàng)建這個驅(qū)動的 Class 對象,然后通過 Class 對象的 newInstance() 方法創(chuàng)建一個驅(qū)動實現(xiàn)類的實例對象。
private S nextService() {
...
// 創(chuàng)建驅(qū)動實現(xiàn)類的 Class 對象
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
...
// 基于驅(qū)動實現(xiàn)類的 Class 對象創(chuàng)建一個實例對象
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
...
}
}
另外,我們會發(fā)現(xiàn)具體的驅(qū)動實現(xiàn)類,比如 MySQL 驅(qū)動的 Driver 類內(nèi),存在一個 static 的代碼塊。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
// 注冊 MySQL 驅(qū)動到 DriverManager
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
這個 static 代碼塊會在創(chuàng)建 Driver 的實例對象時被觸發(fā)執(zhí)行,而上面 ServiceLoader 類的 nextService 方法內(nèi)就創(chuàng)建了 Driver 的實例,所以觸發(fā)了 Driver 類的 static 代碼塊執(zhí)行,也就是把 Driver 類注冊到了 DriverManager 中的 registeredDrivers 列表里面。
到這里,我們講解了 DriverManager.getConnection 內(nèi)的一部分邏輯,也就是 loadInitialDrivers 方法的邏輯。它的內(nèi)部使用 ServiceLoader 掃描 classpath 下所有的 jar 包,并找到實現(xiàn) Driver 接口的驅(qū)動包,然后注冊驅(qū)動實現(xiàn)類到 DriverManager。
上面我們講解了如何注冊驅(qū)動到 DriverManager,下面我們繼續(xù)看當(dāng) DriverManager.getConnection 獲取數(shù)據(jù)庫連接時,如何使用驅(qū)動來具體獲取數(shù)據(jù)庫連接的:
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException{}
...
// 遍歷 registeredDrivers
for(DriverInfo aDriver : registeredDrivers) {
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
// 從驅(qū)動獲取連接
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
}
...
}
...
}
}
由上面代碼可知,getConnection 方法內(nèi)會遍歷 registeredDrivers 中的驅(qū)動實現(xiàn)類,然后調(diào)用驅(qū)動實現(xiàn)類的 connect 方法,每個驅(qū)動實現(xiàn)類的 connect 方法根據(jù) URL 來判斷當(dāng)前請求的 URL 是否需要自己處理,如果不需要就返回 null,否則返回具體的連接對象。
總的來說,JDK 對數(shù)據(jù)庫驅(qū)動進行了抽象,提供了 SPI 接口 Driver 和 Connection。然后,驅(qū)動開發(fā)者就可以實現(xiàn)這個 SPI 接口,來提供具體數(shù)據(jù)庫的驅(qū)動實現(xiàn)包。驅(qū)動開發(fā)者提供的驅(qū)動包里面需要包含 META-INF/services/java.sql.Driver 文件,并且文件內(nèi)要寫入驅(qū)動實現(xiàn)類的類名。
JDK 提供的 ServiceLoader 機制會掃描 classpath 下的所有 jar 包,并且找到含有 META-INF/services/java.sql.Driver 文件的 jar,判定它為數(shù)據(jù)庫驅(qū)動包,然后 ServiceLoader 會根據(jù)實現(xiàn)類的名稱實例化這個驅(qū)動實現(xiàn)類,并注冊驅(qū)動實現(xiàn)類到 DriverManager 內(nèi)。當(dāng)我們調(diào)用 DriverManager 的 getConnection 方法時,就可以獲取到具體的驅(qū)動實現(xiàn)類并獲取數(shù)據(jù)庫連接了。
請注意,在 JDK 的 SPI 機制實現(xiàn)中,ServiceLoader 會把所有驅(qū)動實現(xiàn)包中的驅(qū)動實現(xiàn)類都實例化(創(chuàng)建一個對應(yīng)的實例對象)。如果某些驅(qū)動實現(xiàn)類初始化很耗時,實例化會很浪費資源,并且會降低應(yīng)用啟動速度。
Dubbo 中的 SPI 機制
Apache Dubbo 是一款微服務(wù)框架,為大規(guī)模微服務(wù)實踐提供高性能 RPC 通信、流量治理、可觀測性等解決方案。
Dubbo 的 SPI 實現(xiàn)借鑒了 JDK 的 SPI 思想,但是進行了一些優(yōu)化改進,解決了 JDK SPI 的以下問題:
- JDK SPI 會一次性實例化所有實現(xiàn)類,有的擴展實現(xiàn)類初始化很耗時,但實際又沒用,還會拖慢啟動速度;
- JDK SPI 在實例化擴展實現(xiàn)類失敗時,不會友好地通知用戶具體異常。
Dubbo SPI 增加了對擴展實現(xiàn)類的 IoC 和 AOP 的支持,一個擴展實現(xiàn)類可以直接注入其它擴展實現(xiàn)類,也可以使用 Wrapper 類對擴展實現(xiàn)類進行功能增強。
Dubbo 框架的實現(xiàn)采用了分層架構(gòu)思想,架構(gòu)中的每層都是一個獨立模塊,上層依賴下層提供的功能,下層對上層提供服務(wù),下層的改變對上層不可見。
圖片
在這個架構(gòu)圖中,從上往下看,除去 Service 和 Config 層是 API 層外,剩下的從 Proxy 層到 Serialize 層都是 SPI 層,這意味著從 Proxy 層到 Serialize 層每層都是可擴展的、可被替換的。
比如,Cluster 層默認提供了豐富的集群容錯策略,但是如果開發(fā)者有定制化需求,可以通過 Dubbo 提供的 SPI 擴展接口 org.apache.dubbo.rpc.cluster.Cluster 提供個性化的集群容錯策略,其中 SPI 接口 org.apache.dubbo.rpc.cluster.Cluster 的定義如下:
@SPI("failover")
public interface Cluster {
@Adaptive
<T> Invoker<T> join(Directory<T> var1) throws RpcException;
}
我們通過 CustomCluster 類實現(xiàn)了 SPI 接口——Cluster,其中 CustomClusterInvoker 為具體的容錯策略的實現(xiàn)。
public class CustomCluster implements Cluster {
@Override
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return new CustomClusterInvoker<>(directory);
}
}
創(chuàng)建好 CustomCluster 類后,我們需要在 resources 目錄下創(chuàng)建一個名稱為 META-INF.dubbo 的文件夾,然后在它的下面創(chuàng)建一個名為 org.apache.dubbo.rpc.cluster.Cluster 的文件,文件內(nèi)容為:customCluster=org.apache.dubbo.demo.cluster.CustomCluster。
圖片
最后,我們在消費端發(fā)起請求時,可以設(shè)置集群容錯策略。
// 0.創(chuàng)建服務(wù)引用對象實例
ReferenceConfig<GreetingService> referenceConfig = new ReferenceConfig<GreetingService>();
// 1.設(shè)置應(yīng)用程序信息
referenceConfig.setApplication(new ApplicationConfig("first-dubbo-consumer"));
// 2.設(shè)置服務(wù)注冊中心
referenceConfig.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));
// 3.設(shè)置服務(wù)接口和超時時間
referenceConfig.setInterface(GreetingService.class);
// 4.設(shè)置集群容錯策略
referenceConfig.setCluster("customCluster");
// 5.設(shè)置服務(wù)分組與版本
referenceConfig.setVersion("1.0.0");
referenceConfig.setGroup("dubbo");
// 6.引用服務(wù)
greetingService = referenceConfig.get();
代碼 4 就是我們設(shè)置的集群容錯策略——customCluster。你可能會問,Dubbo 如何根據(jù)集群容錯策略的名稱——customCluster 找到具體的容錯策略實現(xiàn)類呢?其實就是通過 Dubbo 的增強 SPI 機制來實現(xiàn)的,這個機制和 JDK SPI 機制差不多。
總結(jié)
圖片
今天我們首先學(xué)習(xí)了 SPI 的定義,然后基于 JDK 中數(shù)據(jù)庫驅(qū)動的例子,重點講解了如何基于 SPI 來實現(xiàn)數(shù)據(jù)庫驅(qū)動的擴展性。JDK 對數(shù)據(jù)庫驅(qū)動進行了抽象,提供了抽象的 Driver 和 Connection 接口,這些接口就是 SPI 接口。
具體的驅(qū)動包實現(xiàn)者可以實現(xiàn)這些 SPI 接口來實現(xiàn)具體數(shù)據(jù)庫驅(qū)動。JDK 通過使用 ServiceLoader 機制來掃描驅(qū)動包的實現(xiàn)類,并注冊這些驅(qū)動到 DriverManager,所以我們可以通過 DriverManager.getConnection 方法獲取數(shù)據(jù)庫連接。
接下來,我們了解了 JDK SPI 的不足,概要介紹了 Dubbo 中增強的 SPI 的特點以及 Dubbo 如何基于 SPI 實現(xiàn)可擴展性。最后,我們基于 Dubbo 的集群容錯策略擴展接口,講解了 Dubbo 中如何來實現(xiàn)擴展。