Dubbo如何經過SPI提升框架的可擴展性?

介紹

最近看了一下Dubbo的源碼,國人寫的框架和國外的果真是兩種不一樣的風格,Dubbo的源碼仍是比較清晰容易懂的。Spring框架一個Bean的初始化過程就能繞死在源碼中.java

Dubbo的架構是基於分層來設計的,每層執行固定的功能,上層依賴下層,下層的改變對上層不可見,每層都是能夠被替換的組件
git


Service和Config爲API接口層,讓Dubbo使用者方便的發佈和引用服務
其餘各層均爲SPI層,意味着每層都是組件化的,能夠被替換

例如,註冊中心能夠用Redis,Zookeeper。傳輸協議能夠用dubbo,rmi,hessian等。
網絡通訊能夠用mina,netty。序列化能夠用fastjson,hessian2,java原生的方式等github

SPI 全稱爲 Service Provider Interface,是一種服務發現機制。SPI 的本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類。這樣能夠在運行時,動態爲接口替換實現類。正所以特性,咱們能夠很容易的經過 SPI 機制爲咱們的程序提供拓展功能web

那麼Dubbo的SPI是怎麼實現的呢?先來了解一下Java SPIspring

Java SPI

Java SPI是經過策略模式實現的,一個接口提供多個實現類,而使用哪一個實現類不在程序中肯定,而是配置文件配置的,具體步驟以下apache

  1. 定義接口及其對應的實現類json

  2. 在META-INF/services目錄下建立以接口全路徑命名的文件微信

  3. 文件內容爲實現類的全路徑名網絡

  4. 在代碼中經過java.util.ServiceLoader#load加載具體的實現類架構

寫個Demo演示一下

public interface Car {

    void getBrand();
}

public class BenzCar implements Car {

    @Override
    public void getBrand() {
        System.out.println("benz");
    }
}

public class BMWCar implements Car {

    @Override
    public void getBrand() {
        System.out.println("bmw");
    }
}

org.apache.dubbo.Car的內容以下

org.apache.dubbo.BenzCar
org.apache.dubbo.BMWCar

測試類

public class JavaSpiDemo {

    public static void main(String[] args) {
        ServiceLoader<Car> carServiceLoader = ServiceLoader.load(Car.class);
        // benz
        // bmw
        carServiceLoader.forEach(Car::getBrand);
    }
}

Dubbo SPI

用Dubbo SPI將上面的例子改造一下

@SPI
public interface Car {

    void getBrand();
}

public class BenzCar implements Car {

    @Override
    public void getBrand() {
        System.out.println("benz");
    }
}

public class BMWCar implements Car {
    @Override
    public void getBrand() {
        System.out.println("bmw");
    }
}

org.apache.dubbo.quickstart.Car的內容以下

benz=org.apache.dubbo.quickstart.BenzCar
bmw=org.apache.dubbo.quickstart.BMWCar

測試類

public class DubboSpiDemo {

    public static void main(String[] args) {
        ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
        Car car = extensionLoader.getExtension("benz");
        car.getBrand();
    }
}

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {

    String value() default "";

}

@SPI標記接口是一個Dubbo SPI接口,便是一個擴展點,value屬性能夠指定默認實現

Dubbo 並未使用 Java 原生的 SPI 機制,而是對其進行了加強,使其可以更好的知足需求。Dubbo SPI的優勢以下

  1. JDK標準的SPI會一次性實例化擴展點的全部實現。而Dubbo SPI能實現按需加載

  2. Dubbo SPI增長了對擴展點Ioc和Aop的支持

Dubbo SPI的實現步驟以下

  1. 定義接口及其對應的實現類,接口上加@SPI註解,代表這是一個擴展類

  2. 在META-INF/services目錄下建立以接口全路徑命名的文件

  3. 文件內容爲實現類的全路徑名

  4. 在代碼中經過ExtensionLoader加載具體的實現類

Dubbo SPI 擴展點的特性

自動包裝

擴展類的構造函數是一個擴展點,則認爲這個類是一個Wrapper類,即AOP

用例子演示一下

@SPI
public interface Car {

    void getBrand();
}

public class BenzCar implements Car {
    @Override
    public void getBrand() {
        System.out.println("benz");
    }
}

public class CarWrapper implements Car {

    private Car car;

    public CarWrapper(Car car) {
        this.car = car;
    }

    @Override
    public void getBrand() {
        System.out.println("start");
        car.getBrand();
        System.out.println("end");
    }
}

org.apache.dubbo.aop.Car內容以下(resources\META-INF\services目錄下)

benz=org.apache.dubbo.aop.BenzCar
org.apache.dubbo.aop.CarWrapper

測試類

public class DubboSpiAopDemo {

    public static void main(String[] args) {
        ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
        Car car = extensionLoader.getExtension("benz");
        // start
        // benz
        // end
        car.getBrand();
    }
}

BenzCar是一個擴展類,CarWrapper是一個包裝類,當獲取BenzCar的時候實際獲取的是被CarWrapper包裝後的對象,相似代理模式

自動加載

若是一個擴展類是另外一個擴展類的成員變量,而且擁有set方法,框架會自動注入這個擴展點的實例,即IOC。先定義2個擴展點

org.apache.dubbo.ioc.Car(resources\META-INF\services目錄下)

benz=org.apache.dubbo.ioc.BenzCar

org.apache.dubbo.ioc.Wheel(resources\META-INF\services目錄下)

benz=org.apache.dubbo.ioc.BenzWheel
@SPI
public interface Wheel {

    void getBrandByUrl();
}

public class BenzWheel implements Wheel {

    @Override
    public void getBrandByUrl() {
        System.out.println("benzWheel");
    }
}

@SPI
public interface Car {

    void getBrandByUrl();
}

public class BenzCar implements Car {

    private Wheel wheel;

    public void setWheel(Wheel wheel) {
        this.wheel = wheel;
    }

    @Override
    public void getBrandByUrl() {
        System.out.println("benzCar");
        wheel.getBrandByUrl();
    }
}

測試demo

public class DubboSpiIocDemo {

    public static void main(String[] args) {
        ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
        Car car = extensionLoader.getExtension("benz");
        car.getBrandByUrl();
    }
}

我跑這個代碼的時候直接報異常,看了一下官網才發現dubbo是能夠注入接口的實現的,但不像spring那麼智能,
dubbo必須用URL(相似總線)來指定擴展類對應的實現類.。這就不得不提到@Adaptive註解了

自適應

使用@Adaptive註解,動態的經過URL中的參數來肯定要使用哪一個具體的實現類

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {

    String[] value() default {};

}

@SPI
public interface Wheel {

    @Adaptive("wheel")
    void getBrandByUrl(URL url);
}

public class BenzWheel implements Wheel {

    @Override
    public void getBrandByUrl(URL url) {
        System.out.println("benzWheel");
    }
}

@SPI
public interface Car {

    void getBrandByUrl(URL url);
}

public class BenzCar implements Car {

    // 這個裏面存的是代理對象
    private Wheel wheel;

    public void setWheel(Wheel wheel) {
        this.wheel = wheel;
    }

    @Override
    public void getBrandByUrl(URL url) {
        System.out.println("benzCar");
        // 代理類根據URL找到實現類,而後再調用實現類
        wheel.getBrandByUrl(url);
    }
}

public class DubboSpiIocDemo {

    public static void main(String[] args) {
        ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
        Car car = extensionLoader.getExtension("benz");
        Map<String, String> map = new HashMap<>();
        // 指定wheel的實現類爲benz
        map.put("wheel""benz");
        URL url = new URL(""""1, map);
        // benzCar
        // benzWheel
        car.getBrandByUrl(url);
    }
}

能夠看到BenzCar對象成功注入了BenzWheel。BenzCar中其實注入的是BenzWheel的代碼對象,這個代理對象會根據@Adaptive("wheel")獲取到wheel,而後從url中找到key爲wheel的值,這個值即爲實現類對應的key。

上面的註釋提到BenzCar裏面注入的Wheel實際上是一個代理對象(框架幫咱們生成),在代理對象中根據url找到相應的實現類,而後調用實現類。

由於代理對象是框架在運行過程當中幫咱們生成的,沒有文件能夠查看,因此用Arthas來查看一下生成的代理類

curl -O https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar
# 根據前面的序號選擇進入的進程,而後執行下面的命令
jad org.apache.dubbo.adaptive.Wheel$Adaptive

生成的Wheel

package org.apache.dubbo.adaptive;

import org.apache.dubbo.adaptive.Wheel;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;

public class Wheel$Adaptive
implements Wheel 
{
    public void getBrandByUrl(URL uRL) {
        if (uRL == null) {
            throw new IllegalArgumentException("url == null");
        }
        URL uRL2 = uRL;
        String string = uRL2.getParameter("wheel");
        if (string == null) {
            throw new IllegalStateException(new StringBuffer().append("Failed to get extension (org.apache.dubbo.adaptive.Wheel) name from url (").append(uRL2.toString()).append(") use keys([wheel])").toString());
        }
        Wheel wheel = (Wheel)ExtensionLoader.getExtensionLoader(Wheel.class).getExtension(string);
        wheel.getBrandByUrl(uRL);
    }
}

@Adaptive能夠標記在類上或者方法上

標記在類上:將該實現類直接做爲默認實現,再也不自動生成代碼
標記在方法上:經過參數動態得到實現類,好比上面的例子

用源碼演示一下用在類上的@Adaptiv,Dubbo爲自適應擴展點生成代碼,如咱們上面的WheelAdaptive,但生成的代碼還須要編譯才能生成class文件。咱們能夠用JavassistCompiler(默認的)或者JdkCompiler來編譯(須要配置),這個小小的功能就用到了@Adaptive

若是想用JdkCompiler須要作以下配置

<dubbo:application compiler="jdk" />

Compiler類圖以下

@SPI("javassist")
public interface Compiler {

    Class<?> compile(String code, ClassLoader classLoader);

}

Compiler用@SPI指定了默認實現類爲javassist

源碼中獲取Compiler調用了以下方法

org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();

getAdaptiveExtension()會獲取自適應擴展類,那麼這個自適應擴展類是誰呢?

是AdaptiveCompiler,由於類上有@Adaptive註解

@Adaptive
public class AdaptiveCompiler implements Compiler {

    private static volatile String DEFAULT_COMPILER;

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

    /**
     * 獲取對應的Compiler,並調用compile作編譯
     * 用戶設置了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);
    }

}

從compile方法能夠看到,若是用戶設置了編譯方式,則用用戶設置的,若是沒有設置則用默認的,即JavassistCompiler

自動激活

使用@Activate註解,能夠標記對應的擴展點默認被激活使用

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Activate {

    // 所屬組,例如消費端,服務端
    String[] group() default {};

    // URL中包含屬性名爲value的鍵值對,過濾器才處於激活狀態
    String[] value() default {};

    // 指定執行順序,before指定的過濾器在該過濾器以前執行
    @Deprecated
    String[] before() default {};

    // 指定執行順序,after指定的過濾器在該過濾器以後執行
    @Deprecated
    String[] after() default {};

    // 指定執行順序,值越小,越先執行
    int order() default 0;
}

能夠經過指定group或者value,在不一樣條件下獲取自動激活的擴展點。before,after,order是用來排序的,感受一個order參數就能夠搞定排序的功能,因此官方把before,after標記爲@Deprecated

Dubbo Filter就是基於這個來實現的。Dubbo Filter是Dubbo可擴展性的一個體現,能夠在調用過程當中對請求進行進行加強

我寫個demo演示一下這個自動激活是怎麼工做的

@SPI
public interface MyFilter 
    void filter();
}

consumer組能激活這個filter

@Activate(group = {"consumer"})
public class MyConsumerFilter implements MyFilter {
    @Override
    public void filter() {

    }
}

provider組能激活這個filter

@Activate(group = {"provider"})
public class MyProviderFilter implements MyFilter {
    @Override
    public void filter() {

    }
}

consumer組和provide組都能激活這個filter

@Activate(group = {"consumer""provider"})
public class MyLogFilter implements MyFilter {
    @Override
    public void filter() {

    }
}

consumer組和provide組都能激活這個filter,同時url中指定key的value爲cache

@Activate(group = {"consumer""provider"}, value = "cache")
public class MyCacheFilter implements MyFilter {
    @Override
    public void filter() {

    }
}

測試類以下
getActivateExtension有3個參數,依次爲url, key, group

public class ActivateDemo {

    public static void main(String[] args) {
        ExtensionLoader<MyFilter> extensionLoader = ExtensionLoader.getExtensionLoader(MyFilter.class);
        // url中沒有參數
        URL url = URL.valueOf("test://localhost");
        List<MyFilter> allFilterList = extensionLoader.getActivateExtension(url, ""null);
        /**
         * org.apache.dubbo.activate.MyConsumerFilter@53e25b76
         * org.apache.dubbo.activate.MyProviderFilter@73a8dfcc
         * org.apache.dubbo.activate.MyLogFilter@ea30797
         *
         * 不指定組則全部的Filter都被激活
         */

        allFilterList.forEach(item -> System.out.println(item));
        System.out.println();

        List<MyFilter> consumerFilterList = extensionLoader.getActivateExtension(url, """consumer");
        /**
         * org.apache.dubbo.activate.MyConsumerFilter@53e25b76
         * org.apache.dubbo.activate.MyLogFilter@ea30797
         *
         * 指定consumer組,則只有consumer組的Filter被激活
         */

        consumerFilterList.forEach(item -> System.out.println(item));
        System.out.println();

        // url中有參數myfilter
        url = URL.valueOf("test://localhost?myfilter=cache");
        List<MyFilter> customerFilter = extensionLoader.getActivateExtension(url, "myfilter""consumer");
        /**
         * org.apache.dubbo.activate.MyConsumerFilter@53e25b76
         * org.apache.dubbo.activate.MyLogFilter@ea30797
         * org.apache.dubbo.activate.MyCacheFilter@aec6354
         *
         * 指定key在consumer組的基礎上,MyCacheFilter被激活
         */

        customerFilter.forEach(item -> System.out.println(item));
        System.out.println();
    }
}

總結一下就是,getActivateExtension不指定組就是激活全部的Filter,指定組則激活指定組的Filter。指定key則從Url中根據key取到對應的value,假設爲cache,而後把@Activate註解中value=cache的Filter激活

即group用來篩選,value用來追加,Dubbo Filter就是靠這個屬性激活不一樣的Filter的

ExtensionLoader的工做原理

ExtensionLoader是整個Dubbo SPI的主要實現類,有以下三個重要方法,搞懂這3個方法基本上就搞懂Dubbo SPI了。

加載擴展類的三種方法以下

  1. getExtension(),獲取普通擴展類

  2. getAdaptiveExtension(),獲取自適應擴展類

  3. getActivateExtension(),獲取自動激活的擴展類

getExtension()上面的例子中已經有了。自適應的特性上面已經演示過了,當獲取Wheel的實現類是框架會調用getAdaptiveExtension()方法。

代碼就不放了,這3個方法的執行過程仍是比較簡單的,若是你有看不懂的,能夠看我給源碼加的註釋。

https://github.com/erlieStar/dubbo-analysis

理解了Dubbo SPI你應該就把Dubbo搞懂一半了,剩下就是一些服務導出,服務引入,服務調用的過程了

歡迎關注


有幫助?點贊!轉發!

本文分享自微信公衆號 - Java識堂(erlieStar)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索