1. 程式人生 > >Dubbo原始碼實現四:Dubbo中的擴充套件點與SPI

Dubbo原始碼實現四:Dubbo中的擴充套件點與SPI

       SPI的全稱是ServiceProviderInterface,即服務提供商介面。直白的說,它主要用來實現一個可擴充套件的Java應用。有人會覺得這就是建立在面向介面程式設計下的一種為了使元件可擴充套件或動態變更實現的規範,常見的類SPI的設計有JDBC、JNDI和JAXP等。例如JDBC的架構是由一套API組成,用於給Java應用提供訪問不同資料庫的能力,而資料庫提供商的驅動軟體各不相同,JDBC通過提供一套通用行為的API介面,底層可以由提供商自由實現,雖然JDBC的設計沒有指明是SPI,但也和SPI的設計類似。這裡有興趣的讀者可以參看2002年的一篇老文章:Replaceable Components andthe Service Provider Interface

      JDK為SPI的實現提供了工具類,即java.util.ServiceLoader,ServiceLoader中定義的SPI規範沒有什麼特別之處,只需要有一個提供者配置檔案(provider-configuration file),該檔案需要在resource目錄META-INF/services下,檔名就是服務介面的全限定名。檔案內容是提供者Class的全限定名列表,顯然提供者Class都應該實現服務介面。檔案必須使用UTF-8編碼。

         動嘴不如動手,直接結合例子理解起來更簡單,先新建我們服務介面類,該介面只有一個方法,那就是獲取提供者名稱:

package com.manzhizhen.study.spi;

/**

 * SPI服務介面
 */

public interface SpiService {

/**

 * 獲取提供商名稱

 * @return 提供商名稱

 */

     String getProviderName();

}

這裡我們假設有兩家服務提供商,StandardSpiService和MzzSpiService,他們當然都需要實現SpiService介面,程式碼如下:

package com.manzhizhen.study.spi;

public class StandardSpiService implements SpiService {

    @Override

public

String getProviderName() {

       return"Thisis StandardSpiService";

    }

}

package com.manzhizhen.study.spi;

public class MzzSpiService implements SpiService {

    @Override

public String getProviderName() {

       return"Thisis MzzSpiService";

    }

}

萬事俱備,現在我們可以建立服務提供者檔案了,在META-INF/services下新建名稱為com.manzhizhen.study.spi.SpiService的服務提供者檔案,因為目前只有兩家提供商,所以該檔案內容如下:

com.manzhizhen.study.spi.StandardSpiService

com.manzhizhen.study.spi.MzzSpiService

現在,我們寫一個main方法來樂呵(測試)一下:

public static void main(String[] args) {

   ServiceLoader<SpiService> spiServiceLoader = ServiceLoader.load(SpiService.class);

    while(true) {

       for (SpiService spiService : spiServiceLoader){

           System.out.println(spiService.getProviderName());

        }

       // 過段時間修改com.manzhizhen.study.spi.SpiService檔案看是否能做到動態增減SPI的實現

try {

           Thread.sleep(1000);

 // 為了驗證動態載入功能,這裡每隔一秒都重新reload

           spiServiceLoader.reload();

       } catch(InterruptedException e) {

           e.printStackTrace();

        }

    }

}

執行該main方法後,我們可以看到控制檯輸出如下:

This isStandardSpiService

This isMzzSpiService

This isStandardSpiService

This isMzzSpiService

...

然後我們刪除com.manzhizhen.study.spi.SpiService檔案的第二行(即MzzSpiService提供商),結果輸出變了:

This isStandardSpiService

This isMzzSpiService

This isStandardSpiService

This isMzzSpiService

This isStandardSpiService

This isStandardSpiService

This isStandardSpiService

...

有興趣的讀者可以看ServiceLoader內部的實現,其實整個流程就包含如下幾步:

1.讀取服務提供配置檔案。

2.newInstance例項化配置檔案中列舉的服務提供類,所以服務提供類需要有預設的構造方法。

3.將例項化的服務提供類儲存起來,即LinkedHashMap<String, S> providers = new LinkedHashMap<>()。

         可見,SPI並沒有什麼神奇的地方,只不過是一種通過面向介面程式設計來實現服務透明變更的規範而已,帶來的好處自然是我們業務系統變得擴充套件性更強。

         接下來我們看看Dubbo中利用SPI做了哪些事情。Dubbo中SPI進行了擴充套件,對服務提供者配置檔案中的內容進行了改造,由原來的提供者類的全限定名列表改成了KV形式的列表,這也導致了Dubbo中無法直接使用ServiceLoader,所以,與之對應的,在Dubbo中有ExtensionLoader,ExtensionLoader是擴充套件點載入器,用於載入Dubbo中的各種可配置元件,比如動態代理方式(ProxyFactory)、負載均衡策略(LoadBalance)、RCP協議(Protocol)、攔截器(Filter)、容器型別(Container)、叢集方式(Cluster)和註冊中心型別(RegistryFactory)等,總之,Dubbo為了應對各種場景,它的所有內部元件都是通過這種SPI的方式來管理的,這也是為什麼Dubbo需要將服務提供者配置檔案設計成KV鍵值對形式,這個K就是我們在Dubbo配置檔案或註解中用到的K,Dubbo直接通過服務介面(上面提到的ProxyFactory、LoadBalance、Protocol、Filter等)和配置的K從ExtensionLoader拿到服務提供的實現類。與ServiceLoader的load方法對應的是ExtensionLoader的getExtensionLoader方法:

public static <T>ExtensionLoader<T> getExtensionLoader(Class<T> type) {

    if (type == null)

       thrownew IllegalArgumentException("Extension type ==null");

    if(!type.isInterface()) {

       thrownew IllegalArgumentException("Extensiontype("+ type + ") is notinterface!");

    }

    if(!withExtensionAnnotation(type)) {

       thrownew IllegalArgumentException("Extensiontype("+ type +

                ") is not extension, because [email protected]"+ SPI.class.getSimpleName()+"Annotation!");

    }

   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;

}

其中的EXTENSION_LOADERS的定義如下:

private static final ConcurrentMap<Class<?>,ExtensionLoader<?>>EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();

可見,不同的服務介面型別都在EXTENSION_LOADERS中有對應一個ExtensionLoader物件,這樣能方便的管理不同服務介面的擴充套件點。當得到對應服務介面的ExtensionLoader後,就直接通過服務提供配置檔案中的K來拿對應的服務提供者實現類的例項了(通過ExtensionLoader#),所以,在Dubbo的原始碼中隨處可見如下程式碼:

ExtensionLoader.getExtensionLoader(Container.class).getExtension("spring");

ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(Constants.DEFAULT_LOADBALANCE);

ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(RoundRobinLoadBalance.NAME);

當然,對於提供者配置檔案來說,是可以重複配置的,只是多配置的會被忽略掉,這裡ServiceLoader和ExtensionLoader採取的策略是類似的。

          看到這裡,我們已經明白Dubbo框架內部是如何採用SPI來組裝自己的功能的,那麼如果某些擴充套件點是由使用方(即Dubbo的使用者)來定義的,Dubbo是怎麼載入它們的呢?前面說了ServiceLoader中預設的服務提供者配置檔案的目錄是MEAT-INF/services/,該目錄由ServiceLoader的PREFIX來定義,而Dubbo中則會在三種目錄下去載入服務提供者配置檔案,由ExtensionLoader的三個成員變數來定義:

private static final String SERVICES_DIRECTORY = "META-INF/services/";

private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";

private static final String DUBBO_INTERNAL_DIRECTORY= DUBBO_DIRECTORY+"internal/";

即對於每個CLASSPATH,Dubbo都會去掃描資原始檔夾下的這三個目錄來載入擴充套件點,載入過程可以參看ExtensionLoader#ExtensionLoader:

private Map<String, Class<?>>loadExtensionClasses() {

    final SPI defaultAnnotation = type.getAnnotation(SPI.class);

    if(defaultAnnotation != null) {

        Stringvalue = defaultAnnotation.value();

       if(value != null&& (value = value.trim()).length() > 0) {

           String[] names = NAME_SEPARATOR.split(value);

           if(names.length> 1) {

                throw new IllegalStateException("more than 1 default extension name onextension "+ type.getName()

                        + ": " + Arrays.toString(names));

            }

           if(names.length== 1) cachedDefaultName = names[0];

        }

    }

   Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();

   loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);

   loadFile(extensionClasses, DUBBO_DIRECTORY);

   loadFile(extensionClasses, SERVICES_DIRECTORY);

    return extensionClasses;

}

可以看到最後幾行就是去這三個目錄下去找服務提供者配置檔案,loadFile的方法是通用的,因為很多開源框架都有資源查詢的需求,比如說Spring,所以loadFile的實現這裡就不給出了。從這個方法也可以看出,Dubbo中的所有服務介面都標有@SPI註解,這樣就能輕易的區分出該介面下是否會有服務提供類。但Dubbo是怎麼保證掃描到所有包含服務提供者配置檔案的呢?因為我們知道,在Spring容器中,如果它需要掃描包含@Service註解的實現類時,它需要我們去指導Spring去哪些CLASSPATH路徑下面找這些需要註冊的服務Bean,但Dubbo好像並不需要我們顯式的告訴它去哪些地方找這些服務提供者配置檔案。後來才發現,在ClassLoader中已經把它所載入的class和其對應的package資訊(packages)、對應的檔案系統路徑(pdcache)都儲存了起來,由於我們實現Dubbo的服務介面來自定義Dubbo擴充套件點時,我們需要依賴Dubbo的包,所以業務類的類載入器只會是Dubbo容器的父類載入器或者是同一個類載入器,這樣就能很容易找到所有的CLASSPATH了(雙親委派模型),所以Dubbo也是在所有的CLASS PATH下去查詢這三個檔案。

可以看出,Java體系的SPI還是大有用途,是一種面向介面程式設計的經典案例。