深度Dubbo源碼 – SPI的使用與好處
- 2019 年 10 月 5 日
- 筆記
背景
相信閱讀過Dubbo源碼的同學應該看到在Dubbo中的很多介面上都有一個 @SPI
的註解,筆者也不例外,但是一直不知道這個註解具體是幹什麼的,為了解決什麼問題,怎麼去使用?網上簡單檢索了下,中文名:服務供給介面,詳見下圖(來自百度百科)。

也許因為 dubbo
本身的功能強大,所以筆者也只是知道能 dubbo
可以自定義實現某些策略,比如負載均衡、序列化、執行緒池類型等等,但是還未正式在線上環境中使用。趁著節假日花些時間研究下,記錄下,希望對大家有用。
程式碼樣例
以下程式碼均是經過本地驗證的,純屬手敲,具體執行詳見https://github.com/GloryXu/spring-boot
註:測試項目搭建 spring-boot
+ spring-boot-dubbo
(https://github.com/apache/dubbo-spring-boot-project)
驗證思路
正如上圖所述,說 @SPI
是實現某個特定的服務,那就來個簡單的實現,最熟悉的莫過於負載均衡( LoadBalance
)策略了,本地啟動兩個 provider
,埠不同,通過 consumer
的入參來決定訪問指定的 provider
。
啟動provider
程式碼極其簡單,程式碼框架如下

import org.apache.dubbo.config.annotation.Service; import org.apache.dubbo.rpc.RpcContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; // 指定版本和分組 @Service(version = "1.0.0",group = "glory") public class DemoServiceImpl implements DemoService { private static final Logger logger = LoggerFactory.getLogger(DemoServiceImpl.class); @Override public String sayHello(Integer port) { logger.info("Hello " + port + " request from consumer: " + RpcContext.getContext().getRemoteAddress()); return "Hello ,"+port+" response from provider: " + RpcContext.getContext().getLocalAddress(); } }
以下為 application.yml
配置文件
server: port: 8083 dubbo: application: name: dubbo-common-provider scan: base-packages: com.redsun.rpc.dubbo protocol: name: dubbo port: 12345 registry: address: zookeeper://127.0.0.1:2181
還需要注意的是,在啟動時需要指定不同埠,否則無法啟動。

此時就能正常啟動兩個本地應用了,啟動效果如下:

啟動consumer
consumer
的程式碼框架也很簡單

想必大家猜也能看出來, GloryLoadBalance
就是自定義實現的一種負載均衡策略,通過前端傳入的參數來選擇 invoker
public class GloryLoadBalance implements LoadBalance { private static final Logger logger = LoggerFactory.getLogger(GloryLoadBalance.class); @Override public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException { Integer port = (Integer) invocation.getArguments()[0];// 前端傳入的參數 logger.info("前端傳入埠為:" + port); // java8的流式編程,有興趣的同學可以研究下,後續會再專門寫一篇 Invoker<T> invokerRet = invokers.stream().filter(invoker -> invoker.getUrl().getPort() == port).findFirst().get(); return invokerRet == null ? invokers.get(0) : invokerRet; } }
此處還簡單寫了個 controller
,方便動態改參數
@RestController @RequestMapping("/admin") public class AdminController { private static final Logger logger = LoggerFactory.getLogger(AdminController.class); @Reference(version = "1.0.0",group = "glory", loadbalance = "glory") private DemoService demoService; @RequestMapping("/invoke") public String invoke(@RequestParam(name = "port") Integer port) { logger.info("invoke method be invoked!port = " + port); return demoService.sayHello(port); } }
當然還會最後一步,也是最重要的,在 META-INF.dubbo.internal
目錄添加配置文件,下面等號前面的 glory
其實就是你配置的 loadbalance
的key,如果路徑錯了,或者未配置,還是會獲取到默認的實現 random
glory=com.redsun.rpc.dubbo.loadbalance.GloryLoadBalance
dubbo
會載入 META-INF.dubbo.internal
目錄中的所有配置資訊,在 dubbo
目錄下也會有很多的默認實現

Postman調用
以下兩張圖測試,滿足預期。


源碼
下面就到了翻源碼的時候了,這裡簡單講一下我本人看源碼的一點心得。
- 別糾結於每個方法的實現,一般都是debug跟,很容易跟暈,最後就失去了興趣
- 學會看注釋,尤其是方法名、類名、變數名,源碼不同於平時自己編寫的程式碼,既然開源了,就是面向所有人的,所以注釋、起名往往會寫的很詳細很規範,英文不認識?翻譯看,次數多了也就認識了
- 在debug跟的時候記住幾個核心的類,看完之後梳理下整個的調用鏈,對程式碼結構先要有個大概的認知(實在不想看,百度也行,我也經常這麼干,然後再自己跟一下,認證下)
載入
ExtensionLoader#getExtensionClasses
// synchronized in getExtensionClasses private Map<String, Class<?>> loadExtensionClasses() { cacheDefaultExtensionName(); Map<String, Class<?>> extensionClasses = new HashMap<>(); loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName()); loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName()); loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName()); loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); return extensionClasses; }

/** * put clazz in extensionClasses */ private void saveInExtensionClass(Map<String, Class<?>> extensionClasses, Class<?> clazz, String name) { Class<?> c = extensionClasses.get(name); if (c == null) { // 將擴展類GloryLoadBalance存入map中,最終會將默認提供的幾種都存入map中 extensionClasses.put(name, clazz); } else if (c != clazz) { throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + name + " on " + c.getName() + " and " + clazz.getName()); } }
載入好實現類最終會將 ReferenceConfig
的負載均衡參數設置為 glory

運行
AbstractClusterInvoker#invoke
在 consumer
發起 invoke
的時候會根據config的key進行有選擇的創建實例
public Result invoke(final Invocation invocation) throws RpcException { checkWhetherDestroyed(); // binding attachments into invocation. Map<String, String> contextAttachments = RpcContext.getContext().getAttachments(); if (contextAttachments != null && contextAttachments.size() != 0) { ((RpcInvocation) invocation).addAttachments(contextAttachments); } List<Invoker<T>> invokers = list(invocation); // 初始化載入負載均衡類 LoadBalance loadbalance = initLoadBalance(invokers, invocation); RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation); return doInvoke(invocation, invokers, loadbalance); }
ExtensionLoader#getExtensionLoader
每個 META-INF.dubbo.internal
目錄中的文件都是 ExtensionLoader
對象,存儲在一個靜態的類變數 EXTENSION_LOADERS
中
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) { if (type == null) { throw new IllegalArgumentException("Extension type == null"); } if (!type.isInterface()) { throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!"); } // 此處判斷是否以SPI註解修飾 if (!withExtensionAnnotation(type)) { throw new IllegalArgumentException("Extension type (" + type + ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!"); } ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type); if (loader == null) { EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type)); loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type); } return loader; }
ExtensionLoader#getExtension
如果此時為空,則會通過上面載入的Class資訊newInstance一個實例使用,程式碼比較簡單,有興趣的可以跟一下。

總結
通過以上應該可以看出,後續如果想擴展使用dubbo會非常的簡單,增加一個實現類(實現對應介面),再在 META-INF
目錄下添加一個配置文件,key對應好就能完成了。序列化方式也可以按照自己的意願來。在跟程式碼的時候也能看到對於一些其他不使用的擴展類,dubbo都將Class對象載入進去了,也算是一點點小瑕疵吧。