0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Dubbo源碼(一)SPI vs Spring

冬至子 ? 來源:程序猿阿越 ? 作者:程序猿阿越 ? 2023-05-23 14:44 ? 次閱讀

ExtensionLoader

1、私有構(gòu)造

ExtensionLoader的構(gòu)造方法傳入一個Class,代表當(dāng)前擴(kuò)展點(diǎn)對應(yīng)的SPI接口。

每個ExtensionLoader實(shí)例管理自己Class的擴(kuò)展點(diǎn),包括加載、獲取等等。

圖片

type:當(dāng)前擴(kuò)展點(diǎn)對應(yīng)spi接口class;

objectFactory:擴(kuò)展點(diǎn)工廠AdaptiveExtensionFactory,主要用于setter注入,后面再看。

圖片

2、單例

ExtensionLoader提供靜態(tài)方法,構(gòu)造ExtensionLoader實(shí)例。

圖片

單例往往要針對一個范圍(scope)來說,比如Spring中所說的單例,往往是在一個BeanFactory中,而一個應(yīng)用可以運(yùn)行多個BeanFactory。又比如Class對象是單例,往往隱含的scope是同一ClassLoader。

ExtensionLoader在一個擴(kuò)展點(diǎn)接口Class下只有一個實(shí)例,而每個擴(kuò)展點(diǎn)實(shí)現(xiàn)實(shí)例在全局只有一個。

圖片

3、成員變量

ExtensionLoader的成員變量可以分為幾類

普通擴(kuò)展點(diǎn)相關(guān):

圖片

active擴(kuò)展點(diǎn)相關(guān):

圖片

adaptive擴(kuò)展點(diǎn)相關(guān):

圖片

wrapper擴(kuò)展點(diǎn)相關(guān):

圖片

4、加載擴(kuò)展點(diǎn)Class

ExtensionLoader#getExtensionClasses:

當(dāng)需要加載某個擴(kuò)展點(diǎn)實(shí)現(xiàn)實(shí)例前,總會優(yōu)先加載該擴(kuò)展點(diǎn)所有實(shí)現(xiàn)Class,并緩存到cachedClasses中。

圖片

ExtensionLoader#loadExtensionClasses:加載所有擴(kuò)展點(diǎn)實(shí)現(xiàn)類

圖片

ExtensionLoader#cacheDefaultExtensionName:加載SPI注解中的value屬性,作為默認(rèn)擴(kuò)展點(diǎn)名稱,默認(rèn)擴(kuò)展點(diǎn)只能存在一個。

圖片

ExtensionLoader會掃描classpath三個路徑下的擴(kuò)展點(diǎn)配置文件:

  • META-INF/dubbo/internal:dubbo框架自己用的
  • META-INF/dubbo/:用戶擴(kuò)展用的
  • META-INF/services/:官方也沒建議這樣使用

圖片

ExtensionLoader#loadDirectory:

1)類加載器:優(yōu)先線程類加載器,其次ExtensionLoader自己的類加載器;

2)掃描擴(kuò)展點(diǎn)配置文件;

3)加載擴(kuò)展類;

圖片

ExtensionLoader#loadResource:加載文件中每一行,key是擴(kuò)展名,value是擴(kuò)展實(shí)現(xiàn)類名。

圖片

ExtensionLoader#loadClass:最終將每個擴(kuò)展實(shí)現(xiàn)類Class按照不同的方式,緩存到ExtensionLoader實(shí)例中。

圖片

普通擴(kuò)展點(diǎn)

案例

對于一個擴(kuò)展點(diǎn)MyExt:

@SPI
public interface MyExt {
    String echo(URL url, String s);
}

MyExtImplA實(shí)現(xiàn)MyExt:

public class MyExtImplA implements MyExt {
    @Override
    public String echo(URL url, String s) {
        return "ext1";
    }
}

可以配置多個擴(kuò)展點(diǎn)實(shí)現(xiàn)META-INF/dubbo/x.y.z.MyExt:

A=x.y.z.impl.MyExtImplA
B=x.y.z.impl.MyExtImplB

使用ExtensionLoader#getExtention獲取對應(yīng)擴(kuò)展點(diǎn):

@Test
void testExtension() {
    ExtensionLoader

這種用法,對于用戶來說,和beanFactory極為類似,當(dāng)然實(shí)現(xiàn)并不同。

MyExt a = beanFactory.getBean("A", MyExt.class);

原理

ExtensionLoader#getExtention固然有單例緩存(cachedInstances),這個直接跳過。

圖片

ExtensionLoader#createExtension:創(chuàng)建擴(kuò)展點(diǎn)實(shí)現(xiàn)

0)getExtensionClasses:確保所有擴(kuò)展點(diǎn)class被加載

1)通過無參構(gòu)造,實(shí)例化擴(kuò)展點(diǎn)instance

2)injectExtention:對擴(kuò)展點(diǎn)instance執(zhí)行setter注入,暫時忽略

3)包裝類相關(guān),暫時忽略

4)執(zhí)行instance的初始化方法

圖片

initExtension:初始化

圖片

ExtensionLoader和Spring的創(chuàng)建bean流程相比,確實(shí)很像,比如:

1)Spring可以通過各種方式選擇bean的一個構(gòu)造方法創(chuàng)建一個bean(AbstractAutowireCapableBeanFactory#createBeanInstance),而ExtensionLoader只能通過無參構(gòu)造創(chuàng)建擴(kuò)展點(diǎn);

2)Spring可以通過多種方式進(jìn)行依賴注入(AbstractAutowireCapableBeanFactory#populateBean),比如Aware接口/setter/注解等,而ExtensionLoader只能支持setter注入;

3)Spring可以通過多種方式進(jìn)行初始化(AbstractAutowireCapableBeanFactory#initializeBean),比如PostConstruct注解/InitializingBean/initMethod等,而ExtensionLoader只支持InitializingBean(LifeCycle)這種方式;

包裝擴(kuò)展點(diǎn)

案例

上面在ExtensionLoader#createExtension的第三步,可能會走包裝擴(kuò)展點(diǎn)邏輯。

假設(shè)有個擴(kuò)展點(diǎn)MyExt2:

@SPI
public interface MyExt2 {
    String echo(URL url, String s);
}

有普通擴(kuò)展點(diǎn)實(shí)現(xiàn)MyExt2ImplA:

public class MyExt2ImplA implements MyExt2 {
    @Override
    public String echo(URL url, String s) {
        return "A";
    }
}

除此以外,還有兩個實(shí)現(xiàn)MyExt2的擴(kuò)展點(diǎn)的MyExtWrapperA和MyExtWrapperB, 特點(diǎn)在于他有MyExt2的單參數(shù)構(gòu)造方法 。

public class MyExtWrapperA implements MyExt2 {

    private final MyExt2 myExt2;

    public MyExtWrapperA(MyExt2 myExt2) {
        this.myExt2 = myExt2;
    }

    @Override
    public String echo(URL url, String s) {
        return "wrapA>>>" + myExt2.echo(url, s);
    }
}
public class MyExtWrapperB implements MyExt2 {

    private final MyExt2 myExt2;

    public MyExtWrapperB(MyExt2 myExt2) {
        this.myExt2 = myExt2;
    }

    @Override
    public String echo(URL url, String s) {
        return "wrapB>>>" + myExt2.echo(url, s);
    }
}

然后編寫配置文件META-INF/x.y.z.myext2.MyExt2:

A=x.y.z.myext2.impl.MyExt2ImplA
wrapperA=x.y.z.myext2.impl.MyExtWrapperA
wrapperB=x.y.z.myext2.impl.MyExtWrapperB

測試驗(yàn)證,echo方法輸出wrapB>>>wrapA>>>A。

@Test
void testWrapper() {
    ExtensionLoader

但是包裝擴(kuò)展點(diǎn)不能通過getExtension顯示獲取 ,比如:

// 包裝類無法通過name直接獲取
@Test
void testWrapper_IllegalStateException() {
    ExtensionLoader

原理

包裝類之所以不暴露給用戶直接獲取,是因?yàn)榘b類提供類似aop的用途,對于用戶來說是透明的。

類加載階段

在類加載階段,isWrapperClass判斷一個擴(kuò)展類是否是包裝類,如果是的話放入cachedWrapperClasses緩存。

對于包裝類,不會放入普通擴(kuò)展點(diǎn)的緩存map,所以無法通過getExtension顯示獲取。

圖片

判斷是否是包裝類,取決于擴(kuò)展點(diǎn)實(shí)現(xiàn)clazz是否有對應(yīng)擴(kuò)展點(diǎn)type的單參構(gòu)造方法。

圖片

實(shí)例化階段

包裝類實(shí)例化,是通過ExtensionLoader.getExtension("A")獲取普通擴(kuò)展點(diǎn)觸發(fā)的,而返回的會是一個包裝類。

如果一個擴(kuò)展點(diǎn)存在包裝類,客戶端通過getExtension永遠(yuǎn)無法獲取到原始擴(kuò)展點(diǎn)實(shí)現(xiàn) 。

圖片

包裝類是硬編碼實(shí)現(xiàn)的:

1)本質(zhì)上包裝的順序是無序的,取決于擴(kuò)展點(diǎn)配置文件的掃描順序。(SpringAOP可以設(shè)置順序)

2)包裝類即使只關(guān)注擴(kuò)展點(diǎn)的一個方法,也必須要實(shí)現(xiàn)擴(kuò)展點(diǎn)的所有方法,擴(kuò)展點(diǎn)新增方法如果沒有默認(rèn)實(shí)現(xiàn),需要修改所有包裝類。(SpringAOP如果用戶只關(guān)心其中一個方法,也可以實(shí)現(xiàn),因?yàn)槭莿討B(tài)代理)

3)性能較好。(無反射)

自適應(yīng)擴(kuò)展點(diǎn)

對于一個擴(kuò)展點(diǎn)type,最多只有一個自適應(yīng)擴(kuò)展點(diǎn)實(shí)現(xiàn)。

可以通過用戶硬編碼實(shí)現(xiàn),也可以通過dubbo自動生成,優(yōu)先取用戶硬編碼實(shí)現(xiàn)的自適應(yīng)擴(kuò)展點(diǎn)。

硬編碼(Adaptive注解Class)

案例

假如有個水果擴(kuò)展類,howMuch來統(tǒng)計交易上下文中該水果能賣多少錢。

@SPI
public interface Fruit {
    int howMuch(String context);
}

有蘋果香蕉等實(shí)現(xiàn),負(fù)責(zé)計算自己能賣多少錢。

public class Apple implements Fruit {
    @Override
    public int howMuch(String context) {
        return context.contains("apple") ? 1 : 0;
    }
}
public class Banana implements Fruit {
    @Override
    public int howMuch(String context) {
        return context.contains("banana") ? 2 : 0;
    }
}

這里引入一個AdaptiveFruit,在類上加了Adaptive注解,目的是統(tǒng)計上下文中所有水果能賣多少錢。

getSupportedExtensionInstances這個方法能加載所有擴(kuò)展點(diǎn),并依靠Prioritized接口實(shí)現(xiàn)排序,這個原理忽略,和Spring的Ordered差不多。

@Adaptive
public class AdaptiveFruit implements Fruit {
    private final Set

測試方法如下,用戶購買蘋果和香蕉,共花費(fèi)3元。

核心api是ExtensionLoader#getAdaptiveExtension獲取自適應(yīng)擴(kuò)展點(diǎn)實(shí)現(xiàn)。

@Test
void testAdaptiveFruit() {
    ExtensionLoader

原理

在類加載階段,被Adaptive注解修飾的擴(kuò)展點(diǎn)Class會被緩存到cachedAdaptiveClass。

注意,Adaptive注解類也不會作為普通擴(kuò)展點(diǎn)暴露給用戶,即不能通過ExtensionLoader.getExtension通過擴(kuò)展名直接獲取。

圖片

ExtensionLoader#getAdaptiveExtension獲取自適應(yīng)擴(kuò)展點(diǎn)。

實(shí)例化階段,無參構(gòu)造反射創(chuàng)建Adaptive擴(kuò)展點(diǎn),并執(zhí)行setter注入。

圖片

dubbo優(yōu)先選取用戶實(shí)現(xiàn)的Adaptive擴(kuò)展點(diǎn)實(shí)現(xiàn),否則會動態(tài)生成Adaptive擴(kuò)展點(diǎn)。

圖片

動態(tài)生成(Adaptive注解Method)

案例

假設(shè)現(xiàn)在有個秒殺水果擴(kuò)展點(diǎn)SecKillFruit。

相較于剛才的Fruit擴(kuò)展點(diǎn), 區(qū)別在于入?yún)⒏臑榱薝RL,且方法加了Adaptive注解

@SPI
public interface SecKillFruit {
    @Adaptive
    int howMuch(URL context);
}

蘋果秒殺0元,香蕉秒殺1元。

public class SecKillApple implements SecKillFruit {
    @Override
    public int howMuch(URL context) {
        return 0;
    }
}
public class SecKillBanana implements SecKillFruit {
    @Override
    public int howMuch(URL context) {
        return 1;
    }
}

擴(kuò)展點(diǎn)配置文件META-INF/x.y.z.myext4.SecKillFruit:

apple=x.y.z.myext4.impl.SecKillApple
banana=x.y.z.myext4.impl.SecKillBanana

假設(shè)場景,每次只能秒殺一種水果,需要根據(jù)上下文不同,決定秒殺的是哪種水果,計算不同的價錢。

有下面的測試案例,關(guān)鍵點(diǎn)在于URL里增加了sec.kill.fruit=擴(kuò)展點(diǎn)名,零編碼實(shí)現(xiàn)根據(jù)URL走不同策略。

sec.kill.fruit是SecKillFruit駝峰解析為小寫后用點(diǎn)分割得到。

@Test
void testAdaptiveFruit2() {
    ExtensionLoader

也可以通過指定Adaptive注解的value,讓獲取擴(kuò)展點(diǎn)名字的邏輯更加清晰。

比如取URL中的fruitType作為獲取擴(kuò)展名的方式。

@SPI
public interface SecKillFruit {
    @Adaptive("fruitType")
    int howMuch(URL context);
}

原理

由于Dubbo內(nèi)部就是用URL做全局上下文來用,你可以理解為字符串無所不能。

所以為了減少重復(fù)代碼,很多策略都通過動態(tài)生成自適應(yīng)擴(kuò)展來實(shí)現(xiàn)。

ExtensionLoader#createAdaptiveExtensionClass:如果沒有用戶Adaptive注解實(shí)現(xiàn)擴(kuò)展點(diǎn),走這里動態(tài)生成。

圖片

關(guān)鍵點(diǎn)在于AdaptiveClassCodeGenerator#generate如何生成java代碼。

擴(kuò)展點(diǎn)接口必須有Adaptive注解方法,否則getAdaptiveExtension會異常。

圖片

關(guān)鍵在于generateMethodContent如何實(shí)現(xiàn)adaptive方法邏輯。

對于沒有Adaptive注解的方法,直接拋出異常。

對于Adaptive注解的方法,分為四步:

1)獲取URL:優(yōu)先從參數(shù)列表里直接找URL,降級從一個有URL的getter方法的Class里獲取URL,否則異常;

2)決定擴(kuò)展名:優(yōu)先從Adaptive注解value屬性獲取,否則取擴(kuò)展點(diǎn)類名去駝峰加點(diǎn);

3)獲取擴(kuò)展點(diǎn):調(diào)用ExtensionLoader.getExtension;

4)委派給目標(biāo)擴(kuò)展實(shí)現(xiàn):調(diào)用目標(biāo)擴(kuò)展的目標(biāo)方法,傳入原始參數(shù)列表;

圖片

比如針對SecKillFruit,最終生成的代碼如下。

對于Dubbo來說,雖然擴(kuò)展點(diǎn)不同,但是都用URL上下文,就可以少寫重復(fù)代碼。

public class SecKillFruit$Adaptive implements x.y.z.myext4.SecKillFruit {
    // Adaptive注解方法,通過ContextHolder.getUrl獲取URL
    public int howMuch2(x.y.z.myext4.ContextHolder arg0) {
        if (arg0 == null)
            throw new IllegalArgumentException("...");
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("...");
        org.apache.dubbo.common.URL url = arg0.getUrl();
        String extName = url.getParameter("fruitType");
        if (extName == null)
            throw new IllegalStateException("...");
        x.y.z.myext4.SecKillFruit extension = (x.y.z.myext4.SecKillFruit)
                                               ExtensionLoader
                                               .getExtensionLoader(x.y.z.myext4.SecKillFruit.class)
                                               .getExtension(extName);
        return extension.howMuch2(arg0);
    }
    // Adaptive注解方法,直接從參數(shù)列表中獲取URL
    public int howMuch(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("fruitType");
        if (extName == null)
            throw new IllegalStateException("...");
        x.y.z.myext4.SecKillFruit extension = (x.y.z.myext4.SecKillFruit)
                                               ExtensionLoader
                                               .getExtensionLoader(x.y.z.myext4.SecKillFruit.class)
                                               .getExtension(extName);
        return extension.howMuch(arg0);
    }

    // 沒有Adaptive注解的方法
    public int howMuch() {
        throw new UnsupportedOperationException("...");
    }
}

Spring+jdk動態(tài)代理實(shí)現(xiàn)

上面原理分析不太好理解,這個事情也可以用Spring+jdk動態(tài)代理來實(shí)現(xiàn)。

其實(shí)這個需求和feign的FeignClient、mybatis的Mapper都比較像,寫完接口就相當(dāng)于寫完實(shí)現(xiàn)。

針對同一個擴(kuò)展點(diǎn)type設(shè)計一個 AdaptiveFactoryBean 。

public class AdaptiveFactoryBean implements FactoryBean {
    private final Class? type; /* 擴(kuò)展點(diǎn) */
    private final String defaultExtName; /* 默認(rèn)擴(kuò)展名 */
    private final Map

核心邏輯在InvocationHandler#invoke代理邏輯中,和AdaptiveClassCodeGenerator#generateMethodContent一樣。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (!cachedMethod2Adaptive.containsKey(method)) {
        throw new UnsupportedOperationException();
    }
    // 1. 獲取URL
    int urlIdx = cachedMethod2URLIndex.get(method);
    URL url = (URL) args[urlIdx];
    // 2. 從url里獲取擴(kuò)展點(diǎn)名
    Adaptive adaptive = cachedMethod2Adaptive.get(method);
    String extName = null;
    for (String key : adaptive.value()) {
        extName = url.getParameter(key);
        if (extName != null) {
            break;
        }
    }
    if (extName == null) {
        extName = defaultExtName;
    }
    if (extName == null) {
        throw new IllegalStateException();
    }
    // 3. 獲取擴(kuò)展點(diǎn)
    Object extension = 
        ExtensionLoader.getExtensionLoader(type).getExtension(extName);
    // 4. 委派給擴(kuò)展點(diǎn)
    return method.invoke(extension, args);
}

為了注入所有包含Adaptive注解方法的擴(kuò)展點(diǎn)AdaptiveFactoryBean,提供一個批量注冊BeanDefinition的 AdaptiveBeanPostProcessor ,實(shí)現(xiàn)比較粗糙,主要為了說明問題。

public class AdaptiveBeanPostProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
    private final String packageToScan;
    private Environment environment;
    public AdaptiveBeanPostProcessor(String packageToScan) {
        this.packageToScan = packageToScan;
    }
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        ClassPathScanningCandidateComponentProvider scanner =
                new ClassPathScanningCandidateComponentProvider(false, this.environment) {
                    @Override
                    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                        // 有Adaptive注解方法
                        return beanDefinition.getMetadata()
                                .hasAnnotatedMethods("org.apache.dubbo.common.extension.Adaptive");
                    }
                };
        scanner.addIncludeFilter(new AnnotationTypeFilter(SPI.class));
        Set

測試驗(yàn)證:

@Configuration
public class AdaptiveFactoryBeanTest {

    @Bean
    public AdaptiveBeanPostProcessor adaptiveBeanPostProcessor() {
        return new AdaptiveBeanPostProcessor("x.y.z.myext4");
    }

    @Test
    void test() {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(AdaptiveFactoryBeanTest.class);
        applicationContext.refresh();
        SecKillFruit secKillFruit = applicationContext.getBean("x.y.z.myext4.SecKillFruit$Adaptive_Spring", SecKillFruit.class);
        URL url = new URL("myProtocol", "1.2.3.4", 1010, "path");
        // 0元秒殺蘋果
        url = url.addParameters("fruitType", "apple");
        int money = secKillFruit.howMuch(url);
        assertEquals(0, money);
        // 1元秒殺香蕉
        url = url.addParameters("fruitType", "banana");
        money = secKillFruit.howMuch(url);
        assertEquals(1, money);
        // 無URL方法異常
        assertThrows(UnsupportedOperationException.class, secKillFruit::howMuch);
    }
}

是不是用spring+動態(tài)代理來說明,更加容易理解了。

依賴注入

無論是對于普通擴(kuò)展點(diǎn)/包裝擴(kuò)展點(diǎn)/自適應(yīng)擴(kuò)展點(diǎn),所有的擴(kuò)展點(diǎn)實(shí)例都會經(jīng)過依賴注入。

案例

InjectExt是個擴(kuò)展點(diǎn),有實(shí)現(xiàn)InjectExtImplA,InjectExtImplA有一個Inner的setter方法。

public class InjectExtImplA implements InjectExt {
    private Inner inner;
    public void setInner(Inner inner) {
        this.inner = inner;
    }
    @Override
    public Inner getInner() {
        return inner;
    }
}

Inner是個擴(kuò)展點(diǎn),且能生成自適應(yīng)擴(kuò)展實(shí)現(xiàn)。

@SPI
public interface Inner {
    @Adaptive
    String echo(URL url);
}

Inner有InnerA實(shí)現(xiàn)。

public class InnerA implements Inner {
    @Override
    public String echo(URL url) {
        return "A";
    }
}

測試方法,InjectExtImplA 被自動注入了Inner的自適應(yīng)實(shí)現(xiàn) 。

@Test
void testInject() {
    ExtensionLoader

原理

ExtensionLoader#injectExtension依賴注入,循環(huán)每個setter方法,找到入?yún)lass和屬性名。

通過ExtensionFactory搜索依賴,整個注入過程的異常都被捕獲。

圖片

ExtensionFactory也是SPI接口。

這里走硬編碼實(shí)現(xiàn)的 AdaptiveExtensionFactory ,循環(huán)每個ExtensionFactory擴(kuò)展點(diǎn),通過type和name找擴(kuò)展點(diǎn)實(shí)現(xiàn)。

圖片

ExtensionFactory擴(kuò)展點(diǎn)有兩個實(shí)現(xiàn)。

原生的 SpiExtensionFactory , 沒有利用setter的屬性name ,直接獲取type對應(yīng)的自適應(yīng)擴(kuò)展點(diǎn)。

這也是為什么案例中,被注入的擴(kuò)展點(diǎn)用了Adaptive。

圖片

Spring相關(guān)的SpringExtensionFactory支持從多個ioc容器中,通過getBean(setter屬性名,擴(kuò)展點(diǎn))獲取bean。

圖片

激活擴(kuò)展點(diǎn)

背景

ExtensionLoader#getExtension可以獲取單個擴(kuò)展點(diǎn)實(shí)現(xiàn)。

ExtensionLoader#getSupportedExtensionInstances可以獲取所有擴(kuò)展點(diǎn)實(shí)現(xiàn)。

現(xiàn)在 需要根據(jù)條件,獲取一類擴(kuò)展點(diǎn)實(shí)現(xiàn),這就是所謂的激活擴(kuò)展點(diǎn)

以Spring為例,如何利用Qualifier做到這點(diǎn)。

假設(shè)現(xiàn)在有個用戶接口,根據(jù)用戶類型和用戶等級有不同實(shí)現(xiàn)。

public interface User {
}

利用Qualifier注解,Category代表用戶類型,Level代表用戶等級。

@Qualifier
public @interface Category {
    String value();
}
@Qualifier
public @interface Level {
    int value();
}

針對User有四種實(shí)現(xiàn),包括vip1級用戶、vip2級用戶、普通用戶、普通2級用戶。

@Component
@Category("vip")
@Level(1)
public static class VipUser implements User {
}
@Component
@Category("vip")
@Level(2)
public static class GoldenVipUser implements User {
}
@Component
@Category("normal")
public static class UserImpl implements User {
}
@Component
@Category("normal")
@Level(2)
public static class UserImpl2 implements User {
}

通過Qualifier,可以按照需求注入不同類型等級用戶集合,做業(yè)務(wù)邏輯。

@Configuration
@ComponentScan
public class QualifierTest {
    @Autowired
    @Category("vip")
    private List

案例

和上面Spring的案例一樣,替換成ExtensionLoader實(shí)現(xiàn),看起來語義差不多。

用戶等級作為分組,在URL參數(shù)上獲取用戶等級。

@Activate(group = {"vip"}, value = {"level:1"})
public class VipUser implements User {
}
@Activate(group = {"vip"}, value = {"level:2"}, order = 1000)
public class GoldenVipUser implements User {
}
@Activate(group = {"normal"}, order = 10)
public class UserImpl implements User {
}
@Activate(group = {"normal"}, value = {"level:2"}, order = 500)
public class UserImpl2 implements User {
}

測試方法如下,發(fā)現(xiàn)與Spring的Qualifier有相同也有不同。

比如通過group=vip和url不包含level去查詢:

1)UserImpl和UserImpl2查不到,因?yàn)間roup不滿足;

2)VipUser和GoldenVipUser查不到,因?yàn)閡rl必須有l(wèi)evel,且分別為1和2;

又比如通過group=null和level=2去查詢:

1)UserImpl沒有設(shè)置Activate注解value,代表對url沒有約束,且查詢條件group=null,代表匹配所有g(shù)roup,所以可以查到;

2)VipUser對url有約束,必須level=1,所以查不到;

3)GoldenVipUser和UserImpl2,都滿足level=2,且查詢條件group=null,代表匹配所有g(shù)roup,所以都能查到;

@Test
void testActivate() {
    ExtensionLoader

原理

類加載階段,激活擴(kuò)展點(diǎn)在普通擴(kuò)展點(diǎn)分支邏輯中。

所以 激活擴(kuò)展點(diǎn)只是篩選普通擴(kuò)展點(diǎn)的方式 ,屬于普通擴(kuò)展點(diǎn)的子集。

圖片

ExtensionLoader#getActivateExtension獲取激活擴(kuò)展點(diǎn)的入?yún)糠郑?/p>

1)查詢URL;2)查詢擴(kuò)展點(diǎn)名稱集合;3)查詢分組

其中1和3用于Activate匹配,2用于直接從getExtension獲取擴(kuò)展點(diǎn)加在Activate匹配的擴(kuò)展點(diǎn)之后。

圖片

重點(diǎn)看isMatchGroup和isActive兩個方法。

isMatchGroup :如果查詢條件不包含group,則匹配,如果查詢條件包含group,注解中必須有g(shù)roup與其匹配。

圖片

isActive :匹配url

1)Activate沒有value約束,匹配

2)url匹配成功條件:如果注解value配置為k:v模式,要求url參數(shù)kv完全匹配;如果注解value配置為k模式,只需要url包含kv參數(shù)即可。其中k還支持后綴匹配。

比如@Activate(value = {"level"})只需要url中有l(wèi)evel=xxx即可,

而@Activate(value = {"level:2"})需要url中l(wèi)evel=2。

圖片

總結(jié)

本文分析了Dubbo2.7.6的SPI實(shí)現(xiàn)。

ExtensionLoader相較于java的spi能按需獲取擴(kuò)展點(diǎn),還有很多高級特性,與Spring的ioc和aop非常相似。

看似ExtensionLoader的功能都能通過Spring實(shí)現(xiàn),但是Dubbo不想依賴Spring,所以造了套輪子。

題外話:非??鋸埖氖?,Dubbo一個RPC框架,竟然有27w行代碼,而同樣是RPC框架的sofa-rpc5.9.0只有14w行。

除了很多com.alibaba的老包兼容代碼,輪子是真的多,早期版本連json庫都是自己實(shí)現(xiàn)的,現(xiàn)在是換成fastjson了。

普通擴(kuò)展點(diǎn)

ExtensionLoader#getExtension(name),普通擴(kuò)展點(diǎn)通過擴(kuò)展名獲取。

@SPI
public interface MyExt {
    String echo(URL url, String s);
}

創(chuàng)建普通擴(kuò)展點(diǎn)分為四個步驟

1)無參構(gòu)造

2)依賴注入

3)包裝

4)初始化

包裝擴(kuò)展點(diǎn)

如果擴(kuò)展點(diǎn)實(shí)現(xiàn)包含該擴(kuò)展點(diǎn)的單參構(gòu)造方法,被認(rèn)為是包裝擴(kuò)展點(diǎn)。

public class WrapperExt implements Ext {
    public WrapperExt(Ext ext) {
    }
}

包裝擴(kuò)展點(diǎn)無法通過擴(kuò)展名顯示獲取,而是在用戶獲取普通擴(kuò)展點(diǎn)時,自動包裝普通擴(kuò)展點(diǎn),返回給用戶,整個過程是透明的。

自適應(yīng)擴(kuò)展點(diǎn)

ExtensionLoader#getAdaptiveExtension獲取自適應(yīng)擴(kuò)展點(diǎn)。

每個擴(kuò)展點(diǎn)最多只有一個自適應(yīng)擴(kuò)展點(diǎn)。

自適應(yīng)擴(kuò)展點(diǎn)分為兩類:硬編碼、動態(tài)生成。

硬編碼自適應(yīng)擴(kuò)展點(diǎn),在擴(kuò)展點(diǎn)實(shí)現(xiàn)class上標(biāo)注Adaptive注解,優(yōu)先級高于動態(tài)生成自適應(yīng)擴(kuò)展點(diǎn)。

@Adaptive
public class AdaptiveFruit implements Fruit {

}

動態(tài)生成自適應(yīng)擴(kuò)展點(diǎn)。

出現(xiàn)的背景是,dubbo中有許多依賴URL上下文選擇不同擴(kuò)展點(diǎn)策略的場景,如果通過硬編碼實(shí)現(xiàn),會有很多重復(fù)代碼。

動態(tài)生成自適應(yīng)擴(kuò)展點(diǎn),針對@Adaptive注解方法且方法參數(shù)有URL的擴(kuò)展點(diǎn),采用javassist字節(jié)碼技術(shù),動態(tài)生成策略實(shí)現(xiàn)。

@SPI
public interface SecKillFruit {
    @Adaptive("fruitType")
    int howMuch(URL context);
}

激活擴(kuò)展點(diǎn)

激活擴(kuò)展點(diǎn)屬于普通擴(kuò)展點(diǎn)的子集。

激活擴(kuò)展點(diǎn)利用Activate注解,根據(jù)條件匹配一類擴(kuò)展點(diǎn)實(shí)現(xiàn) 。

@Activate(group = {"vip"}, value = {"level:2"}, order = 1000)
public class GoldenVipUser implements User {
}

ExtensionLoader#getActivateExtension:通過group和URL查詢一類擴(kuò)展點(diǎn)實(shí)現(xiàn)。

@Test
void testActivate() {
    ExtensionLoader

依賴注入

無論是普通/包裝/自適應(yīng)擴(kuò)展點(diǎn),在暴露給用戶使用前,都會進(jìn)行setter依賴注入。

依賴注入對象可來源于兩部分:

1)SpiExtensionFactory根據(jù)type獲取自適應(yīng)擴(kuò)展點(diǎn)

2)SpringExtensionFactory根據(jù)setter屬性+type從ioc容器獲取擴(kuò)展點(diǎn)

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • ECHO
    +關(guān)注

    關(guān)注

    1

    文章

    73

    瀏覽量

    27098
  • RPC
    RPC
    +關(guān)注

    關(guān)注

    0

    文章

    110

    瀏覽量

    11475
  • URL
    URL
    +關(guān)注

    關(guān)注

    0

    文章

    138

    瀏覽量

    15217
  • SPI接口
    +關(guān)注

    關(guān)注

    0

    文章

    258

    瀏覽量

    34228
收藏 人收藏

    評論

    相關(guān)推薦

    什么是java spring

    水平,學(xué)習(xí)和研究Spring源碼將會使你收到意想不到的效果。Spring框架的好處在我們進(jìn)入細(xì)節(jié)以前,讓我們看Spring可以給
    發(fā)表于 09-11 11:16

    怎么閱讀Spring源碼

    注入)。如果其中有個類container里沒找到,則拋出異常,比如常見的spring無法找到該類定義,無法wire的異常。還有就是嵌套bean則用了下遞歸,container會放到
    發(fā)表于 05-04 15:21

    獨(dú)家專訪阿里高級技術(shù)專家北緯:Dubbo開源重啟半年來的快意江湖

    的主要用戶分布在北上廣深和杭州,進(jìn)步的,我們也會重點(diǎn)考慮成都和南京。沙龍活動的分享主題是面向工程師向的,會包含架構(gòu)分析、源碼解讀、Hands On、以及友商案例分享等內(nèi)容,由于 Dubbo
    發(fā)表于 05-16 22:27

    聊聊Dubbo - Dubbo可擴(kuò)展機(jī)制實(shí)戰(zhàn)

    OSGI容器Dubbo作為個框架,不希望強(qiáng)依賴其他的IoC容器,比如Spring,Guice。OSGI也是個很重的實(shí)現(xiàn),不適合Dubbo
    發(fā)表于 06-04 17:33

    聊聊Dubbo - Dubbo可擴(kuò)展機(jī)制源碼解析

    很不錯呀,接下來,我們就深入Dubbo源碼睹廬山真面目。在Dubbo可擴(kuò)展機(jī)制實(shí)戰(zhàn)中,我們了解了Dubbo擴(kuò)展機(jī)制的
    發(fā)表于 06-05 18:43

    Dubbo開源現(xiàn)狀與未來規(guī)劃

    摘要: Dubbo 在過去段時間疏于維護(hù),去年阿里高調(diào)宣布重啟 Dubbo 開源之后,社區(qū)里問的最多的問題是,這次開源與上次有什么樣,還有就是
    發(fā)表于 07-05 15:21

    Dubbo Cloud Native 之路的實(shí)踐與思考

    Spring Boot Project 以及匯報 Dubbo 與 Cloud Native 整合過程中的些實(shí)踐與思考,如適配 Spring Cloud 、服務(wù)發(fā)現(xiàn)、服務(wù)網(wǎng)關(guān)、服務(wù)跟
    發(fā)表于 07-05 16:05

    Dubbo源代碼實(shí)現(xiàn)服務(wù)調(diào)用的動態(tài)代理和負(fù)載均衡

    我們知道,Dubbo將服務(wù)調(diào)用封裝成普通的Spring的Bean,于是我們可以像使用本地的Spring Bean樣,來調(diào)用遠(yuǎn)端的Dubbo
    發(fā)表于 03-12 14:35 ?0次下載

    服務(wù)化改造實(shí)踐()| Dubbo + ZooKeeper

    localhost即可。4、服務(wù)端:啟動服務(wù)在 main 方法中通過啟動Spring Context來對外提供Dubbo服務(wù)。public?class?ProviderBootstrap?{public
    發(fā)表于 08-27 16:36 ?214次閱讀
    服務(wù)化改造實(shí)踐(<b class='flag-5'>一</b>)| <b class='flag-5'>Dubbo</b> + ZooKeeper

    服務(wù)化改造實(shí)踐()| Dubbo + ZooKeeper

    ZooKeeper,只需要簡單的將這里的$ DOCKER_HOST換成localhost即可。4、服務(wù)端:啟動服務(wù)在 main 方法中通過啟動Spring Context來對外提供Dubbo服務(wù)
    發(fā)表于 08-27 17:25 ?281次閱讀
    服務(wù)化改造實(shí)踐(<b class='flag-5'>一</b>)| <b class='flag-5'>Dubbo</b> + ZooKeeper

    STM32入門:軟件 SPI 源碼分享

    軟件 SPI 源碼分享項(xiàng)目需求,只需要軟件 SPI 的寫入功能,后面有時間了再把讀取功能補(bǔ)上。spi.h//spi.h#ifndef __
    發(fā)表于 12-22 19:24 ?11次下載
    STM32入門:軟件 <b class='flag-5'>SPI</b> <b class='flag-5'>源碼</b>分享

    JDK內(nèi)置的種服務(wù)SPI機(jī)制

    SPI(Service Provider Interface)是JDK內(nèi)置的種服務(wù)提供發(fā)現(xiàn)機(jī)制,可以用來啟用框架擴(kuò)展和替換組件,主要用于框架中開發(fā),例如DubboSpring
    的頭像 發(fā)表于 02-15 09:15 ?713次閱讀

    基于springSPI擴(kuò)展機(jī)制是如何實(shí)現(xiàn)的?

    基本上,你說是基于 springSPI 擴(kuò)展機(jī)制,再把spring.factories文件和EnableAutoConfiguration提
    的頭像 發(fā)表于 03-07 09:17 ?909次閱讀

    Java、Spring、Dubbo三者SPI機(jī)制的原理和區(qū)別

    其實(shí)我之前寫過篇類似的文章,但是這篇文章主要是剖析dubboSPI機(jī)制的源碼,中間只是簡單地介紹了下Java、
    的頭像 發(fā)表于 06-05 15:21 ?860次閱讀
    Java、<b class='flag-5'>Spring</b>、<b class='flag-5'>Dubbo</b>三者<b class='flag-5'>SPI</b>機(jī)制的原理和區(qū)別

    dubbospring cloud區(qū)別

    DubboSpring Cloud是兩個非常流行的微服務(wù)框架,各有自己的特點(diǎn)和優(yōu)勢。在本文中,我們將詳細(xì)介紹DubboSpring Cloud的區(qū)別。 1.架構(gòu)設(shè)計:
    的頭像 發(fā)表于 12-04 14:47 ?1318次閱讀