dubbo是如何實現可擴充套件的?(二)

水木竹水發表於2022-05-28

 

牛逼的框架,看似複雜難懂,思路其實很清晰。---me

 


 

上篇文章,在整體擴充套件思路上進行了原始碼分析,比較粗糙,現在就某些點再詳細梳理下。

dubbo SPi的擴充套件,基於一類、三註解。

  • 一類是ExtensionLoader類
  • 三註解是@SPI、@Adaptive、@Activate

本文總結dubbo是如何使用ExtensionLoader實現擴充套件的,詳細看看它是怎麼設計的,為何這樣設計?

1. ExtensionLoader屬性

首先是ExtensionLoader包含的屬性,如下。

主要包含常量定義(如dubbo SPi路徑META-INF/services/等)、載入的型別type、一系列快取容器。

 

2. dubbo是如何載入SPI擴充套件類的呢?是一次性把所有的擴充套件都讀到記憶體中嗎?

當然不是,dubbo不是一次性把所有的SPI擴充套件檔案都載入。而是根據型別,即type,進行載入。

可以看到上圖中有兩個關鍵欄位,如下

private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();

private final Class<?> type;

 

其中 EXTENSION_LOADERS 是一個全域性 擴充套件載入器 的容器,key為擴充套件介面,即type型別的SPI介面;value為介面對應的ExtensionLoader例項。

 

在上篇文章中說到,dubbo載入SPI與JDK載入SPI類似,讀取指定路徑檔案中的定義。載入路徑如下:

Map<String, Class<?>> loadExtensionClasses()
    void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst)
        void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL)
            void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) 

 在第二步loadDirectory時,傳入路徑和type(介面的全限定名),在方法內部拼出SPI路徑,如下:

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst) {
        String fileName = dir + type;
        .......
}    

 

所以,得到的結論是:

  • 每個型別對應一個ExtensionLoader載入器;
  • 載入器載入擴充套件實現類時,只讀取type對應的實現類。

 

3. 為何要設計自適應類?帶來了什麼好處?

dubbo官網這樣解釋:“在 Dubbo 中,很多擴充都是通過 SPI 機制進行載入的,比如 Protocol、Cluster、LoadBalance 等。有時,有些擴充並不想在框架啟動階段被載入,而是希望在擴充方法被呼叫時,根據執行時引數進行載入。這聽起來有些矛盾。擴充未被載入,那麼擴充方法就無法被呼叫(靜態方法除外)。擴充方法未被呼叫,擴充就無法被載入。對於這個矛盾的問題,Dubbo 通過自適應擴充機制很好的解決了。自適應擴充機制的實現邏輯比較複雜,首先 Dubbo 會為擴充介面生成具有代理功能的程式碼。然後通過 javassist 或 jdk 編譯這段程式碼,得到 Class 類。最後再通過反射建立代理類,整個過程比較複雜。”

dubbo擴充套件非常多,所有的底層關鍵介面都可以擴充套件,為了不在啟動的時候載入所有類,而想在方法呼叫時載入,即懶漢方式。

所以引入了自適應擴充套件機制,它的好處:

  • 封裝所有擴充套件類,根據URL引數動態選擇具體實現類
  • 框架啟動時,減少不必要擴充套件的載入損耗

 

自適應類分為兩種,一種是動態生成的,一種是自定義。前者使用@Adaptive在方法上,後者使用該註解在類上。

區別就在於此,修飾在類上,表示該類為自適應類,無序dubbo再動態生成。

(1)自定義自適應類

這種方式的自適應類比較少,目前有ExtensionFactory、Compiler介面在使用。

AdaptiveExtensionFactory是ExtensionFactory的自適應類,它持有所有ExtensionFactory的實現,然後根據type和name遍歷所有容器載入擴充套件類物件,上篇文章有介紹。

@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {

    private final List<ExtensionFactory> factories;

    public AdaptiveExtensionFactory() {
      //持有ExtensionFactory所有介面實現類 ExtensionLoader
<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class); List<ExtensionFactory> list = new ArrayList<ExtensionFactory>(); for (String name : loader.getSupportedExtensions()) { list.add(loader.getExtension(name)); } factories = Collections.unmodifiableList(list); } @Override public <T> T getExtension(Class<T> type, String name) { for (ExtensionFactory factory : factories) { T extension = factory.getExtension(type, name); if (extension != null) { return extension; } } return null; } }

 

同理,AdaptiveCompiler是Compiler的自適應類,會根據name使用指定的編譯器,預設情況使用JavassistCompiler。

@Adaptive
public class AdaptiveCompiler implements Compiler {

    private static volatile String DEFAULT_COMPILER;

    public static void setDefaultCompiler(String compiler) {
        DEFAULT_COMPILER = compiler;
    }

    @Override
    public Class<?> compile(String code, ClassLoader classLoader) {
        Compiler compiler;
        ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
        String name = DEFAULT_COMPILER; // copy reference
        if (name != null && name.length() > 0) {
            compiler = loader.getExtension(name);
        } else {
            compiler = loader.getDefaultExtension();
        }
        return compiler.compile(code, classLoader);
    }

}

 

(2)動態生成的自適應類

如上篇文章中,介面如下:

@SPI("human")
public interface HelloService {
    String  sayHello();
    @Adaptive
    String  sayHello(URL  url);
}

 

動態生成的自適應類,如下:

package com.lagou.service;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class HelloService$Adaptive implements com.lagou.service.HelloService {
public java.lang.String sayHello() { throw new UnsupportedOperationException("The method public abstract java.lang.String com.lagou.service.HelloService.sayHello() of interface com.lagou.service.HelloService is not adaptive method!"); }
public java.lang.String sayHello(org.apache.dubbo.common.URL arg0) { if (arg0 == null) throw new IllegalArgumentException("url == null"); org.apache.dubbo.common.URL url = arg0; String extName = url.getParameter("hello.service", "human"); if(extName == null) throw new IllegalStateException("Failed to get extension (com.lagou.service.HelloService) name from url (" + url.toString() + ") use keys([hello.service])"); com.lagou.service.HelloService extension = (com.lagou.service.HelloService)ExtensionLoader.getExtensionLoader(com.lagou.service.HelloService.class).getExtension(extName); return extension.sayHello(arg0); } }

 

其中沒有被@Adaptive修飾的方法,生成的方法只有一個異常語句。被修飾的方法會根據URL引數及ExtensionLoader擴充套件機制,動態獲取使用的擴充套件實現類。

 

(3)dubbo是怎麼區分是否要動態生成,還是直接使用定義好的自適應類呢?

這個涉及到ExtensionLoader中的屬性cachedAdaptiveClass,其快取了自定義的自適應類,在SPI擴充套件載入的時候進行識別並快取。

如果沒有自定義的自適應類,則不會用到該快取。

涉及到自適應類的操作包含兩個步驟:載入+使用

 

  1)首先,載入。程式碼在loadClass方法中,如下:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) {
     
    if (clazz.isAnnotationPresent(Adaptive.class)) {
         cacheAdaptiveClass(clazz);
    }
  
}

 

當類被Aaptive修飾時,則將載入的class快取到cachedAdaptiveClass中,從原始碼中可以看到,只允許一個SPI介面具有一個自定義的自適應類。

private void cacheAdaptiveClass(Class<?> clazz) {
        if (cachedAdaptiveClass == null) {
            cachedAdaptiveClass = clazz;
        } else if (!cachedAdaptiveClass.equals(clazz)) {
            throw new IllegalStateException("More than 1 adaptive class found: "
                    + cachedAdaptiveClass.getName()
                    + ", " + clazz.getName());
        }
    }

 

   2)其次是使用。獲取自適應類的路徑為:

public T getAdaptiveExtension()
    private T createAdaptiveExtension()
        private Class<?> getAdaptiveExtensionClass()

 

看看getAdaptiveExtensionClass的程式碼,你就明白原來這麼簡單。

private Class<?> getAdaptiveExtensionClass() {
        getExtensionClasses();
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

 

該邏輯有三步:

  • 載入對應type的SPI擴充套件類,包括自定義的自適應類,即上邊描述的載入快取過程
  • 判斷是否有自定義的自適應類,有則直接返回
  • 沒有,則動態生成

我們來看看是怎麼動態生成的?原始碼寫的也很清楚。

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);
    }

 

總共四步:

  • 生成自適應類的原始碼code,通過字串拼接,如package、import、class、method等,具體不在這展開
  • 獲取類載入器
  • 獲取編譯器,此處會呼叫到Compiler的自定義的自適應類
  • 對code進行編譯,得到自適應類的class

 

4. 自動啟用類是怎麼回事?一般應用在什麼地方?

自動啟用,官網描述:“對於集合類擴充套件點,比如:FilterInvokerListenerExportListenerTelnetHandlerStatusChecker 等,可以同時載入多個實現,此時,可以用自動啟用來簡化配置”。

比如,過濾器Filter,可以使用@Activate,自動啟用自定義的過濾器,以使與業務相關的控制能參與到dubbo的呼叫鏈中。如日誌記錄、方法執行時間等。

 

對ExtensionLoader來說,如何識別和使用自動啟用類呢?

在ExtensionLoader中的cachedActivates屬性,快取了type 擴充套件類中被@Activate註解修飾的類資訊。

Map<String, Object> cachedActivates = new ConcurrentHashMap<>();

key為定義在SPI檔案中的key,Object為Activate註解的例項(注意這裡不是存的被修飾類對應的例項,而是Activate註解本身)。

涉及到該快取的操作有兩處,一處是載入SPI檔案時存入,一處是通過URL等資訊判斷是否需要啟用對應的類。

 

(1)Activate類是如何識別並載入的?

在loadClass中,有一段涉及Activate的程式碼,如下:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) {
   ......
    String[] names = NAME_SEPARATOR.split(name);
    if (ArrayUtils.isNotEmpty(names)) {
     //快取具有Activate註解的擴充套件類 cacheActivateClass(clazz, names[
0]); }
   ...... }

其中name為SPi檔案中定義的key,從下邊原始碼可以看出,cachedActivates  map中存的是Activate例項。

private void cacheActivateClass(Class<?> clazz, String name) {
        Activate activate = clazz.getAnnotation(Activate.class);
        if (activate != null) {
            cachedActivates.put(name, activate);
        } else {
            // support com.alibaba.dubbo.common.extension.Activate
            com.alibaba.dubbo.common.extension.Activate oldActivate = clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class);
            if (oldActivate != null) {
                cachedActivates.put(name, oldActivate);
            }
        }
    }

 

(2)Activate類又是如何使用的呢?

getActivateExtension方法是獲取Activate類的具體實現(下方程式碼有省略),我們可以看到分為兩步:

  • 載入擴充套件類,並獲取cacheActivates
  • 根據URL引數與Activate宣告的規則進行匹配
public List<T> getActivateExtension(URL url, String[] values, String group) {
        List<T> exts = new ArrayList<>();
        List<String> names = values == null ? new ArrayList<>(0) : Arrays.asList(values);
        if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
              //載入擴充套件類,獲取到cachedActivates
            getExtensionClasses();
              //遍歷cachedActivates,獲取與URL、group等引數匹配的Activate類
            for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
                String name = entry.getKey();
                Object activate = entry.getValue();

                String[] activateGroup, activateValue;

                if (activate instanceof Activate) {
                      //Activate類宣告的group
                    activateGroup = ((Activate) activate).group();
                      //Activate類宣告的value
                    activateValue = ((Activate) activate).value();
                } else {
                    continue;
                }
                  //1.組匹配;2.value匹配
                if (isMatchGroup(group, activateGroup)
                        && !names.contains(name)
                        && !names.contains(REMOVE_VALUE_PREFIX + name)
                        && isActive(activateValue, url)) {
                    exts.add(getExtension(name));
                }
            }
            exts.sort(ActivateComparator.COMPARATOR);
        }
        ......
        return exts;
    }

 

 以Filter為例,在ProtocolFilterWrapper.refer構造dubbo介面的invoker後,會對invoker增加過濾器,原始碼如下:

@Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        if (UrlUtils.isRegistry(url)) {
            return protocol.refer(type, url);
        }
        return buildInvokerChain(protocol.refer(type, url), REFERENCE_FILTER_KEY, CommonConstants.CONSUMER);
    }

 

傳給buildInvokerChain方法三個引數:

  • 第一個是為dubbo介面生成的invoker
  • 第二個是常量:reference.filter,對應URL中的某個key
  • 第三個是常量:consumer,對應group
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
        Invoker<T> last = invoker;
        List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);

        if (!filters.isEmpty()) {
            for (int i = filters.size() - 1; i >= 0; i--) {
                final Filter filter = filters.get(i);
                final Invoker<T> next = last;
                last = new Invoker<T>() {
                     ......
                };
            }
        }

        return last;
    }

 

buildInvokerChain方法,構建過濾器鏈的邏輯比較簡單:

  • dubbo介面的invoker在過濾器連結串列的最後,即在執行的時候,最後執行實際介面呼叫
  • 載入Filter對應的與group等引數匹配的自動啟用類
  • 構建過濾器鏈

(3)舉個例子

比如我們自定義一個過濾器

@Activate(group = {CommonConstants.CONSUMER,CommonConstants.PROVIDER})
public class DubboInvokeTimeFilter  implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        long   startTime  = System.currentTimeMillis();
        try {
            // 執行方法
            return  invoker.invoke(invocation);
        } finally {
            System.out.println("invoke time:"+(System.currentTimeMillis()-startTime) + "毫秒");
        }
    }
}

 

在META- INF.dubbo中增加org.apache.dubbo.rpc.Filter檔案,內容如下:

timeFilter=com.lagou.filter.DubboInvokeTimeFilter

 

5. dubbo的包裝器類是什麼,有何用處?

 dubbo中包裝器是SPI擴充套件類的一種,準確的說是一個代理類,實現了對擴充套件類的AOP。

  • 實現SPI介面
  • 持有SPI介面的物件

如上文中的ProtocolFilterWrapper,就是protocol介面對應的一個包裝器

public class ProtocolFilterWrapper implements Protocol {

    private final Protocol protocol;

    public ProtocolFilterWrapper(Protocol protocol) {
        if (protocol == null) {
            throw new IllegalArgumentException("protocol == null");
        }
        this.protocol = protocol;
    }
      
      ......
      
    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        if (UrlUtils.isRegistry(invoker.getUrl())) {
            return protocol.export(invoker);
        }
        return protocol.export(buildInvokerChain(invoker, SERVICE_FILTER_KEY, CommonConstants.PROVIDER));
    }

    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        if (UrlUtils.isRegistry(url)) {
            return protocol.refer(type, url);
        }
        return buildInvokerChain(protocol.refer(type, url), REFERENCE_FILTER_KEY, CommonConstants.CONSUMER);
    }
  
        ......
}

 並且,該類也是定義在dubbo-rpc模組下的SPI org.apache.dubbo.rpc.Protocol檔案中,如下:

filter=org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=org.apache.dubbo.rpc.support.MockProtocol

 

在通過某Protocol實現類建立物件時,會自動為該物件封裝該包裝器。這樣也就實現了對invoker的過濾攔截。

 

(1)dubbo是如何實現包裝器類的識別和載入的呢?

要從ExtensionLoader的cachedWrapperClasses屬性說起,該屬性快取了包裝器類。

private Set<Class<?>> cachedWrapperClasses;

 

 載入擴充套件類時,loadClass方法中會對class進行判斷,如下:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) {
     if (isWrapperClass(clazz)) {
          cacheWrapperClass(clazz);
     }
}

 

  • 判斷是否為包裝器類
  • 快取包裝器類

看isWrapperClass方法,呼叫的是Class獲取建構函式的方法,如果不存在具有type型別引數的建構函式,則拋異常,通過攔截返回false。

private boolean isWrapperClass(Class<?> clazz) {
        try {
            clazz.getConstructor(type);
            return true;
        } catch (NoSuchMethodException e) {
            return false;
        }
    }

 

快取包裝器類,程式碼很簡單

private void cacheWrapperClass(Class<?> clazz) {
        if (cachedWrapperClasses == null) {
            cachedWrapperClasses = new ConcurrentHashSet<>();
        }
        cachedWrapperClasses.add(clazz);
    }

  

(2)dubbo是如何對某例項進行包裝的呢?

通過例子來說明,extensionLoader.getExtension(extension) 這句程式碼是根據副檔名獲取對應的擴充套件類例項的,包裝器就在這個過程中把原來的例項進行封裝代理了。

public static void main(String[] args) {
        // 獲取擴充套件載入器
        ExtensionLoader<HelloService>  extensionLoader  = ExtensionLoader.getExtensionLoader(HelloService.class);
        // 遍歷所有的支援的擴充套件點 META-INF.dubbo
        Set<String>  extensions = extensionLoader.getSupportedExtensions();
        for (String extension : extensions){
            String result = extensionLoader.getExtension(extension).sayHello();
            System.out.println(result);
        }
    }

 定義HelloWrapper類:

public class HelloWrapper implements HelloService {

    private HelloService helloService;

    public HelloWrapper(HelloService helloService) {
        this.helloService = helloService;
    }

    @Override
    public String sayHello() {
        System.out.println("sayHello------>");
        return helloService.sayHello();
    }

    @Override
    public String sayHello(URL url) {
        System.out.println("sayHello------>url");
        return helloService.sayHello(url);
    }
}

 

測試輸出:

sayHello------>url
wang url

 

dubbo如何對例項進行封裝的呢?

 核心邏輯在createExtension方法中,即根據傳入的name建立擴充套件類例項的方法中。

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);
            }
            injectExtension(instance);
       //wrapper Set
<Class<?>> wrapperClasses = cachedWrapperClasses; if (CollectionUtils.isNotEmpty(wrapperClasses)) { for (Class<?> wrapperClass : wrapperClasses) {
            //包裝類的建立 instance
= injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); } } initExtension(instance); return instance; } catch (Throwable t) { throw new IllegalStateException("Extension instance (name: " + name + ", class: " + type + ") couldn't be instantiated: " + t.getMessage(), t); } }

 

 從程式碼可以看出分為兩步:

  • getExtensionClasses方法載入擴充套件類,存到cachedWrapperClasses中
  • 遍歷cachedWrapperClasses,通過構造器例項化包裝器,同時還為包裝類進行依賴注入。(無論多少wrapper,都會一層一層進行封裝)

這樣返回給使用者的擴充套件例項,即為經過層層封裝的擴充套件類。

 

6. 總結

  • ExtensionLoader是實現dubbo可擴充套件的核心類,為各擴充套件點提供框架層面的支援,
  • ExtensionLoader的邏輯看著複雜,其實思路比較簡單,第一是擴充套件點載入,第二是建立指定的擴充套件點例項
  • 從程式碼分析,可以看出該類包含了一系列快取容器,這些快取在擴充套件點載入的時候進行識別和儲存
  • 在擴充套件點例項建立時,會通過自適應類動態找到目標擴充套件;將自動啟用類應用到擴充套件例項或dubbo的核心invoker上;並將例項封裝到wrapper類中 

相關文章