天天看點

Dubbo Adaptive機制詳解實戰

開篇

 本文嘗試通過一個示例來講解Adaptive機制的用法,然後會從源碼的角度對其實作原理進行講解

Adaptive機制

 對應于Adaptive機制,Dubbo提供了一個注解@Adaptive,該注解可以用于接口的某個實作類上,也可以用于接口方法上。如果用在接口的子類上,則表示Adaptive機制的實作會按照該子類的方式進行自定義實作;如果用在方法上,則表示Dubbo會為該接口自動生成一個子類,并且按照一定的格式重寫該方法,而其餘沒有标注@Adaptive注解的方法将會預設抛出異常。

 對于第一種Adaptive的使用方式,Dubbo裡隻有ExtensionFactory接口使用了,其有一個子類AdaptiveExtensionFactory就使用了@Adaptive注解進行了标注,主要作用就是在擷取目标對象時,分别通過ExtensionLoader和Spring容器兩種方式擷取,主要用在SPI的IOC實作中用于擷取上下文的Bean對象。

 對于第二種使用@Adaptive注解标注在接口方法上以實作Adaptive機制的使用原理,通過一個下面的例子進行說明。

用法示例

例子說明

  • 定義PlantsWater的接口并通過@SPI注解進行注解,注解可選擇帶預設值。
  • 将watering()方法使用@Adaptive注解進行了标注,表示該方法在自動生成的子類中是需要動态實作的方法。
  • 增加grant()方法是為了表明不帶@Adaptive在自動生成的子類方法内部會抛出異常。
  • 為PlantsWater增加兩個實作,AppleWater和BananaWater,實際調用通過參數控制。
  • 在META-INF/dubbo下建立一個檔案,該檔案的名稱是目标接口的全限定名,這裡是org.apache.dubbo.spi.example.PlantsWater,在該檔案中需要指定該接口所有可提供服務的子類。
  • 定義主函數ExtensionLoaderDemo模拟SPI調用的驗證。
----定義基礎應用類

public interface Fruit {}
public class Apple implements Fruit {}
public class Banana implements Fruit{}



----定義SPI類

@SPI("banana")
public interface PlantsWater {

    Fruit grant();

    @Adaptive
    String watering(URL url);
}


public class AppleWater implements PlantsWater {
    public Fruit grant() {
        return new Apple();
    }

    public String watering(URL url) {
        System.out.println("watering apple");
        return "watering finished";
    }
}


public class BananaWater implements PlantsWater {

    public Fruit grant() {
        return new Banana();
    }

    public String watering(URL url) {
        System.out.println("watering banana");
        return "watering success";
    }
}



----resources檔案 org.apache.dubbo.spi.example.PlantsWater

apple=org.apache.dubbo.spi.example.AppleWater
banana=org.apache.dubbo.spi.example.BananaWater


------測試代碼内容

public class ExtensionLoaderDemo {

    public static void main(String[] args) {
        // 首先建立一個模拟用的URL對象
        URL url = URL.valueOf("dubbo://192.168.0.101:20880?plants.water=apple");
        // 通過ExtensionLoader擷取一個PlantsWater對象,getAdaptiveExtension已經加載了所有SPI類
        PlantsWater plantsWater = ExtensionLoader.getExtensionLoader(PlantsWater.class)
                .getAdaptiveExtension();
        // 使用該PlantsWater調用其"自适應标注的"方法,擷取調用結果
        String result = plantsWater.watering(url);
        System.out.println(result);
    }
}


-----實際輸出内容

十月 11, 2019 7:48:51 下午 org.apache.dubbo.common.logger.LoggerFactory info
資訊: using logger: org.apache.dubbo.common.logger.jcl.JclLoggerAdapter
watering apple
watering finished

Process finished with exit code 0           

原理補充

  • 這裡提供了AppleWater和BananaWater表示的其實是兩種基礎服務類,本質上它們三者的關系是PlantsWater用于對外提供一個規範,而AppleWater和BananaWater則是實作了這種規範的兩種基礎服務。至于調用方需要使用哪種基礎服務來實作其功能,這就需要根據調用方指定的參數來動态選取的,而@Adaptive機制就是提供了這樣一種選取功能。
  • 模拟調用構造了一個URL對象,這個URL對象是Dubbo中進行參數傳遞所使用的一個基礎類,在配置檔案中配置的屬性都會被封裝到該對象中。這裡我們主要要注意該對象是通過一個url構造的,并且url的最後我們有一個參數plants.water=apple,這裡其實就是我們所指定的使用哪種基礎服務類的參數。比如這裡指定的就是使用apple對應的AppleWater。
  • ExtensionLoader.getExtensionLoader(PlantsWater.class).getAdaptiveExtension()的過程中會生成PlantsWater類對應的ExtensionLoader,在該Loader裡面儲存了PlantsWater對應的SPI的各個具體實作類。

PlantsWater$Adaptive

說明:

  • PlantsWater$Adaptive是PlantsWater接口動态生成的子類,通過ExtensionLoader.getAdaptiveExtension()方法動态生成。
  • 針對沒有@Adaptive修飾的方法,子類中實作的方法體内部直接抛出UnsupportedOperationException。
  • 針對有@Adaptive修飾的方法,子類會重新實作動态擷取PlantsWater 執行個體的方法,ExtensionLoader.getExtensionLoader(PlantsWater.class).getExtension(extName)。
  • 在使用@Adaptive注解标注的方法中,其參數中必須有一個參數類型為URL,或者其某個參數提供了某個方法,該方法可以傳回一個URL對象。
  • 在方法的實作中會通過URL對象擷取某個參數對應的參數值,如果在接口的@SPI注解中指定了預設值,那麼在使用URL對象擷取參數值時,如果沒有取到,就會使用該預設值。url.getParameter("plants.water", "banana")中plants.water取URL中帶的參數,"banana"屬于SPI指定的預設值。
  • 根據擷取到的參數值,在ExtensionLoader中擷取該參數值對應的服務提供類對象(通過ExtensionLoader.getExtensionLoader().getExtension(extName)),然後将真正的調用委托給該服務提供類對象進行。
  • 在通過URL對象擷取參數時,參數key擷取的對應規則是,首先會從@Adaptive注解的參數值中擷取,如果該注解沒有指定參數名,那麼就會預設将目标接口的類名轉換為點分形式作為參數名,比如這裡PlantsWater轉換為點分形式就是plants.water。
package org.apache.dubbo.spi.example;
import org.apache.dubbo.common.extension.ExtensionLoader;

public class PlantsWater$Adaptive implements org.apache.dubbo.spi.example.PlantsWater {

    public java.lang.String watering(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("plants.water", "banana");
        
        if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.spi.example.PlantsWater) name from url (" + url.toString() + ") use keys([plants.water])");
        
        org.apache.dubbo.spi.example.PlantsWater extension = (org.apache.dubbo.spi.example.PlantsWater)ExtensionLoader
                                             .getExtensionLoader(org.apache.dubbo.spi.example.PlantsWater.class)
                                             .getExtension(extName);
        
        return extension.watering(arg0);
    }

    public org.apache.dubbo.spi.example.Fruit grant()  {
        throw new UnsupportedOperationException("The method public abstract org.apache.dubbo.spi.example.Fruit org.apache.dubbo.spi.example.PlantsWater.grant() of interface org.apache.dubbo.spi.example.PlantsWater is not adaptive method!");
    }
}           

實作原理

Dubbo Adaptive的實作機制根據上面的講解其實步驟已經比較清晰了,主要分為如下三個步驟:

  • 1、加載标注有@Adaptive注解的接口,如果不存在,則不支援Adaptive機制;
  • 2、為目标接口按照一定的模闆生成子類代碼,并且編譯生成的代碼,然後通過反射生成該類的對象;
  • 3、結合生成的對象執行個體,通過傳入的URL對象,擷取指定key的配置,然後加載該key對應的類對象,最終将調用委托給該類對象進行。

可以看到,通過這種方式,Dubbo就實作了一種通過配置參數動态選擇所使用的服務的目的,而實作這種機制的入口主要在ExtensionLoader.getAdaptiveExtension()方法

public class ExtensionLoaderDemo {

    public static void main(String[] args) {
        // 首先建立一個模拟用的URL對象
        URL url = URL.valueOf("dubbo://192.168.0.101:20880?plants.water=apple");
        // 通過ExtensionLoader擷取一個FruitGranter對象
        PlantsWater plantsWater = ExtensionLoader.getExtensionLoader(PlantsWater.class)
                .getAdaptiveExtension();
        // 使用該FruitGranter調用其"自适應标注的"方法,擷取調用結果
        String result = plantsWater.watering(url);
        System.out.println(result);
    }
}           
  • 上面的代碼作為整個動态生成代碼的入口,關注ExtensionLoader.getExtensionLoader(PlantsWater.class)

    .getAdaptiveExtension()就可以了。

public T getAdaptiveExtension() {
  Object instance = cachedAdaptiveInstance.get();
  if (instance == null) {
    if (createAdaptiveInstanceError == null) {
      synchronized (cachedAdaptiveInstance) {
        instance = cachedAdaptiveInstance.get();
        if (instance == null) {
          try {
            // 建立Adaptive執行個體
            instance = createAdaptiveExtension();
            cachedAdaptiveInstance.set(instance);
          } catch (Throwable t) {
            createAdaptiveInstanceError = t;
            throw new IllegalStateException("Failed to create adaptive " 
                + "instance: " + t.toString(), t);
          }
        }
      }
    } else {
      throw new IllegalStateException("Failed to create adaptive instance: " 
          + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
    }
  }

  return (T) instance;
}           

上面的代碼首先通過雙檢查法來從緩存中擷取Adaptive執行個體,如果沒擷取到,則建立一個。我們這裡繼續看createAdaptiveExtension()方法的實作。

private T createAdaptiveExtension() {
        try {
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
        }
    }           

上面代碼是建立Adaptive執行個體的方法是一個主幹方法,從這裡調用方法的順序就可以看出其主要作用:

  • 擷取一個Adaptive類的class對象,不存在則建立一個,該方法會保證一定存在一個該class對象;
  • 通過反射建立一個Adaptive類的執行個體;
  • 對建立的Adaptive注入相關屬性,需要注意的是,Dubbo目前隻支援通過setter方法注入屬性。
private Class<?> getAdaptiveExtensionClass() {

  // 通過讀取Dubbo的配置檔案,擷取其中的SPI類,其主要處理了四部分的類:
  // 1. 标注了@Activate注解的類,該注解的主要作用是将某個實作子類标注為自動激活,也就是在加載
  //    執行個體的時候也會加載該類的對象;

  // 2. 記錄目标接口是否标注了@Adaptive注解,如果标注了該注解,則表示需要為該接口動态生成子類,或者說
  //    目标接口是否存在标注了@Adaptive注解的子類,如果存在,則直接使用該子類作為Adaptive類;
  
  // 3. 檢查加載到的類是否包含有傳入目标接口參數的構造方法,如果是,則表示該類是一個代理類,也可以
  //    将其了解為最終會被作為責任鍊進行調用的類,這些類最終會在目标類被調用的時候以類似于AOP的方式,
  //    将目标類包裹起來,然後将包裹之後的類對外提供服務;

  // 4. 剩餘的一般類就是實作了目标接口,并且作為基礎服務提供的類。

        getExtensionClasses();



  // 經過上面的類加載過程,如果目标接口某個子類存在@Adaptive注解,就會将其class對象緩存到
  // cachedAdaptiveClass對象中。這裡我們就可以看到@Adaptive注解的兩種使用方式的分界點,也就是說,
  // 如果某個子類标注了@Adaptive注解,那麼就會使用該子類所自定義的Adaptive機制,如果沒有子類标注了
  // 該注解,那麼就會使用下面的createAdaptiveExtensionClass()方式來建立一個目标類class對象
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }

  // 建立一個目标接口的子類class對象
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }


    private Class<?> createAdaptiveExtensionClass() {
  // 為目标接口生成子類代碼,以字元串形式表示
        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();

  // 擷取classloader
        ClassLoader classLoader = findClassLoader();

  // 通過jdk或者javassist的方式編譯生成的子類字元串(預設是javassist),進而得到一個class對象
        org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        return compiler.compile(code, classLoader);
    }           

上面的代碼中主要是一個骨架代碼,首先通過

getExtensionClasses()

擷取配置檔案中配置的各個類對象,其加載的原理可閱讀文章

Dubbo之SPI原理詳解

;加載完成後,會通過

AdaptiveClassCodeGenerator

來為目标類生成子類代碼,并以字元串的形式傳回,最後通過javassist或jdk的方式進行編譯然後傳回class對象。這裡我們主要閱讀

AdaptiveClassCodeGenerator.generate()

方法是如何生成目标接口的子類的。

public String generate() {
        //判斷目标接口是否有方法标注了@Adaptive注解,如果沒有則抛出異常
        if (!hasAdaptiveMethod()) {
            throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
        }

        StringBuilder code = new StringBuilder();
        code.append(generatePackageInfo()); // 生成package資訊
        code.append(generateImports()); // 生成import資訊,這裡隻導入了ExtensionLoader類,其餘的類都通過全限定名的方式來使用
        code.append(generateClassDeclaration()); // 生成類聲明資訊
        
        Method[] methods = type.getMethods();
        for (Method method : methods) {
            code.append(generateMethod(method)); // 為各個方法生成實作方法資訊
        }
        code.append("}");
        
        if (logger.isDebugEnabled()) {
            logger.debug(code.toString());
        }
        return code.toString(); // 傳回生成的class代碼
    }           

上面代碼的generate()方法是生成目标類的主幹方法,其主要分為如下幾個步驟:

  • 生成package資訊;
  • 生成import資訊;
  • 生成類聲明資訊;
  • 生成各個方法的實作;

    前面幾個步驟實作原理都相較比較簡單,繼續閱讀generateMethod()方法的實作原理。

private String generateMethod(Method method) {
        String methodReturnType = method.getReturnType().getCanonicalName(); // 生成傳回值資訊
        String methodName = method.getName(); // 生成方法名資訊
        String methodContent = generateMethodContent(method); // 生成方法體資訊
        String methodArgs = generateMethodArguments(method); // 生成方法參數資訊
        String methodThrows = generateMethodThrows(method); // 生成異常資訊
        return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent); // 對方法進行格式化傳回
    }           

上面代碼的方法的生成,也拆分成了幾個子步驟,主要包括:

  • 生成傳回值資訊;
  • 生成方法名資訊;
  • 生成方法參數資訊;
  • 生成方法的異常資訊;
  • 生成方法體資訊;

    需要注意的是,這裡所使用的所有類都是使用的其全限定類名,通過前面我們展示的PlantsWater的子類代碼也可以看出這一點。上面生成的資訊中,方法的傳回值,方法名,方法參數以及異常資訊都可以通過接口聲明擷取到,而方法體則需要根據一定的邏輯來生成。關于方法參數,需要說明的是,Dubbo并沒有使用接口中對應參數的名稱,而是對每一個參數的參數名依次使用arg0、arg1等名稱。這裡我們繼續閱讀Dubbo生成方法體内容的代碼。

private String generateMethodContent(Method method) {

        // 擷取方法上标注的@Adaptive注解,前面講到,Dubbo會使用該注解的值作為動态參數的key值
        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
        StringBuilder code = new StringBuilder(512);

        // 如果目前方法沒有标注@Adaptive注解,該方法的實作就會預設抛出異常
        if (adaptiveAnnotation == null) {
            return generateUnsupported(method);
        } else {

            // 擷取參數中類型為URL的參數所在的參數索引位置,因為我們的參數都是通過arg[i]的形式編排的,因而
            // 擷取其索引就可以得到該參數的引用。這裡URL參數的主要作用是擷取目标參數對應的參數值
            int urlTypeIndex = getUrlTypeIndex(method);
            
            if (urlTypeIndex != -1) {
                // 如果參數中存在URL類型的參數,那麼就為該參數進行空值檢查,如果為空,則抛出異常
                code.append(generateUrlNullCheck(urlTypeIndex));
            } else {
                // 如果參數中不存在URL類型的參數,那麼就會檢查每個參數,判斷其是否有某個方法的傳回值是URL類型,
                // 如果存在該方法,則首先對該參數進行空指針檢查,如果為空則抛出異常。然後調用該對象的目标方法,
                // 以擷取到一個URL對象,然後對擷取到的URL對象進行空值檢查,為空也會抛出異常。
                code.append(generateUrlAssignmentIndirectly(method));
            }

            // 這裡主要是擷取@Adaptive注解的參數,如果沒有配置,就會使用目标接口的類型由駝峰形式轉換為點分形式
            // 的名稱作為将要擷取的參數值的key名稱,比如前面的PlantsWater轉換後為plants.water。
            // 這裡需要注意的是,傳回值是一個數組類型,這是因為Dubbo會通過嵌套擷取的方式來的到目标參數,
            // 比如我們使用了@Adaptive({"client", "transporter"})的形式,那麼最終就會在URL對象中擷取兩次
            // 參數,如String extName = url.getParameter("client", url.getParameter("transporter"))
            String[] value = getMethodAdaptiveValue(adaptiveAnnotation);

            // 判斷是否存在Invocation類型的參數
            boolean hasInvocation = hasInvocationArgument(method);
            
            // 為Invocation類型的參數添加空值檢查的邏輯
            code.append(generateInvocationArgumentNullCheck(method));
            
            // 生成擷取extName的邏輯,也即前面通過String[] value生成的通過url.getParameter()的
            // 邏輯代碼,最終會得到使用者配置的擴充的名稱,進而對應某個基礎服務類
            code.append(generateExtNameAssignment(value, hasInvocation));

            // 為extName添加空值檢查代碼
            code.append(generateExtNameNullCheck(value));
            
            // 通過extName在ExtensionLoader中擷取其對應的基礎服務類,比如前面的PlantsWater,在這裡就是
            // PlantsWater extension = ExtensionLoader.getExtensionLoader(ExtensionLoader.class)
            // .getExtension(extName),這樣就得到了一個PlantsWater的執行個體對象
            code.append(generateExtensionAssignment());

            // 生成目标執行個體的目前方法的調用邏輯,然後将結果傳回。比如PlantsWater就是
            // return extension.watering(arg0);
            // 這裡方法名就是目前實作的方法的名稱,而參數就是目前方法傳入的參數,
            // 就是目标接口中的同一方法,而方法參數前面已經講到,都是使用arg[i]的形式命名的,因而這裡直接
            // 将其依次羅列出來即可
            code.append(generateReturnAndInvocation(method));
        }
        
        // 将生成的代碼傳回
        return code.toString();
    }           

上面的邏輯主要分為了如下幾個步驟:

  • 判斷目前方法是否标注了@Adaptive注解,如果沒有标注,則為其生成一個預設實作,該實作中會預設抛出異常,也就是說隻有使用@Adaptive注解标注的方法才是作為自适應機制的方法;
  • 擷取方法參數中類型為URL的參數,如果不存在,則擷取參數中某個存在可以傳回URL類型對象的方法的參數,并且調用該方法擷取URL參數;
  • 通過@Adaptive注解的配置擷取目标參數的key值,然後通過前面得到的URL參數擷取該key對應的參數值,進而得到了基礎服務類對應的名稱;
  • 通過ExtensionLoader擷取該名稱對應的基礎服務類執行個體;
  • 通過調用基礎服務類的執行個體的目前方法來實作最終的基礎服務。

    可以看到,這裡實作的自适應機制邏輯結構是非常清晰的,讀者通過閱讀這裡的源碼也就比較好的了解了Dubbo所提供的自适應機制的原理,也能夠比較好的通過自适應機制來完成某些定制化的工作。

參考

Dubbo Adaptive機制詳解