1. 程式人生 > >搞懂Dubbo SPI可拓展機制

搞懂Dubbo SPI可拓展機制

前言

  閱讀本文需要具備java spi的基礎,本文不講java spi,please google it.

 

一.Dubbo SPI 簡介

  SPI(Service Provider Interface)是服務發現機制,Dubbo沒有使用jdk SPI而對其增強和擴充套件:

  1. jdk SPI僅通過介面類名獲取所有實現,但是Duboo SPI可以根據介面類名和key值獲取具體一個實現
  2. 可以對擴充套件類例項的屬性進行依賴注入,即IOC
  3. 可以採用裝飾器模式實現AOP功能

  你可以發現Dubbo的原始碼中有很多地方都用到了@SPI註解,例如:Protocol(通訊協議),LoadBalance(負載均衡)等。基於Dubbo SPI,我們可以非常容易的進行拓展。ExtensionLoader是擴充套件點核心類,用於載入Dubbo中各種可配置的元件,比如剛剛說的Protocol和LoadBalance等。那麼接下來我們看一下Dubbo SPI的示例

 

二.Dubbo SPI 示例

  比如現在我們要拓展Protocol這個元件,新建一個DefineProtocol類並修改預設埠為8888:

 1 /**
 2  * @author GrimMjx
 3  */
 4 public class DefineProtocol implements Protocol {
 5     @Override
 6     public int getDefaultPort() {
 7         return 8888;
 8     }
 9 
10     @Override
11     public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
12         return null;
13     }
14 
15     @Override
16     public <T> Invoker<T> refer(Class<T> aClass, URL url) throws RpcException {
17         return null;
18     }
19 
20     @Override
21     public void destroy() {
22 
23     }
24 }

  配置檔案的檔名字是介面的全限定名,那麼在這個例子中就是:com.alibaba.dubbo.rpc.Protocol

  Dubbo SPI所需的配置檔案要放在以下3個目錄任意一箇中:

  META-INF/services/

  META-INF/dubbo/

  META-INF/dubbo/internal/

  同時需要將服務提供者配置檔案設計成KV鍵值對的形式,Key是拓展類的name,Value是擴充套件的全限定名實現類。比如:

myProtocol=com.grimmjx.edu.DefineProtocol

  然後測試一下:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("dubbo-client.xml");
Protocol myProtocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol");
System.out.println(myProtocol.getDefaultPort());

  結果如下:

 

三.原始碼解析

  那我們就從上面的方法看起,重要方法紅色標註:

ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol");

1.getExtensionLoader方法,入參是一個可拓展的藉口,返回ExtensionLoader實體類,然後可以通過name(key)來獲取具體的擴充套件:

 1 public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
 2     if (type == null) {
 3         throw new IllegalArgumentException("Extension type == null");
 4     }
 5     // 擴充套件點必須是介面
 6     if (!type.isInterface()) {
 7         throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
 8     }
 9     // 必須有@SPI註解
10     if (!withExtensionAnnotation(type)) {
11         throw new IllegalArgumentException("Extension type (" + type +
12                 ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
13     }
14     // 每個擴充套件只會被載入一次
15     ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
16     if (loader == null) {
17         // 初始化擴充套件
18         EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
19         loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
20     }
21     return loader;
22 }

2.getExtension方法,首先檢查快取,如果沒有則用雙檢鎖方式建立例項:

 1 public T getExtension(String name) {
 2     if (StringUtils.isEmpty(name)) {
 3         throw new IllegalArgumentException("Extension name == null");
 4     }
 5     if ("true".equals(name)) {
 6         // 預設拓展實現類
 7         return getDefaultExtension();
 8     }
 9     // 獲取持有目標物件
10     Holder<Object> holder = getOrCreateHolder(name);
11     Object instance = holder.get();
12     // 雙檢鎖
13     if (instance == null) {
14         synchronized (holder) {
15             instance = holder.get();
16             if (instance == null) {
17                 // 建立例項
18                 instance = createExtension(name);
19                 holder.set(instance);
20             }
21         }
22     }
23     return (T) instance;
24 }

3.createExtension方法,這個方法比較核心。做了有4件事情,第3件和第4件分別為上面介紹Dubbo SPI中對jdk SPI擴充套件的第二和第三點(紅字已標註)。請看程式碼註釋:

 1 private T createExtension(String name) {
 2     // 1.載入配置檔案所有拓展類,得到配置名-拓展類的map,從map中獲取到拓展類
 3     Class<?> clazz = getExtensionClasses().get(name);
 4     if (clazz == null) {
 5         throw findException(name);
 6     }
 7     try {
 8         T instance = (T) EXTENSION_INSTANCES.get(clazz);
 9         if (instance == null) {
10             // 2.通過反射建立例項
11             // EXTENSION_INSTANCES這個map是配置名-拓展類例項的map
12             EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
13             instance = (T) EXTENSION_INSTANCES.get(clazz);
14         }
15         // 3.注入依賴,即IOC
16         injectExtension(instance);
17         Set<Class<?>> wrapperClasses = cachedWrapperClasses;
18         if (CollectionUtils.isNotEmpty(wrapperClasses)) {
19             // 4.迴圈建立Wrapper例項
20             for (Class<?> wrapperClass : wrapperClasses) {
21                 // 通過反射建立Wrapper例項
22                 // 向Wrapper例項注入依賴,最後賦值給instance
23                 // 自動包裝實現類似aop功能
24                 instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
25             }
26         }
27         return instance;
28     } catch (Throwable t) {
29         throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
30                 type + ") couldn't be instantiated: " + t.getMessage(), t);
31     }
32 }

4.getExtensionClasses方法,這裡就是找出所有拓展類,返回一個配置名-拓展類的map。

 1 private Map<String, Class<?>> getExtensionClasses() {
 2     Map<String, Class<?>> classes = cachedClasses.get();
 3     // 雙檢鎖
 4     if (classes == null) {
 5         synchronized (cachedClasses) {
 6             classes = cachedClasses.get();
 7             if (classes == null) {
 8                 // 快取無則載入
 9                 classes = loadExtensionClasses();
10                 cachedClasses.set(classes);
11             }
12         }
13     }
14     return classes;
15 }

5.loadExtensionClasses方法,主要就是解析SPI註解,然後載入指定目錄的配置檔案,也不是很難

 1 private Map<String, Class<?>> loadExtensionClasses() {
 2     // 獲取SPI註解,檢查合法等
 3     final SPI defaultAnnotation = type.getAnnotation(SPI.class);
 4     if(defaultAnnotation != null) {
 5         String value = defaultAnnotation.value();
 6         if(value != null && (value = value.trim()).length() > 0) {
 7             String[] names = NAME_SEPARATOR.split(value);
 8             if(names.length > 1) {
 9                 throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
10                         + ": " + Arrays.toString(names));
11             }
12             if(names.length == 1) cachedDefaultName = names[0];
13         }
14     }
15     
16     Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
17     // META-INF/dubbo/internal/目錄
18     loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
19     // META-INF/dubbo/目錄
20     loadFile(extensionClasses, DUBBO_DIRECTORY);
21     // META-INF/services/目錄
22     loadFile(extensionClasses, SERVICES_DIRECTORY);
23     return extensionClasses;
24 }

  這裡返回的extensionClasses的map就肯定包含了"myProtocol"->"com.grimmjx.edu.DefineProtocol"。同時也可以看到,dubbo支援有很多協議:

  接下來不用多說了吧,再從map裡get出"myProtocol",得到的就是我們自定義的協議類。

  上面3.createExtension方法,這個方法裡註釋的3和4很關鍵,裡面實現了依賴注入和AOP的功能,那麼接下來我們主要看看Dubbo的IOC和AOP

 

Dubbo IOC

   Dubbo IOC是通過setter方法注入依賴的,首先遍歷方法是否有setter方法特徵,如果有則通過objectFactory獲取依賴物件進行注入。Dubbo注入的可能是Dubbo的擴充套件,也有可能是一個Spring bean!

  上面的3方法中有這一行程式碼,實現了Dubbo SPI的IOC

injectExtension(instance);

  1.injectExtension方法。我們主要看這個方法,自動裝配的功能都在這個方法中:

 1 private T injectExtension(T instance) {
 2     try {
 3         if (objectFactory != null) {
 4             for (Method method : instance.getClass().getMethods()) {
 5                 // 如果方法以set開頭 && 只有一個引數 && 方法是public級別的
 6                 if (method.getName().startsWith("set")
 7                         && method.getParameterTypes().length == 1
 8                         && Modifier.isPublic(method.getModifiers())) {
 9                     // 獲取setter方法引數型別
10                     Class<?> pt = method.getParameterTypes()[0];
11                     try {
12                         String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
13 
14                         // 關鍵,從objectFactory裡拿出依賴物件
15                         Object object = objectFactory.getExtension(pt, property);
16                         if (object != null) {
17                             // 利用反射進行注入
18                             method.invoke(instance, object);
19                         }
20                     } catch (Exception e) {
21                         logger.error("fail to inject via method " + method.getName()
22                                 + " of interface " + type.getName() + ": " + e.getMessage(), e);
23                     }
24                 }
25             }
26         }
27     } catch (Exception e) {
28         logger.error(e.getMessage(), e);
29     }
30     return instance;
31 }

  2.objectFactory,究竟這裡的objectFactory是什麼呢?它是ExtensionFactory型別的,自身也是一個擴充套件點。我先告訴你這裡的objectFactory是AdaptiveExtensionFactory。後面會有解釋。

1 private ExtensionLoader(Class<?> type) {
2     this.type = type;
3     objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
4 }

  3.getExtension方法,這裡比較簡單,為了直觀,用debug模式看一下,factories有兩個,分別是SpiExtensionFactory和SpringExtensionFactory

  對於SpringExtensionFactory就是從工廠里根據beanName拿到Spring bean來注入,對於SpiExtensionFactory就是根據傳入Class獲取自適應拓展類,那麼我們寫一段程式碼,來試試獲取一個Spring Bean玩玩,先定義一個bean:

 1 /**
 2  * @author GrimMjx
 3  */
 4 public class Worker {
 5 
 6     private int age = 24;
 7 
 8     public int getAge() {
 9         return age;
10     }
11 
12     public void setAge(int age) {
13         this.age = age;
14     }
15 }

  然後再配置檔案裡配置這個Spring Bean:

  最後簡單寫個Main方法,可以看出SpringExtensionFactory可以載入Spring Bean:

 

Dubbo AOP

  Dubbo中也支援Spring AOP類似功能,通過裝飾者模式,使用包裝類包裝原始的擴充套件點例項。在擴充套件點實現前後插入其他邏輯,實現AOP功能。說這很繞口啊,那什麼是包裝類呢?舉個例子你就知道了:

 1 class A{
 2     private A a;
 3     public A(A a){
 4         this.a = a;
 5     }
 6 
 7     public void do(){
 8         // 插入擴充套件邏輯
 9         a.do();
10     }
11 }

  這裡的插入擴充套件邏輯,是不是就是實現了AOP功能呢?比如說Protocol類,有2個Wrapper,分別是ProtocolFilterWrapper和ProtocolListenerWrapper,我們可以在dubbo-rpc/dubbo-rpc-api/src/main/resources/META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol看到:

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper

  原始碼的話createExtension方法裡的註釋已經寫的很清楚了,這裡可以自行研究。所以我們在最開始的Dubbo SPI的例子中,我們打個斷點就很明顯了,得到的myProtocol物件其實是這樣的:

  如果你呼叫export方法的話,會先經歷ProtocolFilterWrapper的export方法,再經歷ProtocolListenerWrapper的export方法,這樣是不是就實現了Spring AOP的功能呢?

 

Dubbo SPI 自適應

  這裡getAdaptiveExtension到底獲取的是什麼呢,這裡涉及到SPI 自適應擴充套件,十分重要,涉及到@Adaptive註解。

  如果註解加在類上,比如說com.alibaba.dubbo.common.extension.factory.AdaptiveExtensionFactory(自行驗證):直接載入當前的介面卡

  如果註解載入方法上,比如說com.alibaba.dubbo.rpc.Protocol:動態建立一個自適應的介面卡,就像是執行如下程式碼,返回的是一個動態生成的代理類

Protocol adaptiveExtension = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
 1 public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
 2     public void destroy() {
 3         throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
 4     }
 5 
 6     public int getDefaultPort() {
 7         throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
 8     }
 9 
10     public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) {
11         if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
12         if (arg0.getUrl() == null)
13             throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
14         com.alibaba.dubbo.common.URL url = arg0.getUrl();
15         String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
16         if (extName == null)
17             throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
18         com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
19         return extension.export(arg0);
20     }
21 
22     public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) {
23         if (arg1 == null) throw new IllegalArgumentException("url == null");
24         com.alibaba.dubbo.common.URL url = arg1;
25         String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
26         if (extName == null)
27             throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
28         com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
29         return extension.refer(arg0, arg1);
30     }
31 }

  為什麼getDefaultPort和destroy方法都是直接丟擲異常呢?因為Protocol介面只有export和refer方法使用了@Adaptive註解,Dubbo會自動生成自適應例項,其他方法都是拋異常。

  為什麼還要要動態生成呢?有時候拓展不像在框架啟動的時候被載入,而是希望在擴充套件方法被呼叫的時候,根據執行時引數進行載入。

  最後看看上面2個標紅的程式碼,是不是就是本文開始的原始碼分析,是不是很簡單了?

 

 

 

 

 參考

http://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html(官方文件,穩穩的)

https://blog.51cto.com/13679539/212