詳解Apache Dubbo的SPI實現機制

vivo網際網路技術發表於2021-06-21

一、SPI

SPI全稱為Service Provider Interface,對應中文為服務發現機制。SPI類似一種可插拔機制,首先需要定義一個介面或一個約定,然後不同的場景可以對其進行實現,呼叫方在使用的時候無需過多關注具體的實現細節。在Java中,SPI體現了面向介面程式設計的思想,滿足開閉設計原則。

1.1 JDK自帶SPI實現

從JDK1.6開始引入SPI機制後,可以看到很多使用SPI的案例,比如最常見的資料庫驅動實現,在JDK中只定義了java.sql.Driver的介面,具體實現由各資料庫廠商來提供。下面一個簡單的例子來快速瞭解下Java SPI的使用方式:

1)定義一個介面

package com.vivo.study
public interface Car {
void getPrice();
}


2)介面實現

package com.vivo.study.impl
 
/**
 * 實現一
 *
 */
public class AudiCar implements Car {
    @Override
    public void getPrice() {
        System.out.println("Audi A6L's price is  500000 RMB.");
    }
}
 
package com.vivo.study.impl
/**
 * 實現二
 *
 */
public class BydCar implements Car {
    @Override
    public void getPrice() {
        System.out.println("BYD han's price is 220000 RMB.");
    }
}


3)掛載擴充套件類資訊

在META-INF/services目錄下以介面全名為檔名的文字檔案,對應此處即在META-INF/services目錄下建立一個檔名為com.vivo.study.Car的檔案,檔案內容如下:

com.vivo.study.impl.AudiCar
com.vivo.study.impl.BydCar


4)使用

public class SpiDemo {
public static void main(String[] args) {
        ServiceLoader<Car> load = ServiceLoader.load(Car.class);
        Iterator<Car> iterator = load.iterator();
while (iterator.hasNext()) {
            Car car = iterator.next();
            car.getPrice();
        }
    }
}


上面的例子簡單的介紹了JDK SPI機制的使用方式,其中最關鍵的類為ServiceLoader,通過ServiceLoader類來載入介面的實現類,ServiceLoader是Iterable介面的實現類,對於ServiceLoader載入的詳細過程此處不展開。

JDK對SPI的載入實現存在一個較為突出的小缺點,無法按需載入實現類,通過ServiceLoader.load載入時會將檔案中的所有實現都進行例項化,如果想要獲取具體某個具體的實現類需要進行遍歷判斷。

1.2 Dubbo SPI

SPI擴充套件是Dubbo的最大的優點之一,支援協議擴充套件、呼叫攔截擴充套件、引用監聽擴充套件等等。在Dubbo中,根據不同的擴充套件定位,擴充套件檔案分別被放置在META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/這三個路徑下。

Dubbo中有直接使用JDK SPI實現的方式,比如org.apache.dubbo.common.extension.LoadingStrategy放在META-INF/services/路徑下,但大多情況下都是使用其自身對JDK SPI的實現的一種優化方式,可稱為Dubbo SPI,也就是本文要講解的點。

相比於JDK的SPI的實現,Dubbo SPI具有以下特點:

配置形式更靈活:支援以key:value的形式在檔案裡配置類似name:xxx.xxx.xxx.xx,後續可以通過name來進行擴充套件類按需精準獲取。

快取的使用:使用快取提升效能,保證一個擴充套件實現類至多會載入一次。

對擴充套件類細分擴充套件:支援擴充套件點自動包裝(Wrapper)、擴充套件點自動裝配、擴充套件點自適應(@Adaptive)、擴充套件點自動啟用(@Activate)。

Dubbo對擴充套件點的載入主要由ExtensionLoader這個類展開。

二、載入-ExtensionLoader

ExtensionLoader在Dubbo裡的角色類似ServiceLoader在JDK中的存在,用於載入擴充套件類。在Dubbo原始碼裡,隨處都可以見到ExtensionLoader的身影,比如在服務暴露裡的關鍵類ServiceConfig中等,弄清楚ExtensionLoader的實現細節對閱讀Dubbo原始碼有很大的幫助。

2.1 獲取ExtensionLoader的例項

ExtensionLoader沒有提供共有的建構函式,

只能通過ExtensionLoader.getExtensionLoader(Class type)來獲取ExtensionLoader例項。

public
 
    // ConcurrentHashMap快取,key -> Class value -> ExtensionLoader例項
    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);
 
    private ExtensionLoader(Class<?> type) {
        this.type = type;
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }
 
    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!");
        }
 
        // 檢查介面是否是被@SPI註解修飾,如果沒有則丟擲異常
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type (" + type +
                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        }
 
        // 從快取裡取,沒有則初始化一個並放入快取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;
    }
}


上面的程式碼展示了獲取ExtensionLoader例項的過程,可以看出,每一個被@SPI修飾的介面都會對應同一個ExtensionLoader例項,且對應ExtensionLoader只會被初始化一次,並快取在ConcurresntHashMap中。

2.2 載入擴充套件類

載入擴充套件類入口,當使用ExtensionLoader時,getExtensionName、getActivateExtension或是getDefaultExtension都要經過getExtensionClasses方法來載入擴充套件類,如下圖;

getExtensionClasses方法呼叫的路徑如下圖,getExtensionClasses是載入擴充套件類的一個起點,會首先從快取中獲取,如果快取中沒有則通過loadExtensionClasses方法來載入擴充套件類,所以說實際上的載入邏輯入口在loadExtensionClasses。

getExtensionClasses
  |->loadExtensionClasses
    |->cacheDefaultExtensionName
    |->loadDirectory
      |->loadResource
        |->loadClass

2.2.1 loadExtensionClasses載入擴充套件類

由於整個載入過程設計的原始碼較多,因此用一個流程圖來進行描述,具體細節可以結合原始碼進行檢視。

loadExtensionClasses主要做了以下這幾件事:

預設副檔名:

抽取預設擴充套件實現名並快取在ExtensionLoader裡的cachedDefaultName,預設副檔名配置通過@SPI註解在介面上配置,如配置@SPI("defaultName")則預設副檔名為defaultName。

載入擴充套件類資訊:

從META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/這三個路徑下尋找以類的全路徑名命名的檔案,並逐行讀取檔案裡的內容。

載入class並快取:

對擴充套件類分為自適應擴充套件實現類(被@Adaptive修飾的實現類)、包裝類(擁有一個只有一個為這個介面型別的引數的構造方法)、普通擴充套件類,其中普通擴充套件類中又包含自動啟用擴充套件類(被@Activate修飾的類)和真普通的類,對自適應擴充套件實現類、包裝類、自動啟用擴充套件類這三種型別的類分別載入並分別快取到cachedAdaptiveClass、cachedActivates、cachedWrapperClasses。

返回結果Map<String, Class<?>>:

結果返回Map,其中key對應擴充套件檔案裡配置的name,value對應擴充套件的類class,最後在getExtensionClasses方法裡會將此結果放入快取cachedClasses中。此結果Map中包含除了自適應擴充套件實現類和包裝實現類的其他所用的擴充套件類名與對應類的對映關係。

通過loadExtensionClasses方法把擴充套件類(Class物件)都載入到相應的快取中,是為了方便後面例項化擴充套件類物件,通過newInstance()等方法來例項化。

2.2.2擴充套件包裝類

什麼是擴充套件包裝類?是不是類名結尾包含了Wrapper的類就是擴充套件包裝類?

在Dubbo SPI介面的實現擴充套件類中,如果此類包含一個此介面作為引數的構造方法,則為擴充套件包裝類。擴充套件包裝類的作用是持有具體的擴充套件實現類,可以一層一層的被包裝,作用類似AOP。

包裝擴充套件類的作用是類似AOP,方便擴充套件增強。具體實現程式碼如下:

從程式碼中可以得出,可以通過boolean wrap選擇是否使用包裝類,預設情況下為true;如果有擴充套件包裝類,實際的實現類會被包裝類按一定的順序一層一層包起來。

如Protocol的實現類ProtocolFilterWrapper、ProtocolListenerWrapper都是擴充套件包裝類。

2.2.3自適應擴充套件實現類

2.2.3.1 @Adaptive

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
String[] value() default {};
}


從原始碼以及原始碼註釋中可以得出以下幾點:

Adaptive是一個註解,可以修飾類(介面,列舉)和方法。

此註解的作用是為ExtensionLoader注入擴充套件例項提供有用的資訊。

從註釋中理解value的作用:

value可以決定選擇使用具體的擴充套件類。

通過value配置的值key,在修飾的方法的入參org.apache.dubbo.common.URL中通過key獲取到對應的值value,根據value作為extensionName去決定使用對應的擴充套件類。

如果通過2沒有找到對應的擴充套件,會選擇預設的擴充套件類,通過@SPI配置預設擴充套件類。

2.2.3.2 @Adaptive簡單例子

由於@Adaptive修飾類時比較好理解,這裡舉一個@Adaptive修飾方法的例子,使用@Adaptive修飾方法的這種情況在Dubbo也是隨處可見。

/**
* Dubbo SPI 介面
*/
@SPI("impl1")
public interface SimpleExt {
    @Adaptive({"key1", "key2"})
    String yell(URL url, String s);
}

如果呼叫

ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension().yell(url, s)方法,最終呼叫哪一個擴充套件類的例項去執行yell方法的流程大致為:先獲取擴充套件類的名稱extName(對應上面說的name:class中的name),然後通過extName來獲取對應的類Class,再例項化進行呼叫。所以關鍵的步驟在怎麼得到extName,上面的這個例子得到extName的流程為:

通過url.getParameters.get("key1")獲取,

沒有獲取到則用url.getParameters.get("key2"),如果還是沒有獲取到則使用impl1對應的實現類,最後還是沒有獲取到則拋異常IllegalStateException。

可以看出,@Adaptive的好處就是可以通過方法入參決定具體呼叫哪一個實現類。下面會對@Adaptive的具體實現進行詳細分析。

2.2.3.3 @Adaptive載入流程

流程關鍵點說明:

1)黃色標記的,cachedAdaptiveClass是在ExtensionLoader#loadClass方法中載入Extension類時快取的。

2)綠色標記的,如果Extension類中存在被@Adaptive修飾的類時會使用該類來初始化例項。

3)紅色標記的,如果Extension類中不存在被@Adaptive修飾的類時,則需要動態生成程式碼,通過javassist(預設)來編譯生成Xxxx$Adaptive類來例項化。

4)例項化後通過injectExtension來將Adaptive例項的Extension注入(屬性注入)。

後續圍繞上述的關鍵點3詳細展開,關鍵點4此處不展開。

動態生成Xxx$Adaptive類:下面的程式碼為動態生成Adaptive類的相關程式碼,具體生成程式碼的細節在AdaptiveClassCodeGenerator#generate中

public class ExtensionLoader<T> {
// ...
private Class<?> getAdaptiveExtensionClass() {
// 根據對應的SPI檔案載入擴充套件類並快取,細節此處不展開
        getExtensionClasses();
// 如果存在被@Adaptive修飾的類則直接返回此類
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
        }
// 動態生成Xxxx$Adaptive類
return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }
private Class<?> createAdaptiveExtensionClass() {
// 生成Xxxx$Adaptive類程式碼,可自行加日誌或斷點檢視生成的程式碼
        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
        ClassLoader classLoader = findClassLoader();
// 獲取動態編譯器,預設為javassist
        org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
    }
}

AdaptiveClassCodeGenerator#generate生成code的方式是通過字串拼接,大量使用String.format,整個程式碼過程比較繁瑣,可通過debug去了解細節。

最關鍵的的部分是生成被@Adaptive修飾的方法的內容,也就是最終呼叫例項的@Adaptive方法時,可通過引數來動態選擇具體使用哪個擴充套件例項。下面對此部分進行分析:

public class AdaptiveClassCodeGenerator {
// ...
/**
     * generate method content
     */
private String generateMethodContent(Method method) {
// 獲取方法上的@Adaptive註解
        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
        StringBuilder code = new StringBuilder(512);
if (adaptiveAnnotation == null) {
// 方法時沒有@Adaptive註解,生成不支援的程式碼
return generateUnsupported(method);
        } else {
// 方法引數裡URL是第幾個引數,不存在則為-1
int urlTypeIndex = getUrlTypeIndex(method);
// found parameter in URL type
if (urlTypeIndex != -1) {
// Null Point check
                code.append(generateUrlNullCheck(urlTypeIndex));
            } else {
// did not find parameter in URL type
                code.append(generateUrlAssignmentIndirectly(method));
            }
// 獲取方法上@Adaptive配置的value
// 比如 @Adaptive({"key1","key2"}),則會返回String陣列{"key1","key2"}
// 如果@Adaptive沒有配置value,則會根據簡寫介面名按駝峰用.分割,比如SimpleExt對應simple.ext
            String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
// 引數裡是否存在org.apache.dubbo.rpc.Invocation
            boolean hasInvocation = hasInvocationArgument(method);
            code.append(generateInvocationArgumentNullCheck(method));
// 生成String extName = xxx;的程式碼 ,extName用於獲取具體的Extension例項
            code.append(generateExtNameAssignment(value, hasInvocation));
// check extName == null?
            code.append(generateExtNameNullCheck(value));
            code.append(generateExtensionAssignment());
// return statement
            code.append(generateReturnAndInvocation(method));
        }
return code.toString();
    }
}

上述生成Adaptive類的方法內容中最關鍵的步驟在生成extName的部分,也就是generateExtNameAssignment(value,hasInvocation),此方法if太多了(有點眼花繚亂)。

以下列舉幾個例子對此方法的實現流程進行簡單展示:假設方法中的引數不包含org.apache.dubbo.rpc.Invocation,包含org.apache.dubbo.rpc.Invocation的情況會更加複雜。

1)方法被@Adaptive修飾,沒有配置value,且在介面@SPI上配置了預設的實現


@SPI("impl1")
public interface SimpleExt {
@Adaptive
String echo(URL url, String s);
}

對應生成extName的程式碼為:

String extName = url.getParameter("simple.ext", "impl1")

2)方法被@Adaptive修飾,沒有配置value,且在介面@SPI上沒有配置預設的實現

@SPI("impl1")
public interface SimpleExt {
@Adaptive({"key1", "key2"})
String yell(URL url, String s);
}

對應生成extName的程式碼為:

String extName = url.getParameter( "simple.ext")

3)方法被@Adaptive修飾,配置了value(假設兩個,依次類推),且在介面@SPI上配置了預設的實現

@SPI
public interface SimpleExt {
@Adaptive({"key1", "key2"})
String yell(URL url, String s);
}

對應生成extName的程式碼為:

String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));


4)方法被@Adaptive修飾,配置了value(假設兩個,依次類推),且在介面@SPI沒有配置預設的實現

@SPI
public interface SimpleExt {
@Adaptive({"key1", "key2"})
String yell(URL url, String s);
}

對應生成extName的程式碼為:

String extName = url.getParameter("key1", url.getParameter("key2"));

完整的生成類可參見附錄。

2.2.4自動啟用擴充套件類

如果你有擴充套件實現過Dubbo的Filter,那麼一定會對@Activate很熟悉。@Activate註解的作用是可以通過給定的條件來自動啟用擴充套件實現類,通過ExtensionLoader#getActivateExtension(URL,String, String)方法可以找到指定條件下需要啟用的擴充套件類列表。

下面以一個例子來對@Activate的作用進行說明,在Consumer呼叫Dubbo介面時,會經過消費方的過濾器鏈以及提供方的過濾器鏈,在Provider暴露服務的過程中會拼接需要使用哪些Filter。

對應原始碼中的位置在ProtocolFilterWrapper#buildInvokerChain(invoker, key, group)方法中。

// export:key-> service.filter ; group-> provider
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
    // 在Provider暴露服務服務export時,會根據獲取Url中的service.filter對應的值和group=provider來獲取啟用對應的Filter
    List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
}

ExtensionLoader#getActivateExtension(URL, String, String)是怎麼根據條件來自動啟用對應的擴充套件類列表的可以自行檢視該方法的程式碼,此處不展開。

三、總結

本文主要對Dubbo SPI機制的擴充套件類載入過程通過ExtensionLoader類原始碼來進行總結,可以概況為以下幾點:

1.Dubbo SPI結合了JDK SPI的實現,並在此基礎上進行優化,如精準按需載入擴充套件類、快取提升效能。

2.分析ExtensionLoader載入擴充套件類的過程,載入META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/這三個路徑下的檔案,並分類快取在ExtensionLoader例項。

3.介紹擴充套件包裝類及其實現過程,擴充套件包裝類實現了類似AOP的功能。

4.自適應擴充套件類,分析@Adptive修飾方法時動態生成Xxx$Adaptive類的過程,以及通過引數自適應選擇擴充套件實現類完成方法呼叫的案例介紹。

簡單介紹自動啟用擴充套件類及@Activate的作用。

四、附錄

4.1 Xxx$Adaptive完整案例

@SPI介面定義

@SPI("impl1")
public interface SimpleExt {
    // @Adaptive example, do not specify a explicit key.
    @Adaptive
    String echo(URL url, String s);
 
    @Adaptive({"key1", "key2"})
    String yell(URL url, String s);
 
    // no @Adaptive
    String bang(URL url, int i);
}


生成的Adaptive類程式碼

package org.apache.dubbo.common.extension.ext1;
  
import org.apache.dubbo.common.extension.ExtensionLoader;
  
public class SimpleExt$Adaptive implements org.apache.dubbo.common.extension.ext1.SimpleExt {
  
    public java.lang.String yell(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg0;
        String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([key1, key2])");
        org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt)ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);
        return extension.yell(arg0, arg1);
    }
  
    public java.lang.String echo(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg0;
        String extName = url.getParameter("simple.ext", "impl1");
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([simple.ext])");
        org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt) ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);
        return extension.echo(arg0, arg1);
    }
  
    public java.lang.String bang(org.apache.dubbo.common.URL arg0, int arg1) {
        throw new UnsupportedOperationException("The method public abstract java.lang.String org.apache.dubbo.common.extension.ext1.SimpleExt.bang(org.apache.dubbo.common.URL,int) of interface org.apache.dubbo.common.extension.ext1.SimpleExt is not adaptive method!");
    }
  
}

作者:vivo網際網路伺服器團隊-Ning Peng

相關文章