Dubbo的可擴展機制SPI源碼解析

Dubbo的可擴展機制SPI源碼解析

Demo

ExtensionLoader<Protocol> extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol http = extensionLoader.getExtension("dubbo");
System.out.println(http);

上面這個Demo就是Dubbo常見的寫法,表示獲取"dubbo"對應的Protocol擴展點。Protocol是一個接口。
在ExtensionLoader類的內部有一個static的ConcurrentHashMap,用來緩存某個接口類型所對應的ExtensionLoader實例
ExtensionLoader
ExtensionLoader表示一個擴展點加載器,在這個類中除開有上文的Map外,還有兩個非常重要的屬性:

1. Class<?> type:表示當前ExtensionLoader實例是哪個接口的擴展點加載器
2. ExtensionFactory objectFactory:表示當前ExtensionLoader實例的擴展點生成器(一個擴展點,就是一個接口的實現類的對象)

構造方法如下:

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

從上面的構造方法中可以看到一段比較特殊的代碼:

ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()

在ExtensionLoader中有三個常用的方法:

1. getExtension("dubbo"):表示獲取名字爲dubbo的擴展點實例
2. getAdaptiveExtension():表示獲取一個自適應的擴展點實例
3. getActivateExtension(URL url, String[] values, String group):表示一個可以被url激活的擴展點實例,後文詳細解釋

其中,什麼是自適應的擴展點實例?它其實就是當前這個接口類型的一個代理類,可以通過這個代理類去獲取某個名字的擴展點。那爲什麼要這麼設計呢?
這是因爲對於某個接口的實現類實例,並不是僅僅只能通過Dubbo框架來生成,比如通過getExtension(“dubbo”)方法所獲得擴展點實例,是由Dubbo框架
根據名字對應的實現類幫我們生成的一個實例對象。而有的時候,我們需要從Dubbo之外去獲取實例對象,比如從Spring容器中根據名字取獲取bean,來作爲一個擴展點實例。
所以ExtensionFactory表示一個擴展點工廠,在Dubbo中有三個實現類:

  1. AdaptiveExtensionFactory:負責從SpiExtensionFactory或SpringExtensionFactory中得到擴展點實例對象
  2. SpiExtensionFactory:利用Dubbo的Spi機制獲取一個擴展點實例
  3. SpringExtensionFactory:從Spring的ApplicationContext中獲取bean作爲一個擴展點實例
    所以回到上文的那麼代碼,它拿到的就是一個AdaptiveExtensionFactory實例, objectFactory表示一個擴展點實例工廠。

getExtension(String name)方法

在調用getExtension去獲取一個擴展點實例後,會對實例進行緩存,下次再獲取同樣名字的擴展點實例時就會從緩存中拿了。

public T getExtension(String name) {
        if (StringUtils.isEmpty(name)) {
            throw new IllegalArgumentException("Extension name == null");
        }
        // 獲取默認擴展類
        if ("true".equals(name)) {
            return getDefaultExtension();
        }
        final Holder<Object> holder = getOrCreateHolder(name);
        Object instance = holder.get();
        if (instance == null) {
            synchronized (holder) {
                instance = holder.get();
                if (instance == null) {
                    // 創建擴展實例
                    instance = createExtension(name);
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }

createExtension(String name)方法

在調用createExtension(String name)方法去創建一個擴展點實例時,要經過以下幾個步驟:

  1. 根據name找到對應的擴展點實現類
  2. 根據實現類生成一個實例,把實現類和對應生成的實例進行緩存
  3. 對生成出來的實例進行依賴注入(給實例的屬性進行賦值)
  4. 對依賴注入後的實例進行AOP(Wrapper),把當前接口類的所有的Wrapper全部一層一層包裹在實
    例對象上,沒包裹個Wrapper後,也會對Wrapper對象進行依賴注入
  5. 返回最終的Wrapper對象
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);

            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (CollectionUtils.isNotEmpty(wrapperClasses)) {
                for (Class<?> wrapperClass : wrapperClasses) {
                    // 生成一個Wrapper實例(傳入了instance),然後進行依賴注入
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }

            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                    type + ") couldn't be instantiated: " + t.getMessage(), t);
        }
    }

getExtensionClasses

getExtensionClasses()是用來加載當前接口所有的擴展點實現類的,返回一個Map。之後可以從這個Map中按照指定的name獲取對應的擴展點實現類。
當把當前接口的所有擴展點實現類都加載出來後也會進行緩存,下次需要加載時直接拿緩存中的。
Dubbo在加載一個接口的擴展點時,思路是這樣的:

  1. 根據接口的全限定名去META-INF/dubbo/internal/目錄下尋找對應的文件,調用loadResource方法進行加載
  2. 根據接口的全限定名去META-INF/dubbo/目錄下尋找對應的文件,調用loadResource方法進行加載
  3. 根據接口的全限定名去META-INF/services/目錄下尋找對應的文件,調用loadResource方法進行加載
    這裏其實會設計到老版本兼容的邏輯,不解釋了。
/**
     * 加載當前ExtensionLoader對象中指定的接口的所有擴展
     * @return
     */
    private Map<String, Class<?>> getExtensionClasses() {
        Map<String, Class<?>> classes = cachedClasses.get();
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    classes = loadExtensionClasses();
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }

    /**
     * synchronized in getExtensionClasses
     * */
    private Map<String, Class<?>> loadExtensionClasses() {
        // cache接口默認的擴展類
        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;
    }

loadResource方法

loadResource方法就是完成對文件內容的解析,按行進行解析,會解析出"=“兩邊的內容,”="左邊的內容就是擴展點的name,
右邊的內容就是擴展點實現類,並且會利用ExtensionLoader類的類加載器來加載擴展點實現類。
然後調用loadClass方法對name和擴展點實例進行詳細的解析,並且最終把他們放到Map中去。

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
        try {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    final int ci = line.indexOf('#');
                    if (ci >= 0) {
                        line = line.substring(0, ci);
                    }
                    line = line.trim();
                    if (line.length() > 0) {
                        try {
                            String name = null;
                            int i = line.indexOf('=');
                            if (i > 0) {
                                name = line.substring(0, i).trim();
                                line = line.substring(i + 1).trim();
                            }
                            if (line.length() > 0) {
                                // 加載類,並添加到extensionClasses中
                                loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                            }
                        } catch (Throwable t) {
                            IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
                            exceptions.put(line, e);
                        }
                    }
                }
            }
        } catch (Throwable t) {
            logger.error("Exception occurred when loading extension class (interface: " +
                    type + ", class file: " + resourceURL + ") in " + resourceURL, t);
        }
    }

loadClass方法

loadClass方法會做如下幾件事情:

  1. 當前擴展點實現類上是否存在@Adaptive註解,如果存在則把該類認爲是當前接口的默認自適應類(接口代理類),並把該類存到cachedAdaptiveClass屬性上。
  2. 當前擴展點實現是否是一個當前接口的一個Wrapper類,如果判斷的?就是看當前類中是否存在一個構造方法,該構造方法只有一個參數,參數類型爲接口類型,如果存在這一的
    構造方法,那麼這個類就是該接口的Wrapper類,如果是,則把該類添加到cachedWrapperClasses中去, cachedWrapperClasses是一個set。
  3. 如果不是自適應類,或者也不是Wrapper類,則判斷是有存在name,如果沒有name,則報錯。
  4. 如果有多個name,則判斷一下當前擴展點實現類上是否存在@Activate註解,如果存在,則把該類添加到cachedActivates中,cachedWrapperClasses是一個map。
  5. 最後,遍歷多個name,把每個name和對應的實現類存到extensionClasses中去,extensionClasses就是上文所提到的map。
    至此,加載類就走完了。
    回到createExtension(String name)方法中的邏輯,當前這個接口的所有擴展點實現類都掃描完了之後,就可以根據用戶所指定的名字,找到對應的實現類了,然後進行實例化,然後進行IOC(依賴注入)和AOP。
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
        if (!type.isAssignableFrom(clazz)) {
            throw new IllegalStateException("Error occurred when loading extension class (interface: " +
                    type + ", class line: " + clazz.getName() + "), class "
                    + clazz.getName() + " is not subtype of interface.");
        }
        // 當前接口手動指定了Adaptive類
        if (clazz.isAnnotationPresent(Adaptive.class)) {
            cacheAdaptiveClass(clazz);
        } else if (isWrapperClass(clazz)) {
            // 是一個Wrapper類
            cacheWrapperClass(clazz);
        } else {
            // 需要有無參的構造方法
            clazz.getConstructor();

            // 在文件中沒有name,但是在類上指定了Extension的註解上指定了name
            if (StringUtils.isEmpty(name)) {
                name = findAnnotationName(clazz);
                if (name.length() == 0) {
                    throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
                }
            }

            String[] names = NAME_SEPARATOR.split(name);
            if (ArrayUtils.isNotEmpty(names)) {
                // 緩存一下被Activate註解了的類
                cacheActivateClass(clazz, names[0]);

                // 有多個名字
                for (String n : names) {
                    // clazz: name
                    cacheName(clazz, n);
                    // name: clazz
                    saveInExtensionClass(extensionClasses, clazz, n);
                }
            }
        }
    }

Dubbo中的IOC

  1. 根據當前實例的類,找到這個類中的setter方法,進行依賴注入
  2. 先分析出setter方法的參數類型pt
  3. 在截取出setter方法所對應的屬性名property
  4. 調用objectFactory.getExtension(pt, property)得到一個對象,這裏就會從Spring容器或通過DubboSpi機制得到一個對象,比較特殊的是,如果是通過DubboSpi機制得到的
    對象,是pt這個類型的一個自適應對象。
  5. 再反射調用setter方法進行注入
private T injectExtension(T instance) {

        if (objectFactory == null) {
            return instance;
        }

        try {
            for (Method method : instance.getClass().getMethods()) {
                if (!isSetter(method)) {
                    continue;
                }

                // 利用set方法注入

                /**
                 * Check {@link DisableInject} to see if we need auto injection for this property
                 */
                if (method.getAnnotation(DisableInject.class) != null) {
                    continue;
                }
                // set方法中的參數類型
                Class<?> pt = method.getParameterTypes()[0];
                if (ReflectUtils.isPrimitives(pt)) {
                    continue;
                }

                try {
                    // 得到setXxx中的xxx
                    String property = getSetterProperty(method);
                    // 根據參數類型或屬性名,從objectFactory中獲取到對象,然後調用set方法進行注入
                    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;
    }

Dubbo中的AOP

dubbo中也實現了一套非常簡單的AOP,就是利用Wrapper,如果一個接口的擴展點中包含了多個Wrapper類,那麼在實例化完某個擴展點後,就會利用這些Wrapper類對這
個實例進行包裹,比如:現在有一個DubboProtocol的實例,同時對於Protocol這個接口還有很多的Wrapper,比如ProtocolFilterWrapper、ProtocolListenerWrapp
er,那麼,當對DubboProtocol的實例完成了IOC之後,就會先調用new ProtocolFilterWrapper(DubboProtocol實例)生成一個新的Protocol的實例,再對此實例進行IO
C,完了之後,會再調用new ProtocolListenerWrapper(ProtocolFilterWrapper實例)生成一個新的Protocol的實例,然後進行IOC,從而完成DubboProtocol實例的AOP。

AdaptiveClass

上文多次提到了Adaptive,表示一個接口的自適應類,這裏詳細的來講講。
通過getAdaptiveExtension方法獲得的實例就是Adaptive類的實例,在Dubbo中有兩種方式來針對某個接口得到一個Adaptive類,一種是在某個接口的實現類上指定一
個@Adaptive註解,那麼該類就是這個接口的Adaptive類,或者利用Dubbo的默認實現來得到一個Adaptive類,一個接口只能有一個Adaptive類。
如果是手動實現的Adaptive類,那麼自適應邏輯就是自己實現的。如果是有Dubbo默認實現的,那麼我們就看看Dubbo是如何實現Adaptive類的。

createAdaptiveExtensionClass方法

createAdaptiveExtensionClass方法就是Dubbo中默認生成Adaptive類實例的邏輯。說白了,這個實例就是當前這個接口的一個代理對象。比如下面的代碼:
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol protocol = extensionLoader.getAdaptiveExtension();
這個代碼就是Protocol接口的一個代理對象,那麼代理邏輯就是在new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate()方法中。

  1. type就是接口
  2. cacheDefaultName就是該接口默認的擴展點實現的名字
    看個例子,Protocol接口的Adaptive類:
package org.apache.dubbo.rpc;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
    
    public void destroy()  {
        throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
    }
    public int getDefaultPort()  {
        throw new UnsupportedOperationException("The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
    }
    
    public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {
        if (arg0 == null) 
            throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null) 
            throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
        
        org.apache.dubbo.common.URL url = arg0.getUrl();
        
        String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
        if(extName == null) 
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
        
        org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        
        return extension.export(arg0);
    }
    public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg1;
        String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
        if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
        org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }
}

可以看到,Protocol接口中有四個方法,但是隻有export和refer兩個方法進行代理。爲什麼?因爲Protocol接口中在export方法和refer方法上加了@Adaptive註解。
但是,不是只要在方法上加了@Adaptive註解就可以進行代理,還有其他條件,比如:

1. 該方法如果是無參的,那麼則會報錯
2. 該方法有參數,可以有多個,並且其中某個參數類型是URL,那麼則可以進行代理
3. 該方法有參數,可以有多個,但是沒有URL類型的參數,那麼則不能進行代理
4. 該方法有參數,可以有多個,沒有URL類型的參數,但是如果這些參數類型,對應的類中存在getUrl方法(返回值類型爲URL),那麼也可以進行代理
所以,可以發現,某個接口的Adaptive對象,在調用某個方法時,是通過該方法中的URL參數,通過調用ExtensionLoader.getExtensionLoade
r(com.luban.Car.class).getExtension(extName);得到一個擴展點實例,然後調用該實例對應的方法。

Activate擴展點

上文說到,每個擴展點都有一個name,通過這個name可以獲得該name對應的擴展點實例,但是有的場景下,希望一次性獲得多個擴展點實例,可以通過傳入多個name來獲取,可以通過識別URL上的信息來獲取:
extensionLoader.getActivateExtension(url, new String[]{“car1”, “car2”}); 這個可以拿到name爲car1和car2的擴展類實例,同時還會通過傳入的url尋找可用的擴展類, 怎麼找的呢?
在一個擴展點類上,可以添加@Activate註解,這個註解的屬性有:

1. String[] group():表示這個擴展點是屬於拿組的,這裏組通常分爲PROVIDER和CONSUMER,表示該擴展點能在服務提供者端,或者消費端使用
2. String[] value():指示的是URL中的某個參數key,當利用getActivateExtension方法來尋找擴展點時,如果傳入的url中包含的參數的所有key中,包括了當前擴展點中的
value值,那麼則表示當前url可以使用該擴展點。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章