Dubbo系列之 (一)SPI擴充套件

wsMrLin發表於2020-08-06

一、基礎鋪墊

1、@SPI 、@Activate、 @Adaptive

  • a、對於 @SPI,Dubbo預設的特性擴充套件介面,都必須打上這個@SPI,標識這是個Dubbo擴充套件點。如果自己需要新增dubbo的擴充套件點我們就需要新增介面,並且這個介面必須標註@SPI.

  • b、@SPI可以填入一個值,這個值代表某個擴充套件點的名稱,一般作為預設擴充套件點。

  • c、@Activate,這個註解可以打在方法上或者類上,主要的作用是自動的啟用某個擴充套件點。

  • d、@Adaptive 註解為自適應註解,該註解可以標註在方法上或者類上。這個註解特別有用,當它標註在類上時,說明該類是AdaptiveExtension擴充套件,如果標註在方法上,說明該方法可以根據引數自適應的返回值。需要注意的是該方法的引數必須有一個是URL型別。它是通過這個URL所帶的引數進行自適應返回。當@Adaptive標註在方法上時,dubbo的spi擴充套件機制會動態生成一個XXXX$Adaptive擴充套件類,然後實現被@Adaptive標註的方法。內部的實現是通過ExtensionLoader.getActivateExtension 和傳入的URL 來返回具體哪個擴充套件點實現。

2、ExtensionLoader

  • a、該方法就是我們對dubbo 擴充套件點的入口,他跟java的spi的ServiceLoader功能是一樣的,都是載入某個擴充套件點的所有擴充套件具體實現。

  • b、ExtensionLoader 查詢具體擴充套件點實現,是去查詢類路徑下 META-INF/dubbo, META-INF/dubbo/internal,META-INF/services目錄下的擴充套件點介面名相同的檔案。並載入檔案內的具體擴充套件點。

  • c、入口是靜態方法ExtensionLoader.getExtensionLoader(),返回某個擴充套件點的ExtensionLoader<?>的具體事例

  • d、ExtensionLoader<?>.getAdaptiveExtension 是得到自適應的擴充套件點類,如果某個類打上@Adaptive,則返回該類,否則系統通過javassit建立一個。

  • e、ExtensionLoader<?>.getActivateExtension(URL url,key) 可以根據URL.get(key)的值(該值就是擴充套件點名)得到一個啟用的擴充套件點。註解 @Activate 標註的為預設的啟用擴充套件點,可以通過-default來不啟用預設擴充套件點。

  • f、ExtensionLoader<?>.getExtension 通過name 得到想要的擴充套件點,並且如果有Wrapper型別擴充套件點,會對其進行包裹返回。

  • g、當一個具體的擴充套件點的建構函式的引數是其擴充套件點介面型別,即建構函式是TypeImpl(IType type) && TypeImpl implements IType 時,可以稱這種擴充套件點為Wraper擴充套件點。那麼所返回的非Wraper擴充套件點都會被包裹成Wraper型別,如果有多個Wraper的擴充套件點,那麼會一層一層的包裹,但是誰包裹誰,不固定。

  • h、當一個具體的擴充套件點的實現有其setter方法且沒有被@DisableInject標註和注入型別不是原始型別(long,int,short,String,BigDecimal等)時,該setter方法會被呼叫,注入其需要的Object,而Object的例項原來就是通過ExtensionFactory工廠。

3、ExtensionFactory

  • a、首先ExtensionFactory 這個也是SPI的擴充套件點,它也是通過ExtensionLoader來載入的,他載入的時機是某個具體其他擴充套件點通過ExtensionLoader.getExtensionLoader()載入時,內部會呼叫ExtensionLoader私有建構函式,在這個建構函式內部來載入這個工廠,並且載入的這個工廠是自適應工程例項。

  • b、ExtensionLoader獲取具體擴充套件點的來源就是通過ExtensionFactory擴充套件點的自適應擴充套件點AdaptiveExtensionFactory。

  • c、AdaptiveExtensionFactory 的實現可以知道擴充套件點的例項注入來源,包括Spring 容器(通過SpringExtensionFactory)和 SpiExtensionFactory。

二、特性測試

接著我們對上面的特性,進行一輪測試,驗證其結果。
新建一個擴充套件介面 

 

package org.apache.dubbo.mytest;
 @SPI("first")
 public interface InvokerProtocol {
     @Adaptive("getInvoker")
     Invoker getInvoker(String name, URL url);
 }

  

預設的擴充套件點為first,並且getInvoker方法被@Adaptive標註。接著我們在META-INF/services 下新建一個檔名為org.apache.dubbo.mytest.InvokerProtocol檔案,裡面填寫這個擴充套件點的實現,如:
first=org.apache.dubbo.mytest.protocol.FirstInvokerProtocol

測試1

 

@Test
 public  void testDefaultExtension(){
     ExtensionLoader<InvokerProtocol> extensionLoader = ExtensionLoader.getExtensionLoader(InvokerProtocol.class);
     InvokerProtocol firstInvokerProtocol = extensionLoader.getExtension("first");
     Assert.check(firstInvokerProtocol instanceof FirstInvokerProtocol); //true
     InvokerProtocol defaultExtension = extensionLoader.getDefaultExtension(); 
     Assert.check(defaultExtension ==firstInvokerProtocol); //true
 }

  

從上面我們驗證了SPI("first")標註的名稱,指定為預設的擴充套件實現類。接著我們在org.apache.dubbo.mytest.InvokerProtocol檔案,接著新增一個擴充套件點,
second=org.apache.dubbo.mytest.protocol.SecondInvokerProtocol,並把SecondInvokerProtocol類打上@Activate("second")註解。注意,FirstInvokerProtocol沒有打上@Activate註解。接著再一次測試

測試2

 

@Test
public  void testActivateExtension(){
    ExtensionLoader<InvokerProtocol> extensionLoader = ExtensionLoader.getExtensionLoader(InvokerProtocol.class);
    
    URLBuilder urlBuilder = new URLBuilder();
    urlBuilder.setProtocol("test");
    urlBuilder.setPath("defaultInvokerProtocol");
    urlBuilder.addParameter("invokerProtocol", "first");
    List<InvokerProtocol> invokerProtocol = extensionLoader.getActivateExtension(urlBuilder.build(), "invokerProtocol");
    Assert.check(invokerProtocol.size() == 2); //true
}

  

從該上面例子,驗證了getActivateExtension 可以通過URL 來啟用指定的擴充套件實現,並且還會返回被@Activate的擴充套件點類,說明@Activate被標註的擴充套件點類被預設啟用。當我們在org.apache.dubbo.mytest.InvokerProtocol檔案,接著新增二個擴充套件點,如下
firstwraper=org.apache.dubbo.mytest.protocol.FirstWraperInvokerProtocol
secondwraper=org.apache.dubbo.mytest.protocol.SecondWraperInvokerProtocol

這個2個擴充套件點是包裹擴充套件點,它的建構函式如下:

 

public FirstWraperInvokerProtocol(InvokerProtocol invokerProtocol) {
        this.invokerProtocol = invokerProtocol;
}

  

這時,我們執行test1的單測,發現斷言失敗,並且知道返回的擴充套件點例項被這2個wraper擴充套件點包裹。

測試3

 

@Test
 public  void testDefaultExtension(){
     ExtensionLoader<InvokerProtocol> extensionLoader = ExtensionLoader.getExtensionLoader(InvokerProtocol.class);
     InvokerProtocol firstInvokerProtocol = extensionLoader.getExtension("first");
     InvokerProtocol defaultExtension = extensionLoader.getDefaultExtension(); 
 }

  

從上面,得知firstInvokerProtocol 型別是FirstWraperInvokerProtocol 並且持有SecondWraperInvokerProtocol型別,而SecondWraperInvokerProtocol最終持有我們的FirstInvokerProtocol.

接著測試,我們獲取下AdaptiveExtension擴充套件,目前我們只在擴充套件點的介面方法上打上@Adaptive,即getInvoker方法。

測試4

 

@Test
public  void testExtensionAdaptive(){
     InvokerProtocol adaptiveExtension = ExtensionLoader.getExtensionLoader(InvokerProtocol.class).getAdaptiveExtension();
     System.out.println(adaptiveExtension);
}

  

可以發現框架動態為我們生產了一個類名為InvokerProtocol$Adaptive的自適應擴充套件點類。並且實現其getInvoker方法,方法內容為(整理了下)。

 

public class InvokerProtocol$Adaptive implements InvokerProtocol {
    public Invoker getInvoker(String arg0, URL arg1) {
        if (arg1 == null)
            throw new IllegalArgumentException("url == null");
        URL url = arg1;
        String extName = url.getParameter("getInvoker", "first");
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.mytest.InvokerProtocol) name " +
                    "from url (" + url.toString() + ") use keys([getInvoker])");
        InvokerProtocol extension = ExtensionLoader.getExtensionLoader(InvokerProtocol.class).getExtension(extName);
        return extension.getInvoker(arg0, arg1);
    }
}

  

我們知道其內部通過URL引數,根據url.getParameter得到我們我們需要的具體需要的擴充套件點名,接著ExtensionLoader.getExtension得到具體擴充套件點。

接著我們自己實現一個名為AdaptiveInvokerProtocol的自適應的擴充套件點類,並把AdaptiveInvokerProtocol標註上@Adaptive,接著在org.apache.dubbo.mytest.InvokerProtocol檔案下,填入:
adaptive=org.apache.dubbo.mytest.protocol.AdaptiveInvokerProtocol
再一次執行

測試5

 

@Test
public  void testExtensionAdaptive(){
     InvokerProtocol adaptiveExtension = ExtensionLoader.getExtensionLoader(InvokerProtocol.class).getAdaptiveExtension();
     System.out.println(adaptiveExtension);
}

  

可以知道,自適應擴充套件點變為我們實現的AdaptiveInvokerProtocol,框架沒有在為我們建立一個自適應的擴充套件點類。

三、原始碼跟蹤

我們通過如果下幾個問題來跟蹤我們的程式碼。

1、為什麼擴充套件點需要標註@SPI

我們在獲取擴充套件點的入口為ExtensionLoader.getExtensionLoader。在這個靜態方法裡做了@SPI註解判斷。

// 說明該介面上是否有SPI註解
 private static <T> boolean withExtensionAnnotation(Class<T> type) {
        return type.isAnnotationPresent(SPI.class);
 }
 @SuppressWarnings("unchecked")
 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!");
        }
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type (" + type +
                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        }
        // 併為每種型別的擴充套件建立一個具體型別的ExtensionLoader<?>的例項,並放入EXTENSION_LOADERS快取中。
        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;
  }

  

2、@Activate註解的作用如何實現的

我們通過 getActivateExtension 得到啟用的擴充套件點,看下面的具體註釋:

 

    public List<T> getActivateExtension(URL url, String[] values, String group) {
        List<T> activateExtensions = new ArrayList<>();
        List<String> names = values == null ? new ArrayList<>(0) : asList(values);
        // 如果傳入的值,沒有包括-default,說明不排除框架,預設啟用的擴充套件點
        if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
            getExtensionClasses(); // 載入具體擴充套件點類,在載入過程中會把打上@Activate註解的類放到快取cachedActivates中,其中key為擴充套件點名,Value為Activate註解
            for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
                String name = entry.getKey();
                Object activate = entry.getValue();

                String[] activateGroup, activateValue;

                //得到Activate註解上的group和value.
                if (activate instanceof Activate) {
                    activateGroup = ((Activate) activate).group();
                    activateValue = ((Activate) activate).value();
                } else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
                    activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
                    activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
                } else {
                    continue;
                }
                //判斷是否匹配,如果匹配加到activateExtensions。
                if (isMatchGroup(group, activateGroup)
                        && !names.contains(name)
                        && !names.contains(REMOVE_VALUE_PREFIX + name)
                        && isActive(activateValue, url)) {
                    activateExtensions.add(getExtension(name));
                }
            }
            activateExtensions.sort(ActivateComparator.COMPARATOR);
        }

        // 上面的if獲取的是滿足條件預設啟用的擴充套件點類。這裡是URL引數直接指定需要的擴充套件點放到loadedExtensions
        List<T> loadedExtensions = new ArrayList<>();
        for (int i = 0; i < names.size(); i++) {
            String name = names.get(i);

            if (!name.startsWith(REMOVE_VALUE_PREFIX)
                    && !names.contains(REMOVE_VALUE_PREFIX + name)) {
                if (DEFAULT_KEY.equals(name)) {
                    if (!loadedExtensions.isEmpty()) {
                        activateExtensions.addAll(0, loadedExtensions);
                        loadedExtensions.clear();
                    }
                } else {
                    loadedExtensions.add(getExtension(name));
                }
            }
        }
        if (!loadedExtensions.isEmpty()) {
            activateExtensions.addAll(loadedExtensions);
        }
        // 接著返回預設的啟用的擴充套件點+使用者指定的擴充套件點。
        return activateExtensions;
    }

  

3、@Adaptive註解的作用如何實現的

自適應的擴充套件點實現類只能有一個,我們通過getAdaptiveExtension來獲取,內容如下:

 

    public T getAdaptiveExtension() {
        //首先cachedAdaptiveInstance用來存放自適應擴充套件點例項的Holder
        Object instance = cachedAdaptiveInstance.get();
        if (instance == null) { // 如果不存在
            if (createAdaptiveInstanceError != null) {
                throw new IllegalStateException("Failed to create adaptive instance: " +
                        createAdaptiveInstanceError.toString(),
                        createAdaptiveInstanceError);
            }

            synchronized (cachedAdaptiveInstance) { //鎖住該Holder
                instance = cachedAdaptiveInstance.get();//再次獲取,應為該方法可能是併發環境下,所以鎖定雙層判斷
                if (instance == null) {
                    try {
                        instance = createAdaptiveExtension(); //這裡建立一個自適應擴充套件點例項
                        cachedAdaptiveInstance.set(instance);// 並放入Holder中
                    } catch (Throwable t) {
                        createAdaptiveInstanceError = t;
                        throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
                    }
                }
            }
        }

        return (T) instance;
    }

  

接著我們來看下,這個createAdaptiveExtension是如何建立的。

    private T createAdaptiveExtension() {
        try {
            // 步驟非常清晰,首先通過getAdaptiveExtensionClass得到自適應的擴充套件點的Class,然後呼叫newInstance得到一個例項
            //接著呼叫 injectExtension方法為這個例項注入相關需要的熟悉,通過這個例項的setter方法。
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
        }
    }

  

    private Class<?> getAdaptiveExtensionClass() {
        getExtensionClasses(); // 這裡還是通過getExtensionClasses載入擴充套件類,之後分析
        if (cachedAdaptiveClass != null) { //當呼叫完 getExtensionClasses方法後,如果存在自適應的擴充套件點類,會被賦值給cachedAdaptiveClass,那麼直接返回,
            return cachedAdaptiveClass;
        }
        // 如果沒有找到被@Adaptive標註的類,為其建立一個自適應的擴充套件點類
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

  

// 建立的一個自適應的擴充套件點的程式碼比較簡單,就是StringBuilder 建立一個java類檔案內容,然後通不過dubbo提供的compiler工具進行編譯為位元組碼,通過javaassit完成。

 

    private Class<?> createAdaptiveExtensionClass() {
        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
        System.out.println(code);
        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);
    }

  

4、ExtensionLoader是如何處理Wraper擴充套件點的

在通過getExtensionClasses 載入擴充套件點類時,會判斷這個類時否是Wraper,判斷如下:

 

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

  

即建構函式的引數否為自身擴充套件點,並把這些類放入一個名為cachedWrapperClasses的快取中。

 

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

  

當我們呼叫ExtensionLoader<?>.getExtension 時,

 

    public T getExtension(String name) {
        if (StringUtils.isEmpty(name)) {
            throw new IllegalArgumentException("Extension name == null");
        }
        // 如果名稱為true,返回預設的擴充套件點
        if ("true".equals(name)) {
            return getDefaultExtension();
        }
        //從holder中得到名為name的擴充套件點
        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中
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }

 

    private T createExtension(String name) {
        // 呼叫getExtensionClasses 得到擴充套件點類
        Class<?> clazz = getExtensionClasses().get(name);
        if (clazz == null) {
            throw findException(name);
        }
        try {
            T instance = (T) EXTENSION_INSTANCES.get(clazz);
            if (instance == null) {
                // 為空,clazz.newInstance()一個實力
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            }
            // 為擴充套件點setter注入
            injectExtension(instance);

            // 這裡,對包裹擴充套件點進行for迴圈,然後呼叫wrapperClass.getConstructor(type).newInstance ,一次迴圈包裹物件
            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);
        }
    }

  

 

5、ExtensionLoader是如何注入擴充套件點的。

在 injectExtension方法中可以得到答案,這個方法被createAdaptiveExtension和createExtension呼叫,都是在建立完具體擴充套件點例項後,對其注入。

    private T injectExtension(T instance) {

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

        try {
            for (Method method : instance.getClass().getMethods()) {
                if (!isSetter(method)) {
                    continue;
                }
                /**
                 * 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;
    }

  

6、ExtensionFactory工廠在ExtensionLoader內部的運用

在injectExtension方法中,我們看到objectFactory.getExtension(pt, property),即從objectFactory得到需要注入的屬性物件。objectFactory 的例項化,我們是在ExtensionLoader私有建構函式中。

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

  

而私有建構函式在getExtensionLoader的靜態方法上呼叫。並且objectFactory還是自適應的擴充套件點實現。所以我們在載入一個擴充套件點時,首先框架內部會例項化這個ExtensionFactory工廠。並且這個自適應工程名為AdaptiveExtensionFactory,內容就是就是組合Spi和Spring的ExtensionFactory。

7、getExtensionClasses 的流程是什麼。

流程比較清晰,不具體的貼程式碼了。簡潔流程呼叫如下 :
getExtensionClasses (這裡是在快取cachedClasses取,快取取不到下一步) ->loadExtensionClasses(主要從META-INF/services,META-INF/dubbo,META-INF/dubbo/internal檔案下找具體的擴充套件點類)
在這過程中把一些不同型別的擴充套件點放入到快取中。具體看loadClass

 

    private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                           boolean overridden) 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標註,放入快取cachedAdaptiveClass
        if (clazz.isAnnotationPresent(Adaptive.class)) {
            cacheAdaptiveClass(clazz, overridden);
            // 如果該類是Wrapper型別,放入快取cachedWrapperClasses
        } else if (isWrapperClass(clazz)) {
            cacheWrapperClass(clazz);
        } else {
            //這裡判斷是否有預設建構函式,
            clazz.getConstructor();
            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)) {
                cacheActivateClass(clazz, names[0]);
                for (String n : names) {
                    cacheName(clazz, n);
                    saveInExtensionClass(extensionClasses, clazz, n, overridden);
                }
            }
        }
    }

  

通過loadClass我們知道,ExtensionLoader內部快取主要包括如下三個:

  • cachedClasses:存放沒有被@Adaptive標註的且不是Wrapper擴充套件點類

  • cachedWrapperClasses :存放Wrapper型別的擴充套件點類

  • cachedAdaptiveClass :存放@Adaptive標註的類,沒有被@Adaptive標註時,系統自動建立一個。

相關文章