Dubbo Adaptive機制詳解

       Dubbo提供了一種SPI的機制用於動態的加載擴展類,可是如何在運行時動態的選用哪種擴展類來提供服務,這就須要一種機制來進行動態的匹配。Dubbo SPI中提供的Adaptive機制就爲解決這個問題提供了一種良好的解決方案,本文首先會經過一個示例來說解Adaptive機制的用法,而後會從源碼的角度對其實現原理進行講解。java

1. 用法示例

        對應於Adaptive機制,Dubbo提供了一個註解@Adaptive,該註解能夠用於接口的某個子類上,也能夠用於接口方法上。若是用在接口的子類上,則表示Adaptive機制的實現會按照該子類的方式進行自定義實現;若是用在方法上,則表示Dubbo會爲該接口自動生成一個子類,而且按照必定的格式重寫該方法,而其他沒有標註@Adaptive註解的方法將會默認拋出異常。對於第一種Adaptive的使用方式,Dubbo裏只有ExtensionFactory接口使用了,其有一個子類AdaptiveExtensionFactory就使用了@Adaptive註解進行了標註,主要做用就是在獲取目標對象時,分別經過ExtensionLoaderSpring容器兩種方式獲取,該類的實現原理比較簡單,讀者可自行閱讀其源碼,本文主要講解將@Adaptive註解標註在接口方法上以實現Adaptive機制的使用原理。這裏咱們以一個"水果種植者"的示例來進行講解,水果種植者能夠種植諸如蘋果和香蕉等水果。這裏咱們首先定義一個蘋果種植者的接口:apache

@SPI("apple")
public interface FruitGranter {

  Fruit grant();

  @Adaptive
  String watering(URL url);
}

       這裏須要注意的是,若是要使用Dubbo的SPI的支持,必須在目標接口上使用@SPI註解進行標註,後面的值提供了一個默認值,也就是說若是沒有自定義的指定使用哪一個子類,那麼就使用該值所指定的子類。在該接口中,咱們將watering()方法使用@Adaptive註解進行了標註,表示該方法在自動生成的子類中是須要動態實現的方法。下面是咱們爲FruitGranter提供的兩個實現類:數組

// 蘋果種植者
public class AppleGranter implements FruitGranter {

  @Override
  public Fruit grant() {
    return new Apple();
  }

  @Override
  public String watering(URL url) {
    System.out.println("watering apple");
    return "watering finished";
  }
}
// 香蕉種植者
public class BananaGranter implements FruitGranter {

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

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

        這裏提供了AppleGranterBananaGranter表示的實際上是兩種基礎服務類,本質上它們三者的關係是FruitGranter用於對外提供一個規範,而AppleGranterBananaGranter則是實現了這種規範的兩種基礎服務。至於調用方須要使用哪一種基礎服務來實現其功能,這就須要根據調用方指定的參數來動態選取的,而@Adaptive機制就是提供了這樣一種選取功能。緩存

        在Dubbo的SPI中,咱們指定了上述兩種服務類以後,須要在META-INF/dubbo下建立一個文件,該文件的名稱是目標接口的全限定名,這裏是org.apache.dubbo.demo.example.eg19.FruitGranter,在該文件中須要指定該接口全部可提供服務的子類,形式如:app

apple=org.apache.dubbo.demo.example.eg19.AppleGranter
banana=org.apache.dubbo.demo.example.eg19.BananaGranter

       文件中每個子類都有一個key與之對應,這個key也就是前面@SPI註解後面所指定的值,也就是說這裏若是調用方沒有自定義指定使用哪一個子類,那麼默認就會使用AppleGranter來提供服務。下面咱們來看一下調用方代碼如何實現:ide

public class ExtensionLoaderTest {

  @Test
  public void testGetExtensionLoader() {
    // 首先建立一個模擬用的URL對象
    URL url = URL.valueOf("dubbo://192.168.0.101:20880?fruit.granter=apple");
    // 經過ExtensionLoader獲取一個FruitGranter對象
    FruitGranter granter = ExtensionLoader.getExtensionLoader(FruitGranter.class)
      .getAdaptiveExtension();
    // 使用該FruitGranter調用其"自適應標註的"方法,獲取調用結果
    String result = granter.watering(url);
    System.out.println(result);
  }
}

        上述代碼中,咱們首先模擬構造了一個URL對象,這個URL對象是Dubbo中進行參數傳遞所使用的一個基礎類,在配置文件中配置的屬性都會被封裝到該對象中。這裏咱們主要要注意該對象是經過一個url構造的,而且url的最後咱們有一個參數fruit.granter=apple,這裏其實就是咱們所指定的使用哪一種基礎服務類的參數。好比這裏指定的就是使用apple對應的AppleGranterui

        在構造一個URL對象以後,咱們經過ExtensionLoader.getExtensionLoader(FruitGranter.class)方法獲取了一個FruitGranter對應的ExtensionLoader對象,而後調用其getAdaptiveExtension()方法獲取其爲FruitGranter接口構造的子類實例,這裏的子類實際上就是ExtensionLoader經過必定的規則爲FruitGranter接口編寫的子類代碼,而後經過javassistjdk編譯加載這段代碼,加載完成以後經過反射構造其實例,最後將其實例返回。在上面咱們調用該實例,也就是granter對象的watering()方法時,該方法內部就會經過url對象指定的參數來選擇具體的實例,從而將真正的工做交給該實例進行。經過這種方式,Dubbo SPI就實現了根據傳入參數動態的選用具體的實例來提供服務的功能。以下是該ExtensionLoaderFruitGranter動態生成的子類代碼:url

package org.apache.dubbo.demo.example.eg19;

import org.apache.dubbo.common.extension.ExtensionLoader;

public class FruitGranter$Adaptive implements org.apache.dubbo.demo.example.eg19.FruitGranter {

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

  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("fruit.granter", "apple");
    if (extName == null) {
      throw new IllegalStateException(
          "Failed to get extension (org.apache.dubbo.demo.example.eg19.FruitGranter) name " 
        + "from url (" + url.toString() + ") use keys([fruit.granter])");
    }
    org.apache.dubbo.demo.example.eg19.FruitGranter extension =
      (org.apache.dubbo.demo.example.eg19.FruitGranter) ExtensionLoader
        .getExtensionLoader(org.apache.dubbo.demo.example.eg19.FruitGranter.class)
        .getExtension(extName);
    return extension.watering(arg0);
  }
}

        關於該生成的代碼,咱們主要要注意以下幾個問題:.net

  • 全部未使用@Adaptive註解標註的接口方法,默認都會拋出異常;
  • 在使用@Adaptive註解標註的方法中,其參數中必須有一個參數類型爲URL,或者其某個參數提供了某個方法,該方法能夠返回一個URL對象;
  • 在方法的實現中會經過URL對象獲取某個參數對應的參數值,若是在接口的@SPI註解中指定了默認值,那麼在使用URL對象獲取參數值時,若是沒有取到,就會使用該默認值;
  • 最後根據獲取到的參數值,在ExtensionLoader中獲取該參數值對應的服務提供類對象,而後將真正的調用委託給該服務提供類對象進行;
  • 在經過URL對象獲取參數時,參數key獲取的對應規則是,首先會從@Adaptive註解的參數值中獲取,若是該註解沒有指定參數名,那麼就會默認將目標接口的類名轉換爲點分形式做爲參數名,好比這裏FruitGranter轉換爲點分形式就是fruit.granter

2. 實現原理

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

  • 加載標註有@Adaptive註解的接口,若是不存在,則不支持Adaptive機制;
  • 爲目標接口按照必定的模板生成子類代碼,而且編譯生成的代碼,而後經過反射生成該類的對象;
  • 結合生成的對象實例,經過傳入的URL對象,獲取指定key的配置,而後加載該key對應的類對象,最終將調用委託給該類對象進行。

        能夠看到,經過這種方式,Dubbo就實現了一種經過配置參數動態選擇所使用的服務的目的,而實現這種機制的入口主要在ExtensionLoader.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方法注入屬性。

        上面的經過setter方法注入屬性的方法比較簡單,主要是經過反射讀取相關的參數,而後分別在Dubbo的SPI和Spring容器中查找對應的bean,而且將其注入進來,這段代碼比較簡單,讀者可自行閱讀。咱們這裏主要關注Dubbo如何建立Adaptive類對象,也即getAdaptiveExtensionClass()方法的實現:

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的方式編譯生成的子類字符串,從而獲得一個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信息
  // 生成import信息,這裏只導入了ExtensionLoader類,其他的類都經過全限定名的方式來使用
  code.append(generateImports());
  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);	// 對方法進行格式化返回
 }

        能夠看到,方法的生成,也拆分紅了幾個子步驟,主要包括:

  • 生成返回值信息;
  • 生成方法名信息;
  • 生成方法參數信息;
  • 生成方法的異常信息;
  • 生成方法體信息;

        須要注意的是,這裏所使用的全部類都是使用的其全限定類名,經過前面咱們展現的FruitGranter的子類代碼也能夠看出這一點。上面生成的信息中,方法的返回值,方法名,方法參數以及異常信息均可以經過接口聲明獲取到,而方法體則須要根據必定的邏輯來生成。關於方法參數,須要說明的是,Dubbo並無使用接口中對應參數的名稱,而是對每個參數的參數名依次使用arg0arg1等名稱,後續在閱讀代碼時讀者須要注意這一點。這裏咱們繼續閱讀Dubbo生成方法體內容的代碼:

private String generateMethodContent(Method method) {
  // 獲取方法上標註的@Adaptive註解,前面講到,Dubbo會使用該註解的值做爲動態參數的key值
  Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
  StringBuilder code = new StringBuilder(512);
  if (adaptiveAnnotation == null) {
    // 若是當前方法沒有標註@Adaptive註解,該方法的實現就會默認拋出異常
    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名稱,好比前面的FruitGranter轉換後爲fruit.granter。
    // 這裏須要注意的是,返回值是一個數組類型,這是由於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中獲取其對應的基礎服務類,好比前面的FruitGranter,在這裏就是
    // FruitGranter extension = ExtensionLoader.getExtensionLoader(ExtensionLoader.class)
    // .getExtension(extName),這樣就獲得了一個FruitGranter的實例對象
    code.append(generateExtensionAssignment());

    // 生成目標實例的當前方法的調用邏輯,而後將結果返回。好比FruitGranter就是
    // return extension.watering(arg0);
    // 這裏方法名就是當前實現的方法的名稱,而參數就是當前方法傳入的參數,讀者不要忘記了當前方法
    // 就是目標接口中的同一方法,而方法參數前面已經講到,都是使用arg[i]的形式命名的,於是這裏直接
    // 將其依次羅列出來便可
    code.append(generateReturnAndInovation(method));
  }

  // 將生成的代碼返回
  return code.toString();
}

        上面的代碼中,總體邏輯仍是比較清晰的,讀者能夠根據上面的講解,而後結合前面展現的FruitGranter生成的子類代碼進行對比來看,這樣就能夠對整個實現邏輯有比較好的理解了。上面的邏輯主要分爲了以下幾個步驟:

  • 判斷當前方法是否標註了@Adaptive註解,若是沒有標註,則爲其生成一個默認實現,該實現中會默認拋出異常,也就是說只有使用@Adaptive註解標註的方法纔是做爲自適應機制的方法;
  • 獲取方法參數中類型爲URL的參數,若是不存在,則獲取參數中某個存在能夠返回URL類型對象的方法的參數,而且調用該方法獲取URL參數;
  • 經過@Adaptive註解的配置獲取目標參數的key值,而後經過前面獲得的URL參數獲取該key對應的參數值,從而獲得了基礎服務類對應的名稱;
  • 經過ExtensionLoader獲取該名稱對應的基礎服務類實例;
  • 經過調用基礎服務類的實例的當前方法來實現最終的基礎服務。

        能夠看到,這裏實現的自適應機制邏輯結構是很是清晰的,讀者經過閱讀這裏的源碼也就比較好的理解了Dubbo所提供的自適應機制的原理,也可以比較好的經過自適應機制來完成某些定製化的工做。

3. 小結

        本文首先經過一個示例來說解了Dubbo的自適應機制的使用方式,而後在源碼的層面對自適應機制的實現原理進行了講解。

相關文章
相關標籤/搜索