深度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對象載入進去了,也算是一點點小瑕疵吧。