面試官:你說你用過Dubbo,那你說說看Dubbo的SPI 寫在最後

啥是SPI

SPI,全稱爲 Service Provider Interface,是Java提供的一套用來被第三方實現或者擴展的API,它可以用於很多框架的擴展,常見的如Java JDBC、Spring、SpringBoot、Dubbo等

Java SPI

Java SPI由Java核心庫提供,它定義了一個接口,具體的實現由第三方來實現。以Java JDBC Driver爲例,常用的關係型數據有Mysql、Oracle、SQLServer、DB2等,這些不同類型的數據庫使用的驅動程序各不相同,那JDK不可能把所有廠商的驅動都實現,只能制定一個標準接口,各個數據庫廠商根據標準來實現自己的驅動,這就是SPI機制

寫個例子

Java SPI實現起來比較簡單,都是約定俗成的事,按他的標準來就行,在實現Java SPI需要滿足以下條件

  • 寫一個接口,並且提供對應的具體的實現
  • 在META-INF/service/目錄下新建一個以接口的全限定名命名的文件,如jdbc.sql.Driver
  • 使用ServiceLoader.load()方法進行加載

下面開始擼代碼

1、定義一個接口

2、實現接口

3、加配置

在META-INF/service/目錄下新建文件org.kxg.spi.UserService,內容如下:

org.kxg.spi.PCUserLoginServiceImpl
org.kxg.spi.AppUserLoginServiceImpl 

4、使用ServiceLoader加載

運行代碼,結果如下:

PC User Login!
APP User Login! 

可以看到Java SPI實現起來還是挺簡單的,下面看看Java SPI具體是如何工作的

Java SPI工作原理

1、從ServiceLoader入手

主要是load()方法進行類的加載

代碼也挺簡單的,最終創建了一個LazyIterator對象,可能看到這裏還是沒有明白到底ServiceLoader是如何進行加載的,別急我們繼續看

從上面的代碼我們可以看到ServiceLoader是實現了Iterable接口的,這也是ServiceLoader可以被循環迭代的原因

public final class ServiceLoader<S> implements Iterable<S> 

實現Iterable接口的類在循環迭代時會調用iterator()方法,也就是說ServiceLoader在循環迭代的時候會調用自身的iterator()方法,下面看一下代碼

可以看到iterator()方法中的主要兩個方法hasNext()和next()調用的是lookupIterator的hasNext()和next()方法,這樣就跟前面的對上了,前面我們創建了一個LazyIterator對象

下面看一下lookupIterator的hasNext()和next()方法

這裏可以看到主要是hasNextService()、nextService()這兩個方法

代碼跟到這裏,邏輯基本上已經清楚了,hasNextService()主要完成目錄下文件內容的加載,nextService()方法主要作用是用於類的實例化

Dubbo SPI

既然Java已經提供了SPI,爲啥Dubbo還要自己實現一套SPI,難道是爲了炫技?No,阿里的大神們沒這麼無聊,從上面的例子可以看出來,Java SPI有個缺點:掃描META-INF/service/目錄,加載目錄下所有的service,不管你有沒有使用到,假如遇到一個加載時間很長的service,會影響整體的加載效率,而加載了可能又不使用,純粹是浪費資源

Dubbo在Java SPI的基礎上對其進行加強,使用Key-Value的形式來標識各個Service,並且增加了對擴展點的IOC和AOP支持

首先我們來看一下dubbo SPI的配置

以Protocol爲例

可以看到dubbo同樣以全限定名來命名文件,跟Java SPI保持一致,文件內容與Java SPI不一致,它是以Key=Value的形式來配置的,每個實現都指定了一個名稱

registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
rest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocol 

下面我們通過一個例子來自定義實現一個Protocol

在META-INF/dubbo/目錄下新增com.alibaba.dubbo.rpc.Protocol文件,內容爲:

myProtocol=org.kxg.dubbo.MyProtocol   ## 此路徑爲MyProtocol的包名+類名 

寫個main方法測試一下

輸出結果

9999 

可以看到Dubbo的SPI其實也挺簡單的,下面我們從源碼的角度看看Dubbo的SPI是如何實現的

Dubbo SPI源碼分析

從上面的例子可以看出來,Dubbo主要是通過ExtensionLoader來進行加載的,具體是通過getExtensionLoader方法獲取ExtensionLoader,再通過ExtensionLoader調用getExtension來獲取具體的對象的,下面我們一步步來看

下面來看一下getExtension方法

以上整個SPI的大致流程,還是挺清晰的,簡單畫個圖描述一下

getExtensionClasses

在上面獲取對應的class時,有一個重要的方法getExtensionClasses,它主要是在固定的幾個目錄下去加載配置文件,讀取相應的擴展類

loadFile的代碼還是挺簡單的,從指定的目錄中讀取配置,因爲配置是鍵值對的形式,所以中間會有對應的處理,有興趣可以自己讀一下源碼

Adaptive自適應擴展點

到底啥是Dubbo的自適應擴展點呢?就是我們會根據配置進行加載擴展點,但有時候我們不希望它自動加載,我們希望通過自己傳入的參數來進行動態的加載,這就是Dubbo的自適應擴展點,下面我們看一個例子

運行結果:

class com.alibaba.dubbo.common.compiler.support.AdaptiveCompiler 

我們查看AdaptiveCompiler類的源碼,可以看到它上面標記了@Adaptive註解,這就是Dubbo的自適應擴展點註解

@Adaptive註解

@Adaptive註解可以標註在類和方法上,兩者的區別是:標註在類上說明這個類是自適應擴展點的類,標註在方法上需要動態生成字節碼,然後動態生成類

@Adaptive註解在類上

還是上面的Complie例子,我們跟一下源碼

我們Debug一下,可以看到Complier對應的自適應擴展類是AdaptiveCompiler

@Adaptive註解在方法上

我們以Protocol爲例:

我們看到Protocol接口有兩個方法標註了@Adaptive註解,另外兩個方法沒有標註,我們上面說過@Adaptive標記在方法上時會動態生成字節碼,下面來看一下源碼,我們就從getAdaptiveExtensionClass()方法開始跟

Debug跟一下,看看動態生成的類文件長什麼樣

格式化一下

從圖中標記的位置可以看出來,動態生成的類名稱爲Protocol$Adpative,被@Adpative標記的方法,傳入url參數,根據傳入不同的url參數來自定義擴展點,這就是自適應機制

@Activate註解

@Activate註解有點類似Spring的條件註解,根據條件進行擴展點的激活,在這裏@Activate註解表示擴展類被加載到的條件,有兩個重要的屬性

  • group 根據group過濾,包含則返回擴展
  • value key過濾條件,URL參數中的key包含此value值則返回擴展

詳細的源碼,可以去讀一下getActivateExtension()方法

總結一下

  • SPI,全稱爲 Service Provider Interface,是Java提供的一套用來被第三方實現或者擴展的API
  • Java SPI由Java核心庫提供,它定義了一個接口,具體的實現由第三方來實現
  • Java SPI實現起來比較簡單,都是約定俗成的事,按他的標準來就行,在實現Java SPI需要滿足以下條件
    • 寫一個接口,並且提供對應的具體的實現
    • 在META-INF/service/目錄下新建一個以接口的全限定名命名的文件,如jdbc.sql.Driver
    • 使用ServiceLoader.load()方法進行加載
  • Java SPI工作原理主要是通過ServiceLoader來加載指定目錄下的類實現
  • Java SPI的缺點:掃描META-INF/service/目錄,加載目錄下所有的service,不管你有沒有使用到
  • Dubbo SPI 在Java SPI的基礎上對其進行加強,使用Key-Value的形式來標識各個Service,並且增加了對擴展點的IOC和AOP支持
  • Dubbo主要是通過ExtensionLoader來進行進行擴展點的加載
  • Dubbo的自適應擴展點就是我們通過自己傳入的參數來進行動態的加載擴展點
  • @Adaptive註解可以標註在類和方法上,兩者的區別是:標註在類上說明這個類是自適應擴展點的類,標註在方法上需要動態生成字節碼,然後動態生成類
  • @Activate註解有點類似Spring的條件註解,根據條件進行擴展點的激活


寫在最後

傻姑粉絲福利:更多一線大廠面試題,高併發等主流技術資料盡在下方
github直達地址一線大廠面試題,高併發等主流技術資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章