將近2萬字的 Dubbo 原理解析,徹底搞懂 dubbo (上篇)
01 前言
前面我們研究了RPC的原理,市面上有很多基於RPC思想實現的框架,比如有Dubbo。今天就從Dubbo的SPI機制、服務註冊與發現原始碼及網路通訊過程去深入剖析下Dubbo。
02 Dubbo架構
2.1 概述
Dubbo是阿里巴巴公司開源的一個高效能優秀的服務框架,使得應用可透過高效能的RPC 實現服務的輸出和輸入功能,可以和Spring框架無縫整合。
Dubbo是一款高效能、輕量級的開源Java RPC框架,它提供了三大核心能力:面向介面的遠端方法呼叫,智慧容錯和負載均衡,以及服務自動註冊和發現。
呼叫流程:
-
服務容器負責啟動,載入,執行服務提供者。
-
服務提供者在啟動時,向註冊中心註冊自己提供的服務。
-
服務消費者在啟動時,向註冊中心訂閱自己所需的服務。
-
註冊中心返回服務提供者地址列表給消費者,如果有變更,註冊中心將基於長連線推送變更資料給消費者。
-
服務消費者,從提供者地址列表中,基於軟負載均衡演算法,選一臺提供者進行呼叫,如果呼叫失敗,再選另一臺呼叫。
-
服務消費者和提供者,在記憶體中累計呼叫次數和呼叫時間,定時每分鐘傳送一次統計資料到監控中心。
2.2 架構體系
(1)原始碼結構
-
dubbo-common:公共邏輯模組: 包括Util類和通用模型
-
dubbo-remoting 遠端通訊模組: 相當於dubbo協議的實現,如果RPC使用RMI協議則不需要使用此包
-
dubbo-rpc 遠端呼叫模組: 抽象各種協議,以及動態代理,包含一對一的呼叫,不關心叢集的原理。
-
dubbo-cluster 叢集模組: 將多個服務提供方偽裝成一個提供方,包括負載均衡,容錯,路由等,叢集的地址列表可以是靜態配置的,也可以是註冊中心下發的.
-
dubbo-registry 註冊中心模組: 基於註冊中心下發的叢集方式,以及對各種註冊中心的抽象
-
dubbo-monitor 監控模組: 統計服務呼叫次數,呼叫時間,呼叫鏈跟蹤的服務.
-
dubbo-config 配置模組: 是dubbo對外的api,使用者透過config使用dubbo,隱藏dubbo所有細節
-
dubbo-container 容器模組: 是一個standlone的容器,以簡單的main載入spring啟動,因為服務通常不需要Tomcat/Jboss等web容器的特性,沒必要用web容器去載入服務.
(2)整體設計
-
圖中左邊淡藍背景的為服務消費方使用的介面,右邊淡綠色背景的為服務提供方使用的介面,位於中軸線上的為雙方都用到的介面。
-
圖中從下至上分為十層,各層均為單向依賴,每一層都可以剝離上層被複用,其中,Service 和Config 層為API,其它各層均為SPI。
-
圖中綠色小塊的為擴充套件介面,藍色小塊為實現類,圖中只顯示用於關聯各層的實現類。
-
圖中藍色虛線為初始化過程,即啟動時組裝鏈,紅色實線為方法呼叫過程,即執行時調時鏈,紫色三角箭頭為繼承,可以把子類看作父類的同一個節點,線上的文字為呼叫的方法。
(3)各層說明
-
config 配置層:對外配置介面,以 ServiceConfig , ReferenceConfig 為中心,可以直接初始化配置類,也可以透過spring 解析配置生成配置類
-
proxy 服務代理層:服務介面透明代理,生成服務的客戶端Stub 和伺服器端Skeleton, 以ServiceProxy 為中心,擴充套件介面為 ProxyFactory
-
registry 註冊中心層:封裝服務地址的註冊與發現,以服務URL 為中心,擴充套件介面為RegistryFactory , Registry , RegistryService
-
cluster 路由層:封裝多個提供者的路由及負載均衡,並橋接註冊中心,以 Invoker 為中心,擴充套件介面為 Cluster , Directory , Router , LoadBalance
-
monitor 監控層:RPC 呼叫次數和呼叫時間監控,以 Statistics 為中心,擴充套件介面為MonitorFactory , Monitor , MonitorService
-
protocol 遠端呼叫層:封裝RPC 呼叫,以 Invocation , Result 為中心,擴充套件介面為Protocol , Invoker , Exporter
-
exchange 資訊交換層:封裝請求響應模式,同步轉非同步,以 Request , Response 為中心,擴充套件介面為 Exchanger , ExchangeChannel , ExchangeClient , ExchangeServer
-
transport 網路傳輸層:抽象mina 和netty 為統一介面,以 Message 為中心,擴充套件介面為Channel , Transporter , Client , Server , Codec
-
serialize 資料序列化層:可複用的一些工具,擴充套件介面為 Serialization , ObjectInput ,ObjectOutput , ThreadPool
(4)呼叫流程
對照上面的整體架構圖可以大致分為以下步驟:
1、服務提供者啟動,開啟Netty服務,建立Zookeeper客戶端,向註冊中心註冊服務。
2、服務消費者啟動,透過Zookeeper向註冊中心獲取服務提供者列表,與服務提供者透過Netty建立長連線。
3、服務消費者透過介面開始遠端呼叫服務,ProxyFactory透過初始化Proxy物件,Proxy透過建立動態代理物件。
4、動態代理物件透過invoke方法,層層包裝生成一個Invoker物件,該物件包含了代理物件。
5、Invoker透過路由,負載均衡選擇了一個最合適的服務提供者,在透過加入各種過濾器,協議層包裝生成一個新的DubboInvoker物件。
6、再透過交換成將DubboInvoker物件包裝成一個Reuqest物件,該物件透過序列化透過NettyClient傳輸到服務提供者的NettyServer端。
7、到了服務提供者這邊,再透過反序列化、協議解密等操作生成一個DubboExporter物件,再層層傳遞處理,會生成一個服務提供端的Invoker物件.
8、這個Invoker物件會呼叫本地服務,獲得結果再透過層層回撥返回到服務消費者,服務消費者拿到結果後,再解析獲得最終結果。
03 Dubbo中的SPI機制
3.1 什麼是SPI
(1)概述
在Dubbo 中,SPI 是一個非常重要的模組。基於SPI,我們可以很容易的對Dubbo 進行擴充。如果大家想要學習Dubbo 的原始碼,SPI 機制務必弄懂。接下來,我們先來了解一下Java SPI 與Dubbo SPI 的用法,然後再來分析Dubbo SPI 的原始碼。
SPI是Service Provider Interface 服務提供介面縮寫,是一種服務發現機制。SPI的本質是將介面的實現類的全限定名定義在配置檔案中,並有伺服器讀取配置檔案,並載入實現類。這樣就可以在執行的時候,動態為介面替換實現類。
3.2 JDK中的SPI
Java SPI 實際上是“基於介面的程式設計+策略模式+配置檔案”組合實現的動態載入機制。
透過一個案例我們來認識下SPI
定義一個介面:
package com.laowang; /** * @author 原 * @date 2021/3/27 * @since 1.0 **/ public interface User { String showName(); }
定義兩個實現類
package com.laowang.impl; import com.laowang.User; /** * @author 原 * @date 2021/3/27 * @since 1.0 **/ public class Student implements User { @Override public String showName() { System.out.println("my name is laowang"); return null; } }package com.laowang.impl; import com.laowang.User; /** * @author 原 * @date 2021/3/27 * @since 1.0 **/ public class Teacher implements User { @Override public String showName() { System.out.println("my name is zhangsan"); return null; } }
在resources目錄下建立資料夾META-INF.services,並在該資料夾下建立一個名稱與User的全路徑一致的檔案com.laowang.User
在檔案中寫入,兩個實現類的全路徑名
編寫測試類:
package com.laowang; import java.util.ServiceLoader; /** * @author 原 * @date 2021/3/27 * @since 1.0 **/ public class SpiTest { public static void main(String[] args) { ServiceLoader<User> serviceLoader = ServiceLoader.load(User.class); serviceLoader.forEach(User::showName); } }
執行結果:
我們發現透過SPI機制,幫我們自動執行了兩個實現類。
透過檢視ServiceLoader原始碼:
其實透過讀取配置檔案中實現類的全路徑類名,透過反射建立物件,並放入providers容器中。
總結:
呼叫過程應用程式呼叫ServiceLoader.load方法,建立一個新的ServiceLoader,並例項化該類中的成員變數
應用程式透過迭代器介面獲取物件例項,ServiceLoader先判斷成員變數providers物件中(LinkedHashMap<String,S>型別)是否有快取例項物件,如果有快取,直接返回。如果沒有快取,執行類的裝載,
優點使用Java SPI 機制的優勢是實現解耦,使得介面的定義與具體業務實現分離,而不是耦合在一起。應用程式可以根據實際業務情況啟用或替換具體元件。
缺點不能按需載入。雖然ServiceLoader 做了延遲載入,但是基本只能透過遍歷全部獲取,也就是介面的實現類得全部載入並例項化一遍。如果你並不想用某些實現類,或者某些類例項化很耗時,它也被載入並例項化了,這就造成了浪費。
獲取某個實現類的方式不夠靈活,只能透過Iterator 形式獲取,不能根據某個引數來獲取對應的實現類。
多個併發多執行緒使用ServiceLoader 類的例項是不安全的。
載入不到實現類時丟擲並不是真正原因的異常,錯誤很難定位。
3.3 Dubbo中的SPI
Dubbo 並未使用Java SPI,而是重新實現了一套功能更強的SPI 機制。Dubbo SPI 的相關邏輯被封裝在了ExtensionLoader 類中,透過ExtensionLoader,我們可以載入指定的實現類。
(1)栗子
與Java SPI 實現類配置不同,Dubbo SPI 是透過鍵值對的方式進行配置,這樣我們可以按需載入指定的實現類。下面來演示Dubbo SPI 的用法:
Dubbo SPI 所需的配置檔案需放置在META-INF/dubbo 路徑下,與Java SPI 實現類配置不同,DubboSPI 是透過鍵值對的方式進行配置,配置內容如下。
optimusPrime = org.apache.spi.OptimusPrime bumblebee = org.apache.spi.Bumblebee
在使用Dubbo SPI 時,需要在介面上標註@SPI 註解。
@SPI public interface Robot { void sayHello(); }
透過ExtensionLoader,我們可以載入指定的實現類,下面來演示Dubbo SPI :
public class DubboSPITest { @Test public void sayHello() throws Exception { ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class); Robot optimusPrime = extensionLoader.getExtension("optimusPrime"); optimusPrime.sayHello(); Robot bumblebee = extensionLoader.getExtension("bumblebee"); bumblebee.sayHello(); } }
Dubbo SPI 除了支援按需載入介面實現類,還增加了IOC 和AOP 等特性,這些特性將會在接下來的原始碼分析章節中一一進行介紹。
(2)原始碼分析
ExtensionLoader 的getExtensionLoader 方法獲取一個ExtensionLoader 例項,然後再透過ExtensionLoader 的getExtension 方法獲取擴充類物件。下面我們從ExtensionLoader 的getExtension 方法作為入口,對擴充類物件的獲取過程進行詳細的分析。
public T getExtension(String name) { if (StringUtils.isEmpty(name)) { throw new IllegalArgumentException("Extension name == null"); } if ("true".equals(name)) { // 獲取預設的擴充實現類 return getDefaultExtension(); } // Holder,顧名思義,用於持有目標物件 就是從容器中獲取,如果沒有直接new一個Holder Holder<Object> holder = getOrCreateHolder(name); //獲取目標物件例項 Object instance = holder.get(); // 如果目標物件例項為null 就需要透過雙重檢查建立例項 if (instance == null) { synchronized (holder) { instance = holder.get(); if (instance == null) { // 建立擴充例項 instance = createExtension(name); // 設定例項到 holder 中 holder.set(instance); } } } return (T) instance; }
上面程式碼的邏輯比較簡單,首先檢查快取,快取未命中則建立擴充物件。下面我們來看一下建立擴充物件的過程是怎樣的。
private T createExtension(String name) { // 從配置檔案中載入所有的擴充類,可得到“配置項名稱”到“配置類”的對映關係表 Class<?> clazz = getExtensionClasses().get(name); if (clazz == null) { throw findException(name); } try { //從容器中獲取對應的例項物件 如果不存在就透過反射建立 T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { // 透過反射建立例項 EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } // 向例項中注入依賴 下面是IOC和AOP的實現 injectExtension(instance); Set<Class<?>> wrapperClasses = cachedWrapperClasses; if (CollectionUtils.isNotEmpty(wrapperClasses)) { // 迴圈建立 Wrapper 例項 for (Class<?> wrapperClass : wrapperClasses) { // 將當前 instance 作為引數傳給 Wrapper 的構造方法,並透過反射建立Wrapper 例項。 // 然後向 Wrapper 例項中注入依賴,最後將 Wrapper 例項再次賦值給instance 變數 instance = injectExtension( (T) wrapperClass.getConstructor(type).newInstance(instance)); } }
createExtension 方法的邏輯稍複雜一下,包含了如下的步驟:
-
透過getExtensionClasses 獲取所有的擴充類
-
透過反射建立擴充物件
-
向擴充物件中注入依賴
-
將擴充物件包裹在相應的Wrapper 物件中
以上步驟中,第一個步驟是載入擴充類的關鍵,第三和第四個步驟是Dubbo IOC 與AOP 的具體實現。由於此類設計原始碼較多,這裡簡單的總結下ExtensionLoader整個執行邏輯:
getExtension(String name) #根據key獲取擴充物件 -->createExtension(String name) #建立擴充例項 -->getExtensionClasses #根據路徑獲取所有的擴充類 -->loadExtensionClasses #載入擴充類 -->cacheDefaultExtensionName #解析@SPI註解 -->loadDirectory #方法載入指定資料夾配置檔案 -->loadResource #載入資源 -->loadClass #載入類,並透過 loadClass 方法對類進行快取
3.4 Dubbo的SPI如何實現IOC和AOP的
(1)Dubbo IoC
Dubbo IOC 是透過setter 方法注入依賴。Dubbo 首先會透過反射獲取到例項的所有方法,然後再遍歷方法列表,檢測方法名是否具有setter 方法特徵。若有,則透過ObjectFactory 獲取依賴物件,最後透過反射呼叫setter 方法將依賴設定到目標物件中。整個過程對應的程式碼如下:
private T injectExtension(T instance) { try { if (objectFactory != null) { //獲取例項的所有方法 for (Method method : instance.getClass().getMethods()) { //isSetter做的事:檢測方法是否以 set 開頭,且方法僅有一個引數,且方法訪問級別為 public if (isSetter(method)) { /** * Check {@link DisableInject} to see if we need auto injection for this property */ if (method.getAnnotation(DisableInject.class) != null) { continue; } Class<?> pt = method.getParameterTypes()[0]; if (ReflectUtils.isPrimitives(pt)) { continue; } try { String property = getSetterProperty(method); //獲取依賴物件 Object object = objectFactory.getExtension(pt, property); if (object != null) { //設定屬性 method.invoke(instance, object); } } catch (Exception e) { logger.error("Failed to inject via method " + method.getName() + " of interface " + type.getName() + ": " + e.getMessage(), e); } } } } } catch (Exception e) { logger.error(e.getMessage(), e); } return instance; }
(2)Dubbo Aop
在說這個之前,我們得先知道裝飾者模式
裝飾者模式:在不改變原類檔案以及不使用繼承的情況下,動態地將責任附加到物件上,從而實現動態擴充一個物件的功能。它是透過建立一個包裝物件,也就是裝飾來包裹真實的物件。
在用Spring的時候,我們經常會用到AOP功能。在目標類的方法前後插入其他邏輯。比如通常使用Spring AOP來實現日誌,監控和鑑權等功能。Dubbo的擴充套件機制,是否也支援類似的功能呢?答案是yes。在Dubbo中,有一種特殊的類,被稱為Wrapper類。透過裝飾者模式,使用包裝類包裝原始的擴充套件點例項。在原始擴充套件點實現前後插入其他邏輯,實現AOP功能。
一般來說裝飾者模式有下面幾個參與者:
Component:裝飾者和被裝飾者共同的父類,是一個介面或者抽象類,用來定義基本行為
ConcreteComponent:定義具體物件,即被裝飾者
Decorator:抽象裝飾者,繼承自Component,從外類來擴充套件ConcreteComponent。對於ConcreteComponent來說,不需要知道Decorator的存在,Decorator是一個介面或抽象類
ConcreteDecorator:具體裝飾者,用於擴充套件ConcreteComponent
//獲取所有需要包裝的類 Set<Class<?>> wrapperClasses = cachedWrapperClasses;
我們再看看cachedWrapperClasses是什麼?
private Set<Class<?>> cachedWrapperClasses;
是一個set集合,那麼集合是什麼時候新增元素的呢?
/** * cache wrapper class * <p> * like: ProtocolFilterWrapper, ProtocolListenerWrapper */ private void cacheWrapperClass(Class<?> clazz) { if (cachedWrapperClasses == null) { cachedWrapperClasses = new ConcurrentHashSet<>(); } cachedWrapperClasses.add(clazz); }
透過這個方法新增的,再看看誰呼叫了這個私有方法:
/** * test if clazz is a wrapper class * <p> * which has Constructor with given class type as its only argument */ private boolean isWrapperClass(Class<?> clazz) { try { clazz.getConstructor(type); return true; } catch (NoSuchMethodException e) { return false; } }
原來是透過isWrapperClass這個方法,判斷有沒有其他物件中的構造方法中持有本物件,如果有,dubbo就認為這是個裝飾類,呼叫裝飾者類的構造方法,並返回例項物件
然後透過例項化這個包裝類代替需要載入的這個類。這樣執行的方法就是包裝類的方法。
04 Dubbo中的動態編譯
我們知道在Dubbo 中,很多擴充都是透過SPI 機制 進行載入的,比如Protocol、Cluster、LoadBalance、ProxyFactory 等。有時,有些擴充並不想在框架啟動階段被載入,而是希望在擴充方法被呼叫時,根據執行時引數進行載入,即根據引數動態載入實現類。
這種在執行時,根據方法引數才動態決定使用具體的擴充,在dubbo中就叫做擴充套件點自適應例項。其實是一個擴充套件點的代理,將擴充套件的選擇從Dubbo啟動時,延遲到RPC呼叫時。Dubbo中每一個擴充套件點都有一個自適應類,如果沒有顯式提供,Dubbo會自動為我們建立一個,預設使用Javaassist。
自適應擴充機制的實現邏輯是這樣的
-
首先Dubbo 會為擴充介面生成具有代理功能的程式碼;
-
透過javassist 或jdk 編譯這段程式碼,得到Class 類;
-
透過反射建立代理類;
-
在代理類中,透過URL物件的引數來確定到底呼叫哪個實現類;
4|1javassist
Javassist是一個開源的分析、編輯和建立Java位元組碼的類庫。是由東京工業大學的數學和電腦科學系的Shigeru Chiba (千葉滋)所建立的。它已加入了開放原始碼JBoss 應用伺服器專案,透過使用Javassist對位元組碼操作為JBoss實現動態AOP框架。javassist是jboss的一個子專案,其主要的優點,在於簡單,而且快速。直接使用java編碼的形式,而不需要了解虛擬機器指令,就能動態改變類的結構,或者動態生成類。
/** * Javassist是一個開源的分析、編輯和建立Java位元組碼的類庫 * 能動態改變類的結構,或者動態生成類 */ public class CompilerByJavassist { public static void main(String[] args) throws Exception { // ClassPool:class物件容器 ClassPool pool = ClassPool.getDefault(); // 透過ClassPool生成一個User類 CtClass ctClass = pool.makeClass("com.itheima.domain.User"); // 新增屬性 -- private String username CtField enameField = new CtField(pool.getCtClass("java.lang.String"), "username", ctClass); enameField.setModifiers(Modifier.PRIVATE); ctClass.addField(enameField); // 新增屬性 -- private int age CtField enoField = new CtField(pool.getCtClass("int"), "age", ctClass); enoField.setModifiers(Modifier.PRIVATE); ctClass.addField(enoField); //新增方法 ctClass.addMethod(CtNewMethod.getter("getUsername", enameField)); ctClass.addMethod(CtNewMethod.setter("setUsername", enameField)); ctClass.addMethod(CtNewMethod.getter("getAge", enoField)); ctClass.addMethod(CtNewMethod.setter("setAge", enoField)); // 無參構造器 CtConstructor constructor = new CtConstructor(null, ctClass); constructor.setBody("{}"); ctClass.addConstructor(constructor); // 新增建構函式 //ctClass.addConstructor(new CtConstructor(new CtClass[] {}, ctClass)); CtConstructor ctConstructor = new CtConstructor(new CtClass[] {pool.get(String.class.getName()),CtClass.intType}, ctClass); ctConstructor.setBody("{\n this.username=$1; \n this.age=$2;\n}"); ctClass.addConstructor(ctConstructor); // 新增自定義方法 CtMethod ctMethod = new CtMethod(CtClass.voidType, "printUser",new CtClass[] {}, ctClass); // 為自定義方法設定修飾符 ctMethod.setModifiers(Modifier.PUBLIC); // 為自定義方法設定函式體 StringBuffer buffer2 = new StringBuffer(); buffer2.append("{\nSystem.out.println(\"使用者資訊如下\");\n") .append("System.out.println(\"使用者名稱=\"+username);\n") .append("System.out.println(\"年齡=\"+age);\n").append("}"); ctMethod.setBody(buffer2.toString()); ctClass.addMethod(ctMethod); //生成一個class Class<?> clazz = ctClass.toClass(); Constructor cons2 = clazz.getDeclaredConstructor(String.class,Integer.TYPE); Object obj = cons2.newInstance("itheima",20); //反射 執行方法 obj.getClass().getMethod("printUser", new Class[] {}) .invoke(obj, new Object[] {}); // 把生成的class檔案寫入檔案 byte[] byteArr = ctClass.toBytecode(); FileOutputStream fos = new FileOutputStream(new File("D://User.class")); fos.write(byteArr); fos.close(); } }
4.2 原始碼分析
Adaptive註解
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface Adaptive { String[] value() default {}; }
Adaptive 可註解在類或方法上。
標註在類上:Dubbo 不會為該類生成代理類。
標註在方法上:Dubbo 則會為該方法生成代理邏輯,表示當前方法需要根據 引數URL 呼叫對應的擴充套件點實現。
dubbo中每一個擴充套件點都有一個自適應類,如果沒有顯式提供,Dubbo會自動為我們建立一個,預設使用Javaassist。 先來看下建立自適應擴充套件類的程式碼
//1、看下extensionLoader的獲取方法 ExtensionLoader<Robot>extensionLoader=ExtensionLoader.getExtensionLoader(Robot.class); //2、最終呼叫的是ExtensionLoader的構造方法 private ExtensionLoader(Class<?> type) { this.type = type; objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); } //3、getAdaptiveExtension()看看幹了什麼事 public T getAdaptiveExtension() { //獲取自適應擴充套件類,如果沒有就開始初始化一個 Object instance = cachedAdaptiveInstance.get(); if (instance == null) { if (createAdaptiveInstanceError == null) { synchronized (cachedAdaptiveInstance) { instance = cachedAdaptiveInstance.get(); if (instance == null) { try { //這裡建立了一個自適應擴充套件類 instance = createAdaptiveExtension(); cachedAdaptiveInstance.set(instance); } catch (Throwable t) { createAdaptiveInstanceError = t; throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t); } } } } else { throw new IllegalStateException("Failed to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError); } } return (T) instance; } //看看createAdaptiveExtension() private T createAdaptiveExtension() { try { return injectExtension((T) getAdaptiveExtensionClass().newInstance()); } catch (Exception e) { throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e); } } //再進到getAdaptiveExtensionClass() private Class<?> getAdaptiveExtensionClass() { getExtensionClasses(); if (cachedAdaptiveClass != null) { return cachedAdaptiveClass; } return cachedAdaptiveClass = createAdaptiveExtensionClass(); } //繼續追進去createAdaptiveExtensionClass() private Class<?> createAdaptiveExtensionClass() { String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate(); ClassLoader classLoader = findClassLoader(); org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); return compiler.compile(code, classLoader); } //看看compiler @SPI("javassist") public interface Compiler { /** * Compile java source code. * * @param code Java source code * @param classLoader classloader * @return Compiled class */ Class<?> compile(String code, ClassLoader classLoader); } //其實到這裡就知道了,透過生成一個類的字串,再透過javassist生成一個物件
createAdaptiveExtensionClassCode()方法中使用一個StringBuilder來構建自適應類的Java原始碼。方法實現比較長,這裡就不貼程式碼了。這種生成位元組碼的方式也挺有意思的,先生成Java原始碼,然後編譯,載入到jvm中。透過這種方式,可以更好的控制生成的Java類。而且這樣也不用care各個位元組碼生成框架的api等。因為xxx.java檔案是Java通用的,也是我們最熟悉的。只是程式碼的可讀性不強,需要一點一點構建xx.java的內容。
05 服務暴露與發現
5.1 服務暴露
(1)名詞解釋
在Dubbo 的核心領域模型中:
-
Invoker 是實體域,它是Dubbo 的核心模型,其它模型都向它靠擾,或轉換成它,它代表一個可執行體,可向它發起invoke 呼叫,它有可能是一個本地的實現,也可能是一個遠端的實現,也可能一個叢集實現。在服務提供方,Invoker用於呼叫服務提供類。在服務消費方,Invoker用於執行遠端呼叫。
-
Protocol 是服務域,它是Invoker 暴露和引用的主功能入口,它負責Invoker 的生命週期管理。 export:暴露遠端服務 refer:引用遠端服務
-
proxyFactory:獲取一個介面的代理類 getInvoker:針對server端,將服務物件,如DemoServiceImpl包裝成一個Invoker物件 getProxy:針對client端,建立介面的代理物件,例如DemoService的介面。
-
Invocation 是會話域,它持有呼叫過程中的變數,比如方法名,引數等
(2)整體流程
在詳細探討服務暴露細節之前 , 我們先看一下整體duubo的服務暴露原理
在整體上看,Dubbo 框架做服務暴露分為兩大部分 , 第一步將持有的服務例項透過代理轉換成Invoker, 第二步會把Invoker 透過具體的協議 ( 比如Dubbo ) 轉換成Exporter, 框架做了這層抽象也大大方便了功能擴充套件 。
服務提供方暴露服務的藍色初始化鏈,時序圖如下:
(3)原始碼分析
服務匯出的入口方法是ServiceBean 的onApplicationEvent。onApplicationEvent 是一個事件響應方法,該方法會在收到Spring 上下文重新整理事件後執行服務匯出操作。方法程式碼如下:
@Override public void onApplicationEvent(ContextRefreshedEvent event) { if (!isExported() && !isUnexported()) { if (logger.isInfoEnabled()) { logger.info("The service ready on spring started. service: " + getInterface()); } export(); } }
透過export最終找到doExportUrls()方法
private void doExportUrls() { //載入配置檔案中的所有註冊中心,並且封裝為dubbo內部的URL物件列表 List<URL> registryURLs = loadRegistries(true); //迴圈所有協議配置,根據不同的協議,向註冊中心中發起註冊 for (ProtocolConfig protocolConfig : protocols) { String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version); ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass); ApplicationModel.initProviderModel(pathKey, providerModel); //服務暴露方法 doExportUrlsFor1Protocol(protocolConfig, registryURLs); } }
doExportUrlsFor1Protocol()方法程式碼老多了,我們只關係核心的地方
... if (!SCOPE_NONE.equalsIgnoreCase(scope)) { //本地暴露,將服務資料記錄到本地JVM中 if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) { exportLocal(url); } //遠端暴露,向註冊中心傳送資料 if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) { if (!isOnlyInJvm() && logger.isInfoEnabled()) { logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url); } if (CollectionUtils.isNotEmpty(registryURLs)) { for (URL registryURL : registryURLs) { //if protocol is only injvm ,not register if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) { continue; } url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY)); URL monitorUrl = loadMonitor(registryURL); if (monitorUrl != null) { url = url.addParameterAndEncoded(MONITOR_KEY, monitorUrl.toFullString()); } if (logger.isInfoEnabled()) { logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL); } // For providers, this is used to enable custom proxy to generate invoker String proxy = url.getParameter(PROXY_KEY); if (StringUtils.isNotEmpty(proxy)) { registryURL = registryURL.addParameter(PROXY_KEY, proxy); } // 為服務提供類(ref)生成 Invoker Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString())); // DelegateProviderMetaDataInvoker 用於持有 Invoker 和ServiceConfig DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); // 匯出服務,並生成 Exporter Exporter<?> exporter = protocol.export(wrapperInvoker); exporters.add(exporter); } } else { //不存在註冊中心,僅匯出服務 .... } /** * @since 2.7.0 * ServiceData Store */ MetadataReportService metadataReportService = null; if ((metadataReportService = getMetadataReportService()) != null) { metadataReportService.publishProvider(url); } } } this.urls.add(url);
上面程式碼根據url 中的scope 引數決定服務匯出方式,分別如下:
scope = none,不匯出服務
scope != remote,匯出到本地
scope != local,匯出到遠端
不管是匯出到本地,還是遠端。進行服務匯出之前,均需要先建立Invoker,這是一個很重要的步驟。因此下面先來分析Invoker 的建立過程。Invoker 是由ProxyFactory 建立而來,Dubbo 預設的ProxyFactory 實現類是JavassistProxyFactory。下面我們到JavassistProxyFactory 程式碼中,探索Invoker 的建立過程。如下:
@Override public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) { // 為目標類建立warpper final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type); //建立匿名才invoker物件,並實現doinvoke方法 return new AbstractProxyInvoker<T>(proxy, type, url) { @Override protected Object doInvoke(T proxy, String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Throwable { // 呼叫 Wrapper 的 invokeMethod 方法,invokeMethod 最終會呼叫目標方法 return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments); } }; }
Invoke建立成功之後,接下來我們來看本地匯出
/** * always export injvm */ private void exportLocal(URL url) { URL local = URLBuilder.from(url) .setProtocol(LOCAL_PROTOCOL) // 設定協議頭為 injvm .setHost(LOCALHOST_VALUE)//本地ip:127.0.0.1 .setPort(0) .build(); // 建立 Invoker,並匯出服務,這裡的 protocol 會在執行時呼叫 InjvmProtocol 的export 方法 Exporter<?> exporter = protocol.export( proxyFactory.getInvoker(ref, (Class) interfaceClass, local)); exporters.add(exporter); logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry url : " + local); }
exportLocal 方法比較簡單,首先根據URL 協議頭決定是否匯出服務。若需匯出,則建立一個新的URL並將協議頭、主機名以及埠設定成新的值。然後建立Invoker,並呼叫InjvmProtocol 的export 方法匯出服務。下面我們來看一下InjvmProtocol 的export 方法都做了哪些事情。
@Override public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException { return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap); }
如上,InjvmProtocol 的export 方法僅建立了一個InjvmExporter,無其他邏輯。到此匯出服務到本地就分析完了。
再看看匯出服務到遠端
接下來,我們繼續分析匯出服務到遠端的過程。匯出服務到遠端包含了服務匯出與服務註冊兩個過程。先來分析服務匯出邏輯。我們把目光移動到RegistryProtocol 的export 方法上。
@Override public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException { // 獲取註冊中心 URL URL registryUrl = getRegistryUrl(originInvoker); URL providerUrl = getProviderUrl(originInvoker); final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl); final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker); overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener); providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener); //匯出服務 final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl); // 根據 URL 載入 Registry 實現類,比如 ZookeeperRegistry final Registry registry = getRegistry(originInvoker); //獲取已註冊的服務提供者 URL, final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl); ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl); //to judge if we need to delay publish boolean register = registeredProviderUrl.getParameter("register", true); if (register) { // 向註冊中心註冊服務 register(registryUrl, registeredProviderUrl); providerInvokerWrapper.setReg(true); } // 向註冊中心進行訂閱 override 資料 registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener); exporter.setRegisterUrl(registeredProviderUrl); exporter.setSubscribeUrl(overrideSubscribeUrl); // 建立並返回 DestroyableExporter return new DestroyableExporter<>(exporter); }
上面程式碼看起來比較複雜,主要做如下一些操作:
-
呼叫doLocalExport 匯出服務
-
向註冊中心註冊服務
-
向註冊中心進行訂閱override 資料
-
建立並返回DestroyableExporter
看看doLocalExport 做了什麼
private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) { String key = getCacheKey(originInvoker); return (ExporterChangeableWrapper<T>) bounds.computeIfAbsent(key, s -> { Invoker<?> invokerDelegate = new InvokerDelegate<>(originInvoker, providerUrl); //protocol和配置的協議相關(dubbo:DubboProtocol) return new ExporterChangeableWrapper<>((Exporter<T>) protocol.export(invokerDelegate), originInvoker); }); }
接下來,我們把重點放在Protocol 的export 方法上。假設執行時協議為dubbo,此處的protocol 變數會在執行時載入DubboProtocol,並呼叫DubboProtocol 的export 方法。
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException { URL url = invoker.getUrl(); // export service.獲取服務標識,理解成服務座標也行。由服務組名,服務名,服務版本號以及埠組成。比如:demoGroup/com.alibaba.dubbo.demo.DemoService:1.0.1:20880 String key = serviceKey(url); //建立DubboExporter DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap); exporterMap.put(key, exporter); //key:介面 (DemoService) //export an stub service for dispatching event Boolean isStubSupportEvent = url.getParameter(STUB_EVENT_KEY, DEFAULT_STUB_EVENT); Boolean isCallbackservice = url.getParameter(IS_CALLBACK_SERVICE, false); if (isStubSupportEvent && !isCallbackservice) { String stubServiceMethods = url.getParameter(STUB_EVENT_METHODS_KEY); if (stubServiceMethods == null || stubServiceMethods.length() == 0) { if (logger.isWarnEnabled()) { logger.warn(new IllegalStateException("consumer [" + url.getParameter(INTERFACE_KEY) + "], has set stubproxy support event ,but no stub methods founded.")); } } else { stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods); } } //啟動服務 openServer(url); //最佳化序列器 optimizeSerialization(url); return exporter; }
如上,我們重點關注DubboExporter 的建立以及openServer 方法,其他邏輯看不懂也沒關係,不影響理解服務匯出過程。下面分析openServer 方法。
private void openServer(URL url) { // find server. String key = url.getAddress(); //client can export a service which's only for server to invoke boolean isServer = url.getParameter(IS_SERVER_KEY, true); if (isServer) { //訪問快取 ExchangeServer server = serverMap.get(key); if (server == null) { synchronized (this) { server = serverMap.get(key); if (server == null) { //建立伺服器例項 serverMap.put(key, createServer(url)); } } } else { // server supports reset, use together with override server.reset(url); } } }
接下來分析伺服器例項的建立過程。如下
private ExchangeServer createServer(URL url) { url = URLBuilder.from(url) // send readonly event when server closes, it's enabled by default .addParameterIfAbsent(CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString()) // enable heartbeat by default .addParameterIfAbsent(HEARTBEAT_KEY, String.valueOf(DEFAULT_HEARTBEAT)) .addParameter(CODEC_KEY, DubboCodec.NAME) .build(); String str = url.getParameter(SERVER_KEY, DEFAULT_REMOTING_SERVER); // 透過 SPI 檢測是否存在 server 引數所代表的 Transporter 擴充,不存在則丟擲異常 if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) { throw new RpcException("Unsupported server type: " + str + ", url: " + url); } ExchangeServer server; try { // 建立 ExchangeServer server = Exchangers.bind(url, requestHandler); } catch (RemotingException e) { throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e); } // 獲取 client 引數,可指定 netty,mina str = url.getParameter(CLIENT_KEY); if (str != null && str.length() > 0) { // 獲取所有的 Transporter 實現類名稱集合,比如 supportedTypes = [netty, mina] Set<String> supportedTypes = ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions(); // 檢測當前 Dubbo 所支援的 Transporter 實現類名稱列表中, // 是否包含 client 所表示的 Transporter,若不包含,則丟擲異常 if (!supportedTypes.contains(str)) { throw new RpcException("Unsupported client type: " + str); } } return server; }
如上,createServer 包含三個核心的邏輯。
第一是檢測是否存在server 引數所代表的Transporter 擴充,不存在則丟擲異常。
第二是建立伺服器例項。
第三是檢測是否支援client 引數所表示的Transporter 擴充,不存在也是丟擲異常。兩次檢測操作所對應的程式碼較直白了,無需多說。但建立伺服器的操作目前還不是很清晰,我們繼續往下看。
public static ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException { if (url == null) { throw new IllegalArgumentException("url == null"); } if (handler == null) { throw new IllegalArgumentException("handler == null"); } url = url.addParameterIfAbsent(Constants.CODEC_KEY, "exchange"); // 獲取 Exchanger,預設為 HeaderExchanger。 // 緊接著呼叫 HeaderExchanger 的 bind 方法建立 ExchangeServer 例項 return getExchanger(url).bind(url, handler); }
上面程式碼比較簡單,就不多說了。下面看一下HeaderExchanger 的bind 方法。
public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException { // 建立 HeaderExchangeServer 例項,該方法包含了多個邏輯,分別如下: // 1. new HeaderExchangeHandler(handler) // 2. new DecodeHandler(new HeaderExchangeHandler(handler)) // 3. Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))) return new HeaderExchangeServer(Transporters.bind(url, new ChannelHandler[]{new DecodeHandler(new HeaderExchangeHandler(handler))})); }
HeaderExchanger 的bind 方法包含的邏輯比較多,但目前我們僅需關心Transporters 的bind 方法邏
輯即可。該方法的程式碼如下:
public static Server bind(URL url, ChannelHandler... handlers) throws RemotingException { if (url == null) { throw new IllegalArgumentException("url == null"); } else if (handlers != null && handlers.length != 0) { Object handler; if (handlers.length == 1) { handler = handlers[0]; } else { // 如果 handlers 元素數量大於1,則建立 ChannelHandler 分發器 handler = new ChannelHandlerDispatcher(handlers); } // 獲取自適應 Transporter 例項,並呼叫例項方法 return getTransporter().bind(url, (ChannelHandler)handler); } else { throw new IllegalArgumentException("handlers == null"); } }
如上,getTransporter() 方法獲取的Transporter 是在執行時動態建立的,類名為TransporterAdaptive,也就是自適應擴充類。TransporterAdaptive 會在執行時根據傳入的URL 引數決定載入什麼型別的Transporter,預設為NettyTransporter。呼叫 NettyTransporter.bind(URL,ChannelHandler) 方法。建立一個 NettyServer 例項。呼叫 NettyServer.doOPen() 方法,伺服器被開啟,服務也被暴露出來了。
(4)服務註冊
本節內容以Zookeeper 註冊中心作為分析目標,其他型別註冊中心大家可自行分析。下面從服務註冊
的入口方法開始分析,我們把目光再次移到RegistryProtocol 的export 方法上。如下:
進入到register()方法
public void register(URL registryUrl, URL registeredProviderUrl) { //獲得註冊中心例項 Registry registry = registryFactory.getRegistry(registryUrl); //進行註冊 registry.register(registeredProviderUrl); }
看看getRegistry()方法
@Override public Registry getRegistry(URL url) { url = URLBuilder.from(url) .setPath(RegistryService.class.getName()) .addParameter(INTERFACE_KEY, RegistryService.class.getName()) .removeParameters(EXPORT_KEY, REFER_KEY) .build(); String key = url.toServiceStringWithoutResolving(); // Lock the registry access process to ensure a single instance of the registry LOCK.lock(); try { Registry registry = REGISTRIES.get(key); if (registry != null) { return registry; } //create registry by spi/ioc registry = createRegistry(url); if (registry == null) { throw new IllegalStateException("Can not create registry " + url); } REGISTRIES.put(key, registry); return registry; } finally { // Release the lock LOCK.unlock(); } }
進入createRegistry()方法
@Override public Registry createRegistry(URL url) { return new ZookeeperRegistry(url, zookeeperTransporter); }public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) { super(url); if (url.isAnyHost()) { throw new IllegalStateException("registry address == null"); } //// 獲取組名,預設為 dubbo String group = url.getParameter(GROUP_KEY, DEFAULT_ROOT); if (!group.startsWith(PATH_SEPARATOR)) { group = PATH_SEPARATOR + group; } this.root = group; // 建立 Zookeeper 客戶端,預設為 CuratorZookeeperTransporter zkClient = zookeeperTransporter.connect(url); // 新增狀態監聽器 zkClient.addStateListener(state -> { if (state == StateListener.RECONNECTED) { try { recover(); } catch (Exception e) { logger.error(e.getMessage(), e); } } }); }
在上面的程式碼程式碼中,我們重點關注ZookeeperTransporter 的connect 方法呼叫,這個方法用於建立
Zookeeper 客戶端。建立好Zookeeper 客戶端,意味著註冊中心的建立過程就結束了。
搞懂了服務註冊的本質,那麼接下來我們就可以去閱讀服務註冊的程式碼了。
public void doRegister(URL url) { try { // 透過 Zookeeper 客戶端建立節點,節點路徑由 toUrlPath 方法生成,路徑格式如下: // /${group}/${serviceInterface}/providers/${url} // 比如 /dubbo/org.apache.dubbo.DemoService/providers/dubbo%3A%2F%2F127.0.0.1...... zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true)); } catch (Throwable e) { throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e); } }@Override public void create(String path, boolean ephemeral) { if (!ephemeral) { // 如果要建立的節點型別非臨時節點,那麼這裡要檢測節點是否存在 if (checkExists(path)) { return; } } int i = path.lastIndexOf('/'); if (i > 0) { // 遞迴建立上一級路徑 create(path.substring(0, i), false); } // 根據 ephemeral 的值建立臨時或持久節點 if (ephemeral) { createEphemeral(path); } else { createPersistent(path); } }
好了,到此關於服務註冊的過程就分析完了。整個過程可簡單總結為:先建立註冊中心例項,之後再透過註冊中心例項註冊服務。
總結
-
在有註冊中心,需要註冊提供者地址的情況下,ServiceConfig 解析出的URL 格式為:registry:// registry-host/org.apache.dubbo.registry.RegistryService?export=URL.encode("dubbo://service-host/{服務名}/{版本號}")
-
基於Dubbo SPI 的自適應機制,透過URL registry:// 協議頭識別,就呼叫RegistryProtocol#export() 方法
-
將具體的服務類名,比如 DubboServiceRegistryImpl ,透過ProxyFactory 包裝成Invoker 例項
-
呼叫doLocalExport 方法,使用DubboProtocol 將Invoker 轉化為Exporter 例項,並開啟Netty 服務端監聽客戶請求
-
建立Registry 例項,連線Zookeeper,並在服務節點下寫入提供者的URL 地址,註冊服務
-
向註冊中心訂閱override 資料,並返回一個Exporter 例項
-
根據URL 格式中的 "dubbo://service-host/{服務名}/{版本號}" 中協議頭 dubbo:// 識別,呼叫DubboProtocol#export()方法,開發服務埠
-
RegistryProtocol#export() 返回的Exporter 例項存放到ServiceConfig 的 List<Exporter>exporters 中
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70000181/viewspace-2774479/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java進階專題(二十六) 將近2萬字的Dubbo原理解析,徹底搞懂dubboJava
- Java進階專題(二十七) 將近2萬字的Dubbo原理解析,徹底搞懂dubbo (下)Java
- 徹底搞懂https原理HTTP
- 徹底搞懂 Channel 實現原理
- 看完讓你徹底搞懂Websocket原理Web
- 兩張圖徹底搞懂MyBatis的Mapper原理!MyBatisAPP
- 從原理到實戰,徹底搞懂NginxNginx
- 徹底搞懂Composer自動載入原理
- 徹底搞懂徹底搞懂事件驅動模型 - Reactor事件模型React
- Dubbo2.7的Dubbo SPI實現原理細節
- 一文徹底搞懂BP演算法:原理推導+資料演示+專案實戰(上篇)演算法
- 徹底搞懂 Kubernetes 中的 Events
- 徹底搞懂Python中的類Python
- Dubbo原理和原始碼解析之服務引用原始碼
- Dubbo的Remoting模組解析REM
- Dubbo底層原理分析和分散式實際應用分散式
- 從原理到實戰,徹底搞懂Nginx(高階篇)Nginx
- 徹底搞懂Bean載入Bean
- 徹底搞懂JavaScript作用域JavaScript
- 徹底搞懂 Git-RebaseGit
- 徹底搞懂HTTPS的加密機制HTTP加密
- 徹底搞懂JavaScript中的繼承JavaScript繼承
- Dubbo 實現原理與原始碼解析系列 —— 精品合集原始碼
- Dubbo 3 之 Triple 流控反壓原理解析
- 搞懂Dubbo SPI可擴充機制
- dubbo的SPI應用與原理
- 聊聊Dubbo – Dubbo可擴充套件機制原始碼解析套件原始碼
- 深入JavaScript系列(四):徹底搞懂thisJavaScript
- 一文徹底搞懂BERT
- 徹底搞懂IO多路複用
- 徹底搞懂Scrapy的中介軟體(二)
- 徹底搞懂Scrapy的中介軟體(一)
- 徹底搞懂Object和Function的關係ObjectFunction
- 徹底搞懂Scrapy的中介軟體(三)
- 【Dubbo篇】--Dubbo框架的使用框架
- Dubbo原始碼解析之SPI原始碼
- dubbo原始碼解析-spi(五)原始碼
- dubbo SPI功能解析(一)