一文帶你徹底了解Dubbo的SPI機制

文章目錄

Java SPI使用

Dubbo為何自己實現(xiàn)一套SPI

Dubbo SPI初體驗

SPI

IOC

Aop

什么是包裝類?

AOP增強

@Adaptive

一些需要注意的地方

@Activate

結尾

在分布式系統(tǒng)中服務的調用,就要涉及到RPC。而提起RPC,想到最多的就是dubbo。了解dubbo的工作原理,有助于我們更好的使用它。

打開下載的dubbo工程,我發(fā)現(xiàn)在dubbo的各個子模塊,有很多這樣的類似代碼:ExtensionLoader.getExtensionLoader(xxx.class),經過我一番百度后發(fā)現(xiàn)他們指向一個名次:SPI。官網(wǎng)中表示Dubbo采用微內核+SPI,使得有特殊需求的接入方可以自定義擴展,做定制的二次開發(fā)。好像解釋了什么,但是又好像還不清楚它是干什么的,不過不要急,接下來我們就從例子使用到dubbo的源碼來剖析spi的神秘面紗。

Java SPI使用

在了解dubbo spi之前,我們就不得不提一下Java SPI。SPI的全稱是service provider interface,起初是提供給廠商做插件開發(fā)的 ,它使用了策略模式, 一個接口多種實現(xiàn)。我們只聲明接口, 具體的實現(xiàn)并不在程序中直接確定, 而是由程序之外的配置掌控 。啥意思呢?我們搞個例子就明白了:

//定義一個接口

public interface DoWork {
    void doWork();
}

//兩個實現(xiàn)類
public class WriteBug implements DoWork {
    @Override
    public void doWork() {
        System.out.println("寫bug");
    }
}
public class WriteCode implements DoWork {

    @Override
    public void doWork() {
        System.out.println("寫程序");
    }
}

在META-INF.services目錄下創(chuàng)建一個文件,文件名即為接口的全路徑名:com.alibaba.dubbo.demo.provider.JavaSPI.DoWork:這個樣子;

文件中的內容:

com.alibaba.dubbo.demo.provider.JavaSPI.WriteBug
com.alibaba.dubbo.demo.provider.JavaSPI.WriteCode

準備工作已經做完了,接下來測試一下效果如何:

  public static void main(String[] s){
        //獲取到DoWork的所有實現(xiàn)
        ServiceLoader<DoWork> serviceServiceLoader = ServiceLoader.load(DoWork.class);
        for (DoWork doWork : serviceServiceLoader){
                doWork.doWork();
        }
    }

運行結果:

寫bug
寫程序

可以看到執(zhí)行了全部實現(xiàn)類的方法,因此我們可以理解為spi實際上就是在文件中記錄接口都有哪些實現(xiàn)類,然后根據(jù)接口名來實例化它的實現(xiàn)類。

Dubbo為何自己實現(xiàn)一套SPI

但是實際上dubbo并沒有采用Java的SPI,而是自己實現(xiàn)了一套SPI機制。既然有Java的SPI,為什么dubbo不用呢?

我們觀察上面的例子,可以發(fā)現(xiàn)其實Java SPI實際上是將接口所有的實現(xiàn)類都加載出來了,如果項目過大,那么會加載全部的實現(xiàn)類,那肯定會有一些實現(xiàn)類實現(xiàn)類沒有用上,這樣就造成了浪費。

另一方面,Java的SPI功能也比較單一,dubbo的spi在此基礎上還實現(xiàn)了ioc、aop等功能,這些我會在下面從源碼的角度來分析這些功能。

Dubbo SPI初體驗

既然我們上面看了Java的SPI如何使用,那這個dubbo的spi我們也是需要看看的。

在dubbo中,約定文件是放在以下三個目錄中:

META-INF/services/

META-INF/dubbo/

META-INF/dubbo/internal/

大部分還是比較類似的,先定義一個接口、兩個實現(xiàn)類,要注意接口上面需要加@SPI注解的。

//注意加上@SPI注解
@SPI
public interface DoWork {
    void doWork();
}

public class WriteBug implements DoWork {

    @Override
    public void doWork() {
        System.out.println("寫bug");
    }
}
public class WriteCode implements  DoWork{


    @Override
    public void doWork() {
        System.out.println("寫程序");
    }
}

dubbo spi文件內容與java spi文件內容略微不太相同,可以理解為key-value的形式,value就是實現(xiàn)類的全路徑名,這個key呢可以理解為這個類名的簡稱,這個要記得,下面會用得到:

bug=com.alibaba.dubbo.demo.provider.DubboSPI.WriteBug
code=com.alibaba.dubbo.demo.provider.DubboSPI.WriteCode

public static void main(String[] args) {        
    ExtensionLoader<DoWork> extensionLoader = ExtensionLoader.getExtensionLoader(DoWork.class);  
    //這個code就是上面說的"簡稱"       
      DoWork code = extensionLoader.getExtension("code");        
      code.doWork();       
      DoWork bug = extensionLoader.getExtension("bug");        
      bug.doWork();    
}

ExtensionLoader類包含了整個SPI的核心方法,包括像下面的獲取@Adaptive、@Active注解的信息等,都是在這個類中實現(xiàn)的。

getExtensionLoader方法可以獲得對應接口的Extension加載器,這個方法也是比較簡單的;

@SuppressWarnings("unchecked")
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        if (type == null)
            throw new IllegalArgumentException("Extension type == null");
        if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
        }
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type(" + type +
                    ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
        }


        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);//從緩存中拿
        if (loader == null) {
            //緩存中沒有的話,就去new一個放到這個map中,然后再獲取返回
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

其實就是從緩存中拿,如果緩存中有,那么就取緩存中的,緩存中沒有,那么就new一個放進去。

ok,現(xiàn)在獲取到了擴展實現(xiàn)類加載器,接下來就看如何通過執(zhí)行getExtension(很重要,可以標記為五星重要程度,后面還會用到的)來獲取到對應的實例。

public T getExtension(String name) {
        if (name == null || name.length() == 0)
            throw new IllegalArgumentException("Extension name == null");
        if ("true".equals(name)) {
            return getDefaultExtension();
        }
        Holder<Object> holder = cachedInstances.get(name);
        if (holder == null) {
            cachedInstances.putIfAbsent(name, new Holder<Object>());
            holder = cachedInstances.get(name);
        }
        Object instance = holder.get();
        if (instance == null) {//雙檢鎖
            synchronized (holder) {
                instance = holder.get();
                if (instance == null) {
                    instance = createExtension(name);
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }

代碼行數(shù)雖然比較多,實際上都是從緩存中取,看是否有值,沒有值的話就去創(chuàng)建。因為我們現(xiàn)在是首次加載,那肯定是沒值的,所以就要到這個createExtension方法中:

private T createExtension(String name) {
        Class<?> clazz = getExtensionClasses().get(name);//獲取名稱對應的class
        if (clazz == null) {
            throw findException(name);
        }
        try {
            T instance = (T) EXTENSION_INSTANCES.get(clazz);
            if (instance == null) {
                //通過反射來創(chuàng)建實例
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            }
            injectExtension(instance);//依賴注入
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
                for (Class<?> wrapperClass : wrapperClasses) {//對于那些wrapper類型的,進行包裝一下
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }
            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                    type + ")  could not be instantiated: " + t.getMessage(), t);
        }
    }

這里的核心方法就兩個:getExtensionClasses、injectExtension。這兩個方法在后面的@Adaptive、@Active注解中都會用到。

首先是這個getExtensionClasses(非常重要,切記記得這個方法,后面還會用到他的)方法:

private Map<String, Class<?>> getExtensionClasses() {
        Map<String, Class<?>> classes = cachedClasses.get();
        if (classes == null) {
            synchronized (cachedClasses) {//又是經典的雙檢鎖。
                classes = cachedClasses.get();
                if (classes == null) {
                    classes = loadExtensionClasses();
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }

可以看到他其實也是這個從緩存中拿,沒有的話就創(chuàng)建,核心方法就是這個loadExtensionClasses方法。

private Map<String, Class<?>> loadExtensionClasses() {
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if (defaultAnnotation != null) {
            String value = defaultAnnotation.value();
            if ((value = value.trim()).length() > 0) {
                String[] names = NAME_SEPARATOR.split(value);
                if (names.length > 1) {
                    throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
                            + ": " + Arrays.toString(names));
                }
                if (names.length == 1) cachedDefaultName = names[0];
            }
        }


        Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
        loadDirectory(extensionClasses, DUBBO_DIRECTORY);
        loadDirectory(extensionClasses, SERVICES_DIRECTORY);
        return extensionClasses;
    }

還記得最開始的時候說的dubbo會掃描三個目錄下的文件么?沒錯就是這三個loadDirectory方法里面的目錄。

最終調用的loadClass加載方法:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
        if (!type.isAssignableFrom(clazz)) {
            throw new IllegalStateException("Error when load extension class(interface: " +
                    type + ", class line: " + clazz.getName() + "), class "
                    + clazz.getName() + "is not subtype of interface.");
        }
        if (clazz.isAnnotationPresent(Adaptive.class)) {//如果是自適應的話,只會有一個
            if (cachedAdaptiveClass == null) {
                cachedAdaptiveClass = clazz;
            } else if (!cachedAdaptiveClass.equals(clazz)) {
                throw new IllegalStateException("More than 1 adaptive class found: "
                        + cachedAdaptiveClass.getClass().getName()
                        + ", " + clazz.getClass().getName());
            }
        } else if (isWrapperClass(clazz)) {//包裝類的話,可以放到set中,這個是可以有多個的。
            Set<Class<?>> wrappers = cachedWrapperClasses;
            if (wrappers == null) {
                cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
                wrappers = cachedWrapperClasses;
            }
            wrappers.add(clazz);
        } else {//說明既不是@Adaptive,也不是裝飾類,他只是普通的或者是active。
            clazz.getConstructor();//如果沒有默認構造,那么就報錯
            if (name == null || name.length() == 0) {
                name = findAnnotationName(clazz);
                if (name.length() == 0) {
                    throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
                }
            }
            String[] names = NAME_SEPARATOR.split(name);
            if (names != null && names.length > 0) {
                Activate activate = clazz.getAnnotation(Activate.class);
                if (activate != null) {
                    cachedActivates.put(names[0], activate);
                }
                for (String n : names) {
                    if (!cachedNames.containsKey(clazz)) {
                        cachedNames.put(clazz, n);//設置class與名字的映射
                    }
                    Class<?> c = extensionClasses.get(n);
                    if (c == null) {
                        extensionClasses.put(n, clazz);//記錄名字與類的映射
                    } else if (c != clazz) {
                        throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
                    }
                }
            }
        }
    }






在這里我給大家總結一下這個方法:先看這個類上面有沒有@Adaptive注解。如果有@Adaptive注解,那么就將它緩存起來,原則上一個接口只允許有一個@Adaptive,這是為啥呢?我們可以看上面的代碼,如果有多個@Adaptive注解的話,他是會拋出異常的。

然后再看這個接口的實現(xiàn)類是否有以Wrapper結尾的實現(xiàn)類,比如Protocol的其中一個實現(xiàn)類:ProtocolFilterWrapper,如果是的話,就會放到一個set集合中。如果這兩種都不是的話,那么就是普通的擴展類,將他們存到一個map中返回就可以了。在存到這個map的時候,是以類對應的簡寫為key,類的路徑為value。

IOC

我們可以先看下這個injectExtension中有這么一段方法:

    private T injectExtension(T instance) {
        try {
            if (objectFactory != null) {
                for (Method method : instance.getClass().getMethods()) {
                    if (method.getName().startsWith("set")
                            && method.getParameterTypes().length == 1
                            && Modifier.isPublic(method.getModifiers())) {
  /

可以看到,它是先獲取到這個實例對應類中的全部方法,然后呢,依次循環(huán)遍歷,對于那些以set開頭、只有一個參數(shù)、并且為public的方法,我們就認為這是需要對這個實例進行屬性注入的部分。

Class<?> pt = method.getParameterTypes()[0];
try {
  String property = method.getName().length() > 3 ?       method.getName().substring(3,4).toLowerCase() + method.getName().substring(4) : "";
  Object object = objectFactory.getExtension(pt, property);
  if (object != null) {
    method.invoke(instance, object);
  }
}

這個其實就是要獲取到需要注入的屬性,就是將第四個字符設置為小寫,后面的不動進行分割。比如setUserName,分割完畢后就是userName,這樣有屬性,又有類路徑,就可以通過反射對原先的實例進行屬性注入,也就完成了依賴注入。

Aop

這里分為兩部分:判斷是否為包裝類以及對目標對象進行增強。

什么是包裝類?

舉個簡單的栗子:

public class Son implements Parent {
    private Parent parent;


    public Son(Parent parent) {
        this.parent = parent;
    }
}

在dubbo中是這個樣子的:



也就是實現(xiàn)類的構造方法的參數(shù),為他自己所實現(xiàn)的接口。而且在dubbo中。包裝類都是以Wrapper結尾的。

緩存包裝類

緩存包裝類是在ExtensionLoader.loadClass中:

else if (isWrapperClass(clazz)) {//包裝類的話,可以放到set中,這個是可以有多個的。

            Set<Class<?>> wrappers = cachedWrapperClasses;
            if (wrappers == null) {
                cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
                wrappers = cachedWrapperClasses;
            }
            wrappers.add(clazz);
        }

private boolean isWrapperClass(Class<?> clazz) {
        try {
            Constructor<?> constructor = clazz.getConstructor(type);//看類的構造參數(shù)中,有沒有對應接口的參數(shù),如果有,就說明是包裝類
            return true;
        } catch (NoSuchMethodException e) {
            return false;
        }
    }

通過遍歷所有的實現(xiàn)類,找出這些包裝類,然后放入緩存中。

AOP增強

@SuppressWarnings("unchecked")
    private T createExtension(String name) {
        Map<String, Class<?>> extensionClasses = getExtensionClasses();
        Class<?> clazz = extensionClasses.get(name);//獲取名稱對應的class
        if (clazz == null) {
            throw findException(name);
        }
        try {
            T instance = (T) EXTENSION_INSTANCES.get(clazz);
            if (instance == null) {
                //通過反射來創(chuàng)建實例
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            }
            injectExtension(instance);//依賴注入
            //獲取包裝類集合
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
                //遍歷
                for (Class<?> wrapperClass : wrapperClasses) {
                    //對返回的對象進行包裝增強
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }
            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                    type + ")  could not be instantiated: " + t.getMessage(), t);
        }
    }

@Adaptive

@SPI注解那里存的是默認值,這個會在動態(tài)生成編譯的類中,會設置默認值

從上面的例子中可以看到,通過SPi,我們可以每次只加載對應接口的實現(xiàn)類,這樣可以減少加載全部實現(xiàn)類帶來的開銷。但是我又有一個問題了,我不想在啟動項目的時候加載這些擴展類,而是希望在運行時根據(jù)參數(shù),來動態(tài)加載擴展類,這個該怎么做呢?

此時@Adaptive注解就派上用場了。

@Adaptive有兩種用法:一種是添加到類上,另一種則是添加到方法上。

如果添加到類上,表示該類是接口的適配器。但是實際上,注解添加到類上的是很少的,大部分都是添加到方法上的。目前只有AdaptiveExtensionFactory和AdaptiveCompiler是添加到類上的。

在這里我們就以這個AdaptiveExtensionFactory為例,來看下這個:





AdaptiveExtensionFactory的類上有這個@Adaptive注解,在執(zhí)行構造方法的時候,就會將ExtensionLoader對應的實現(xiàn)類加載到一個List中緩存起來,這樣的話在getExtension的時候,傳入要取的class,key,就可以直接從這個list中取出對應的值。

2.事實上大部分情況下都是注解到方法上的,他會根據(jù)接口的信息,來動態(tài)拼接成一個代理類。為了更清楚了解@Adaptive的實現(xiàn),我們就從源碼的角度,以RegistryFactory為例子來看下這個過程到底是啥樣的:



方法起始于ExtensionLoader.getAdaptiveExtension:

public T getAdaptiveExtension() {
       //從緩存中拿,沒有的話就創(chuàng)建
       instance = createAdaptiveExtension();
      cachedAdaptiveInstance.set(instance);
    
        return (T) instance;
    }

 private T createAdaptiveExtension() {
     //主要方法為getAdaptiveExtensionClass
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    }

 private Class<?> getAdaptiveExtensionClass() {
        getExtensionClasses();//通過這個方法來獲取到Adaptive注解的類。
        if (cachedAdaptiveClass != null) {//如果有,那么就說明有@Adaptive注解添加到類上了。
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();//沒有的話就創(chuàng)建。注解在方法上的話,那么就需要進行編譯。
    }






這里我省略了一些無關緊要的代碼,保留了核心代碼,getExtensionClasses這個方法是不是感覺很熟悉呢?沒錯,在上面我們也介紹過他的源碼了,這里就不多說了,直接ctrl+f搜索就可以了。

因為這個接口上的@Adaptive是在接口上的,所以cachedAdaptiveClass是為空的,這里就需要執(zhí)行createAdaptiveExtensionClass去創(chuàng)建實現(xiàn)類了。

private Class<?> createAdaptiveExtensionClass() {
        String code = createAdaptiveExtensionClassCode();
        ClassLoader classLoader = findClassLoader();
        com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        return compiler.compile(code, classLoader);
    }

這個code里面就是包含了代理實現(xiàn)類的所有信息,他長這個樣子:



看起來不太直接,而且也沒辦法調試,我們可以將他復制下來,ctrl+alt+l進行格式化一下,這樣在下次啟動程序的時候,斷點就可以進入這個類中了。



進入這個方法中,根據(jù)傳入的URL中的參數(shù)值,來調用getExtension方法,最終得到需要的擴展類。

PS:在dubbo3.0的時候項目中就已經放了這些$Adaptive的類了。

一些需要注意的地方

當@Adaptive添加到方法上的時候,要保證方法中的參數(shù)至少有一個為URL。

但是我們發(fā)現(xiàn)有的方法中的參數(shù),只有一個invoker,比如這樣的:



這個方法中就沒有URL,那這是咋回事?

通過查看拼接生成的代理類,我們發(fā)現(xiàn),通過Invoker是可以得到URL的:



所以從某種意義上來說的話,填入了Invoker也相當于填入了URL。

關于SPI注解與@Adaptive注解中的值。

SPI注解后面的值是默認的值,當URL中的值為空的時候,就使用默認的值,比如上面圖片中的:

String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());

這個dubbo就是默認值,也是@SPI中的值。

而@Adaptive添加到方法上時,后面的參數(shù)表示在加載時優(yōu)先使用的值。

@Activate

這個注解是擴展點自動激活加載,在這個注解上可以設置group、value、order等值。

這些值主要表示注解在的類、方法上所屬的分組、執(zhí)行順序等,當傳入的參數(shù)符合直接中的值的時候,可以進行激活。

這個注解對應的源碼,其實大部分在上面都已經說過了,總的來說,并不是特別的難,源碼在這里,大家可以先看一下:

 public List<T> getActivateExtension(URL url, String[] values, String group) {
        List<T> exts = new ArrayList<T>();
        List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
        if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
          //1
            getExtensionClasses();
            for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
                String name = entry.getKey();
                Activate activate = entry.getValue();
                if (isMatchGroup(group, activate.group())) {
                    if (!names.contains(name)
                            && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
                            && isActive(activate, url)) {
                        //2
                        T ext = getExtension(name);
                        exts.add(ext);
                    }
                }
            }
            //排序
            Collections.sort(exts, ActivateComparator.COMPARATOR);
        }
        List<T> usrs = new ArrayList<T>();
        for (int i = 0; i < names.size(); i++) {
            String name = names.get(i);
            if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX)
                    && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) {
                if (Constants.DEFAULT_KEY.equals(name)) {
                    if (!usrs.isEmpty()) {
                        exts.addAll(0, usrs);
                        usrs.clear();
                    }
                } else {
                  //3
                    T ext = c(name);
                    usrs.add(ext);
                }
            }
        }
        if (!usrs.isEmpty()) {
            exts.addAll(usrs);
        }
        return exts;
    }

他的核心方法就是我在里面標記的這幾個,其余的呢,都是循環(huán)啦,取值之類的操作,沒有什么太多的復雜邏輯。

結尾

以上就是dubbo spi的核心內容了,因為spi貫穿了dubbo的所有核心功能,所以要想看懂dubbo的源碼,那么就需要先了解SPI這個前置任務。大家可以一邊對照著文章,一邊打斷點,這樣對SPI也會了解更深入。

作者:java知路


歡迎關注微信公眾號 :java知路