读源码学架构系列:SPI之Dubbo实现

TL;DR

前面两篇已经分析了JDKSpringSPI的实现,今天将分析著名的RPC框架Dubbo中的SPI实现机制。

Dubbo的实现博两家之长,并在实现上更进一步改进,并且基于此,Dubbo框架对开发者十分的友好,几乎所有的组件都给开发者预留出了扩展点,方便开发者自行实现不同的扩展,真正的实现了微内核+插件机制。

Dubbo框架的架构设计非常之美,本篇是读源码学架构设计之SPI分析的最后一篇,后面我会从架构设计的其它方面来进一步分析SpringDubbo,以及Mybatis等框架的源码,尽可能的全方位去感悟这些框架中的优秀设计。

本篇内容大纲:

  1. DubboSPI实现
  2. JDKSpringSPI的差异
  3. DubboSPI的应用
  4. 基于SPI实现Dubbo扩展

0x01 Dubbo的SPI实现

DubboSPI实现,服务的定义必须是用注解@SPI标注的类,该注解可以接收一个String类型的参数,用于指定该服务的默认实现。

服务加载器是ExtensionLoader<T>.class,它默认会从三个路径下加载资源文件:

  • META-INF/services/
  • META-INF/dubbo/
  • META-INF/dubbo/internal/

资源文件的命名与JDKSPI的命名规则相同,必须是服务接口类的全路径类名,内容为key=value的形式,其中key是配置名(就是@SPI注解可以接收的那个参数值),value是扩展实现类的全路径类名,同一个文件中可以有多个实现,每行定义一个。

例如,dubbo-common模块下,com.alibaba.dubbo.common.compiler.Compiler服务的资源文件的内容为:

adaptive=com.alibaba.dubbo.common.compiler.support.AdaptiveCompiler
jdk=com.alibaba.dubbo.common.compiler.support.JdkCompiler
javassist=com.alibaba.dubbo.common.compiler.support.JavassistCompiler

这里定义了三个实现,其中adaptivejdkjavassist分别为配置名,对应的值为实现类的全路径类名。

Compiler接口的定义中:

/**
 * Compiler. (SPI, Singleton, ThreadSafe)
 */
@SPI("javassist")
public interface Compiler {

    /**
     * Compile java source code.
     *
     * @param code        Java source code
     * @param classLoader classloader
     * @return Compiled class
     */
    Class<?> compile(String code, ClassLoader classLoader);

}

@SPI注解指定了参数javassist,即,默认的实现为com.alibaba.dubbo.common.compiler.support.JavassistCompiler

现在,SPI需要的四个角色已经齐了。

除了@SPI注解,Dubbo还提供了另外两个注解:@Adaptive@Activate,这三个注解的用途分别是什么呢?

  • @SPI:主要用于接口上。扩展接口的标识注解,接收默认实现的配置名参数。
  • @Adaptive:主要用于方法上。用于指定哪个目标扩展会被注入,目标扩展的名称取决于URL的参数值和这个注解的参数中指定的参数值。如果没有找到指定的参数,则会使用默认的扩展来注入(在@SPI中指定)。如果配置了多个参数,则会按配置的参数名的顺序依次从URL中查找对应的参数值,一旦找到就会停止查找并进行注入,如果全部找完都没有找到,则会抛出异常。如果服务定义接口的注解@SPI中没有指定默认的配置名,则会按一定的规则自动生成一个(具体的规则是:将类名按大写字母做切分,然后转为全小写,再用.号将切分后转为小写的字符按之前的顺序连接起来,如YyyInvokerWrapper则会转为yyy.invoker.wrapper),并且会用这个名称作为参数名去从URL中查找参数值。
  • @Activate:主要用于扩展实现类上。用于根据给定的条件自动激活确定的扩展,具体的条件通过注解的参数来指定。

Dubbo的服务提供者ExtensionLoader结合这三个注解,极大的增强了SPI的功能。具体来说,Dubbo中的扩展点有以下特性:

  • 扩展点自动包装
  • 扩展点自动装配
  • 扩展点自适应
  • 扩展点自动激活

我们一个一个分析Dubbo是如何来实现这些功能的。

因为这些特性是在扩展伴随着扩展的加载过程而体现出来的,Dubbo提供了三个注解,对应于三个加载的方法,我们逐个分析对应于每一个注解的扩展加载方法,来看看在这些过程中,Dubbo是如何实现上面这些特性的。

对应于三个注解的三个扩展加载方法:

  • getExtension(String name)
  • getAdaptiveExtension()
  • getActivateExtension(URL url, String key)

这三个方法都定义在ExtensionLoader类中,这个类是一个泛型类,构造方法被private修饰掉,工厂方法为getExtensionLoader(Class<T> type),具体实现如下:

从上面的代码可以看到,Dubbo中扩展的定义必须是接口,同时,服务接口必须使用SPI注解标注,否则会抛出异常。

接下来会看到,对于所有的Class<T> type,都会首先从本地缓存中去查找是否已经创建过ExtensionLoader<T>,如果没有,才会新创建,并且会通过CAS操作放入到本地缓存(ConcurrentMap)中。

getExtension(String name)分析

现在来分析前面的三个方法,首先看第一个:getExtension(String name),这个方法是用来根据传入的name名称查找扩展的,如果没有找到会抛出异常。具体实现如下:

通过name查找扩展时,首先也会从本地缓存cachedInstances中查找,缓存中存放时会使用Holder类进行装饰,默认没有查找到时,会创建一个新的Holder对象,并以当前的namekey,新创建的Holder对象为value,组成键值对通过CAS的方式保存到本地缓存中。

然后接下来就从holder中去拿对应name的扩展,如果没有,则通过双重检查的方式来创建(类似于双重检查方式的单例模式,但要注意的是,局部变量的引用需要用volatile修饰,来保证对象创建的过程中,不会发生指令重排序),这里同步的是holder对象,我们查看Holder类的代码如下:

注意这里的valuevolatile修饰,所以上面的双重检查方式可以确保只会创建一次,且创建对象的过程不会发生指令重排序。

接着看createExtension方法:

首先调用getExtensionClasses()方法来获取扩展类的Class

这里又是同样的套路,将Class缓存到本地的cachedClassed缓存中,同样是双重检查(注意cachedClasses的类型,也是一个Holder类型),继续看loadExtensionClasses()方法:

这里会通过反射去查看当前类型的ExtensionLoader(在getExtensionLoader()方法中传入的参数的类型)是否使用了SPI注解标注,并且会判断是否指定了默认值(即指定默认的配置名,这个配置名就是资源文件中配置的键值对的键);然后会从三个目录路径去查找扩展的实现。

loadDirectory方法的具体实现:

查找到匹配的文件之后,会进一步进行解析,即loadResource()方法:

这里会对每个文件按照先前定义的规则进行解析,找到了配置的扩展实现类时,就会调用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<Class<?>> wrappers = cachedWrapperClasses;
            if (wrappers == null) {
                cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
                wrappers = cachedWrapperClasses;
            }
            wrappers.add(clazz);
        } else {
            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<?> 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注解标注,如果有,还会进一步判断cachedAdaptiveClass有没有赋值,如果已经赋值,说明检测到了多个默认实现,此时会抛出异常,否则会将默认实现保存在cachedAdaptiveClass变量中。

如果没有使用@Adaptive注解标注,则会进一步判断实现类是不是扩展接口的包装类:

    private boolean isWrapperClass(Class<?> clazz) {
        try {
            clazz.getConstructor(type);
            return true;
        } catch (NoSuchMethodException e) {
            return false;
        }
    }

判断的标准就是实现类有没有定义针对扩展接口的构造函数,如果有,则会被定义会包装类,否则就不是包装类。

如果判断结果是包装类,则会缓存到本地缓存cachedWrapperClasses中。

如果也不是包装类,则继续走最后的else分支。

这里首先会对配置名(资源文件中定义的键值对的键)进行空判断,里面兼容了过期的注解@Extension;接着,会对配置名按逗号进行分割(这也就是说资源文件中配置扩展的实现类时,配置名可以配置多个,用逗号隔开即可),当配置名存在时,还会先检查实现类有没有被@Activate注解标注,如果有,则会将分割后的第一个配置名和@Activate注解信息作为键值对,缓存在本地缓存cachedActivates中。

接着,对分割后的配置名列表循环处理,首先判断扩展实现类的配置名有没有缓存在本地缓存中,如果没有,则缓存到本地缓存cachedNames中(这里也只会缓存第一个);然后如果实现类和配置名还没有存入loadDirectory()方法传入的Map对象的话,就将解析到的配置名和实现类的Class作为键值对,放入这个Map中。

到这里,对于当前ExtensionLoader<T>类型的所有扩展实现已经解析完成,我们回到createExtension()方法的第一行:getExtensionClasses().get(name),这里会从上面解析的结果Map中按传入的配置名进行查找对应的实现类的Class,如果没有找到就会抛出异常。

    private T createExtension(String name) {
        Class<?> clazz = getExtensionClasses().get(name);
        if (clazz == null) {
            throw findException(name);
        }
        try {
            T instance = (T) EXTENSION_INSTANCES.get(clazz);
            if (instance == null) {
                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);
        }
    }

如果找到了,接下来又是从本地缓存中去查找扩展实例对象,没有找到缓存才通过CAS方式使用class.newInstance()来创建新的实例,同时缓存到本地。接下来有一行调用:injectExtension(instance),具体实现为:

    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())) {
                        /**
                         * Check {@link DisableInject} to see if we need auto injection for this property
                         */
                        if (method.getAnnotation(DisableInject.class) != null) {
                            continue;
                        }
                        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);
                            }
                        } catch (Exception e) {
                            logger.error("fail to inject via method " + method.getName()
                                    + " of interface " + type.getName() + ": " + e.getMessage(), e);
                        }
                    }
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return instance;
    }

首先对objectFactory进行了为空判断,这个变量是在ExtensionLoader的构造方法中初始化的,只要getExtensionLoader(Class<T>)方法传入的类型不是ExtensionFactory类型,则会调用ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension())来初始化,具体过程我们稍后再分析,这个方法也是我们前面要分析的三个方法之一。

我们回到injectExtension()方法,objectFactory不为空时,会反射获取传入的对象实例的所有方法列表,然后判断所有以set开头(即setter方法),且只有一个参数,同时是使用public修饰的方法;找到后,继续判断,要求方法没有被@DisableInject注解标注,然后将找到的方法的参数类型取出来,通过分割setter方法,找到具体的属性名property,然后使用参数类型和属性名去objectFactory中查找有没有对应的扩展对象,如果有,则反射调用setter方法进行注入,这个过程就是Dubbo框架对扩展的自动装配的实现。(objectFactory对象初始化时,也是通过服务加载器ExtensionLoader来加载的,所以,对于查找到的扩展实现会缓存在对应的ExtensionLoader实例中)

继续回到createExtension()方法,接下来,如果之前解析时,找到的当前类型的Wrapper集合不为空,则会循环对Wrapper类进行注入并实例化,这里要注意循环中的包装实例化代码:

for (Class<?> wrapperClass : wrapperClasses) {
    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}

这里包装时,传入的参数是instance,包装实例化完成后又重新赋值给了instance进行下一次包装,直到所有的包装类全部包装完成,最后把所有包装类包装后的实例化对象作为结果返回出去。这也是Dubbo框架对扩展的自动包装的实现

createExtension()方法的执行就到此结束了,我们再转回到getExtension(String name)方法,在创建了扩展实例后,将实例放到了之前创建的空Holder对象中,到这里getExtension(String name)方法就处理完成了。

接下来分析getAdaptiveExtension()方法。

getAdaptiveExtension()分析

方法的具体实现如下:

    public T getAdaptiveExtension() {
        Object instance = cachedAdaptiveInstance.get();
        if (instance == null) {
            if (createAdaptiveInstanceError == null) {
                synchronized (cachedAdaptiveInstance) {
                    instance = cachedAdaptiveInstance.get();
                    if (instance == null) {
                        try {
                            instance = createAdaptiveExtension();
                            cachedAdaptiveInstance.set(instance);
                        } catch (Throwable t) {
                            createAdaptiveInstanceError = t;
                            throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
                        }
                    }
                }
            } else {
                throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
            }
        }

        return (T) instance;
    }

同样是Holder包装的本地缓存cachedAdaptiveInstance配合双重检查的方式创建Adaptive扩展,我们直接看createAdaptiveExtension()方法:

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

该方法直接对getAdaptiveExtensionClass()创建的实例对象进行包装并返回,我们进一步看getAdaptiveExtensionClass()方法:

private Class<?> getAdaptiveExtensionClass() {
        getExtensionClasses();
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

getExtensionClasses()方法前面已经分析过,会查找并解析所有的扩展实现类,并将查找到的结果缓存到本地。接着就是判断cachedAdaptiveClass是否为null,这个cachedAdaptiveClass我们在前面分析loadClass()方法时,有一个条件分支就是判断扩展实现类是否有被@Adaptive注解标注的,有的话会赋值给cachedAdaptiveClass变量,有多个会抛出异常。在这里,如果前面已经检测到了,就会直接返回这个结果,如果没有,则继续调用createAdaptiveExtensionClass()方法。

这个方法的代码很长,就不贴上来了,首先,它会获取当前ExtensionLoader<T>绑定的类型的所有方法列表,依次判断方法有没有被@Adaptive注解标注,如果有,则会给它生成一个Adaptive类,使用一个StringBuilder类来存放该类的代码,最后将生成的Adaptive类的代码以String类型返回。

createAdaptiveExtensionClass()方法在拿到了生成的类的代码后,使用ExtensionLoaderclassLoader对象和Compiler.class扩展的默认实现JavassistCompiler在内存中对生成的类代码进行编译,得到对应的Class文件的内容。

回到createAdaptiveExtension()方法,拿到了Adaptive类的Class后,通过反射生成了该类的实例,然后将该实例对象作为参数传给injectExtension()方法进行依赖注入。这个处理过程就是Dubbo框架对扩展的自适应的实现

getAdaptiveExtension()的作用就是利用装饰模式对extension接口class进行包装,然后生成一个以$Adaptive结尾的装饰类,该类会实现用@Adaptive注解标注的方法,在方法中动态的根据url传入的参数来动态的调用不同的的接口实现类来处理逻辑。

还有一种情况的Adaptive类不是动态生成的,比如Compiler.class接口的AdaptiveCompiler.class类,是本来就实现好的,它会判断ApplicationConfig在配置时是否有指定默认的Compiler,没有的话就获取默认的扩展实现。本质上还是装饰模式的应用。

下面的代码是运行时动态生成的Protocol$Adaptive类的代码:

package com.alibaba.dubbo.rpc;

import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
import com.alibaba.dubbo.rpc.Exporter;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Protocol;
import com.alibaba.dubbo.rpc.RpcException;

public class Protocol$Adaptive implements Protocol {
    public Invoker refer(Class class_, URL uRL) throws RpcException {
        String string;
        if (uRL == null) {
            throw new IllegalArgumentException("url == null");
        }
        URL uRL2 = uRL;
        String string2 = string = uRL2.getProtocol() == null ? "dubbo" : uRL2.getProtocol();
        if (string == null) {
            throw new IllegalStateException(new StringBuffer().append("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(").append(uRL2.toString()).append(") use keys([protocol])").toString());
        }
        Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(string);
        return protocol.refer(class_, uRL);
    }

    public Exporter export(Invoker invoker) throws RpcException {
        String string;
        if (invoker == null) {
            throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
        }
        if (invoker.getUrl() == null) {
            throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
        }
        URL uRL = invoker.getUrl();
        String string2 = string = uRL.getProtocol() == null ? "dubbo" : uRL.getProtocol();
        if (string == null) {
            throw new IllegalStateException(new StringBuffer().append("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(").append(uRL.toString()).append(") use keys([protocol])").toString());
        }
        Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(string);
        return protocol.export(invoker);
    }

    @Override
    public void destroy() {
        throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }

    @Override
    public int getDefaultPort() {
        throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }
}

到这里getAdaptiveExtension()方法的实现就分析完成了,接下来分析最后一个方法getActivateExtension()

getActivateExtension()分析

经过对前面两个方法的分析,已经清楚了前面提到的Dubbo框架对扩展实现的四个特性中的三个,现在还有最后一个关于自适应的特性,我们先来看getActivateExtension()的处理过程:

    /**
     * Get activate extensions.
     *
     * @param url    url
     * @param values extension point names
     * @param group  group
     * @return extension list which are activated
     * @see com.alibaba.dubbo.common.extension.Activate
     */
    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)) {
            getExtensionClasses();
            for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
                String name = entry.getKey();
                Activate activate = entry.getValue();
                if (isMatchGroup(group, activate.group())) {
                    T ext = getExtension(name);
                    if (!names.contains(name)
                            && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
                            && isActive(activate, url)) {
                        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 {
                    T ext = getExtension(name);
                    usrs.add(ext);
                }
            }
        }
        if (!usrs.isEmpty()) {
            exts.addAll(usrs);
        }
        return exts;
    }

在方法的重载调用中,会将URLkey的值取出来,并以逗号分割成一个数组,然后会判断传入的值里面有没有-default配置,如果没有,就会调用前面分析过的getExtensionClasses()方法(该方法会调用到loadClass()方法,它里面的最后一个分支处理时,就有缓存配置名和对应的@Activate注解标注过的实现类的键值对到cachedActivates中),然后循环处理cachedActivates中的键值对。

在处理cachedActivates中的键值对时,会取出@Activate注解的group参数信息进行比较,如果匹配上了,就会查找对应配置名的扩展实现(这里要注意一下匹配的方法isMatchGroup()group参数为null@Activate没有配置group参数时,都会返回true),然后再进一步判断URL参数中传入的配置名是否在cachedActivates中,是否有配置-name格式的配置名,URL中是否有对应配置名为参数的值(包括以.配置名结尾的参数的值,如果匹配到了就会返回true,同时,如果@Activate注解没有配置value参数,默认也会返回true;对于判断结果为true的配置名,会将刚刚查找到的扩展实现加入到List中(这个List是最终的返回结果),然后对列表按ActivateComparator.COMPARATOR定义的排序规则进行排序。

接着,还会进一步对传入的配置名列表进行判断,只要传入的配置名不是以-开头,并且没有传入-name格式的配置名,就会查找该配置的扩展实现,并加入到一个List中,处理完之后,将这个List中的值全部添加到前面那个最终返回的List列表中。

有点绕,简单点说,getActivateExtension()方法就是根据默认规则和配置的参数去判断需要返回哪些要激活的扩展实现,然后把激活的扩展列表返回给调用方。

这个方法的处理逻辑就是Dubbo框架对框架的自动激活的处理

到这里,我们把三个注解对应的三个处理方法都分析了一篇,同时清楚了Dubbo框架对扩展点所做的四种特性的处理,也清楚了DubboSPI实现机制。

接下来,就与前面分析过的JDKSpring中的SPI实现比较一下。

0x02 与JDK和Spring的SPI的差异

JDKSPI最简单直接,SpringSPI实现也很简单清晰,只是在JDK实现的基础上,对资源文件文件名和内容的规范上做出了一些修改,同时,服务加载器中增加了一个方法。

DubboSPI使用了三个注解来实现,并且,对整个SPI实现的处理流程有自己的规范,在JDKSpring的实现基础上,增加了四种特性,并且在处理流程的每个级别上,能使用缓存的地方都使用了缓存,另外,还有内存动态编译等等,这些特性极大的提高了Dubbo框架的扩展性,尤其是在框架已经实现好的基础上,可以通过不修改代码,直接修改配置参数的方式来实现动态的装配不同的组件实现。

三种实现各有优劣,功能强大就对应着实现的复杂性也更大,不同的场景有不同的选择,永远都是那句话:合适的才是最好的。

接下来,我们看一下Dubbo中是如何使用SPI的。

0x03 Dubbo中SPI的应用

其实上面的分析过程中,已经有多处使用了自身的SPI机制来加载扩展实现了。比如ExtensionLoader中的objectFactory属性的初始化,又比如动态编译生成的$Adaptive类的源代码时,查找com.alibaba.dubbo.common.compiler.Compiler.class服务的默认实现,这两个地方都是通过Dubbo自身的SPI机制来实现的。

我们就以Compiler.class为例来看看如何使用DubboSPI吧,这里只是一种基本的使用方式,更完整的使用方式,我们后续从其它角度分析Dubbo源码时再看。

首先找到Compiler.class服务的定义:

/**
 * Compiler. (SPI, Singleton, ThreadSafe)
 */
@SPI("javassist")
public interface Compiler {

    /**
     * Compile java source code.
     *
     * @param code        Java source code
     * @param classLoader classloader
     * @return Compiled class
     */
    Class<?> compile(String code, ClassLoader classLoader);

}

该接口使用了SPI标注,并且SPI注解指定了参数值javassist

我们再看具体的实现:

可以看到有三个具体的实现,分别是AdaptiveCompilerJavassistCompilerJdkCompilerAbstractCompiler是抽象类,封装了通用逻辑,定义了doCompile()抽象方法,不是具体实现类。

再找到资源文件:

资源文件内容如下:

adaptive=com.alibaba.dubbo.common.compiler.support.AdaptiveCompiler
jdk=com.alibaba.dubbo.common.compiler.support.JdkCompiler
javassist=com.alibaba.dubbo.common.compiler.support.JavassistCompiler

以键值对的形式配置了上面的三种实现。

调用时的代码:ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();

前面已经分析过了getAdaptiveExtension()方法的逻辑,因为Compiler.java接口上已经指定了默认配置名,所以,Dubbo在加载Compiler.class服务时会去资源文件中查找javassist对应的实现,也就是JavassistCompiler实现。

0x04 基于SPI实现Dubbo扩展

接下来,我们开始动手实现一个Dubbo的序列化扩展,基于protostuff的实现。

首先找到com.alibaba.dubbo.common.serialize.Serialization.class服务的定义:

/**
 * Serialization. (SPI, Singleton, ThreadSafe)
 */
@SPI("hessian2")
public interface Serialization {

    /**
     * get content type id
     *
     * @return content type id
     */
    byte getContentTypeId();

    /**
     * get content type
     *
     * @return content type
     */
    String getContentType();

    /**
     * create serializer
     *
     * @param url
     * @param output
     * @return serializer
     * @throws IOException
     */
    @Adaptive
    ObjectOutput serialize(URL url, OutputStream output) throws IOException;

    /**
     * create deserializer
     *
     * @param url
     * @param input
     * @return deserializer
     * @throws IOException
     */
    @Adaptive
    ObjectInput deserialize(URL url, InputStream input) throws IOException;

}

该接口使用了SPI标注,并且SPI注解指定了参数值hessian2,也就是说Dubbo默认使用hessian2的实现来序列化对象,现在我们实现一个protostuff的实现,并通过实例来验证。

首先是新建dubbo-serialization-protostuff工程,引入dubbo-serialization-apiprotostuff相关的依赖,然后创建ProtostuffSerialization类,实现com.alibaba.dubbo.common.serialize.Serialization接口,具体代码如下:

/**
 * @author zhaoyang on 2020-06-24.
 */
public class ProtostuffSerialization implements Serialization {

    private static final byte PROTOSTUFF_SERIALIZATION_ID = 12;

    public ProtostuffSerialization() {
        System.out.println("protostuff serialization initialized");
    }

    @Override
    public byte getContentTypeId() {
        return PROTOSTUFF_SERIALIZATION_ID;
    }

    @Override
    public String getContentType() {
        return "x-application/protostuff";
    }

    @Override
    public ObjectOutput serialize(URL url, OutputStream output) throws IOException {
        return new ProtostuffObjectOutput(output);
    }

    @Override
    public ObjectInput deserialize(URL url, InputStream input) throws IOException {
        return new ProtostuffObjectInput(input);
    }

}

其它相关的源文件请参考示例工程(这里的实现代码参考了Dubbo 2.7.xdubbo-serialization-protostuff实现)。

创建META-INF/dubbo/com.alibaba.dubbo.common.serialize.Serialization资源文件,内容如下:

protostuff=me.zy.std.dubbo.serialization.protostuff.ProtostuffSerialization

然后新建一个dubbo-spi-api的接口工程,定义EchoService

/**
 * @author zhaoyang on 2020-06-24.
 */
public interface EchoService {

    String echo(String message);

}

新建dubbo-spi-provider工程,实现服务接口:

/**
 * @author zhaoyang on 2020-06-24.
 */
public class EchoServiceImpl implements EchoService {

    @Override
    public String echo(String message) {
        String now = new SimpleDateFormat("HH:mm:ss").format(new Date());
        System.out.println("[" + now + "] Hello " + message
            + ", request from consumer: " + RpcContext.getContext().getRemoteAddress());
        return message;
    }
}

并创建EchoProvider类:

/**
 * @author zhaoyang on 2020-06-24.
 */
public class EchoProvider {

    public static void main(String[] args) throws InterruptedException {
        ServiceConfig<EchoServiceImpl> service = new ServiceConfig<>();
        service.setInterface(EchoService.class);
        service.setRef(new EchoServiceImpl());
        service.setApplication(new ApplicationConfig("echo-provider"));
        service.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));
        service.setSerialization("protostuff");
        service.export();

        System.out.println("dubbo service started");
        new CountDownLatch(1).await();
    }
}

接下来,创建dubbo-spi-consumer工程,并创建EchoConsumer类:

/**
 * @author zhaoyang on 2020-06-24.
 */
public class EchoConsumer {

    public static void main(String[] args) {
        ReferenceConfig<EchoService> reference = new ReferenceConfig<>();
        reference.setApplication(new ApplicationConfig("echo-service"));
        reference.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));
        reference.setInterface(EchoService.class);
        EchoService service = reference.get();
        String message = service.echo("dubbo");
        System.out.println(message);
    }
}

在运行示例之前,需要在父工程目录下执行mvn installdubbo-spi-api包和dubbo-serialization-protostuff包安装到本地,然后启动zookeeper服务,注意版本(我使用的Dubbo版本为2.6.8,对应的zookeeper的版本为3.4.x)。

准备工作完成后,先运行EchoProvider类,输出如下:

[24/06/20 11:28:52:052 CST] main  INFO logger.LoggerFactory: using logger: com.alibaba.dubbo.common.logger.log4j.Log4jLoggerAdapter
[24/06/20 11:28:52:052 CST] main ERROR common.Version:  [DUBBO] Duplicate class com/alibaba/dubbo/common/Version.class in 2 jar [file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo-common/2.6.8/dubbo-common-2.6.8.jar!/com/alibaba/dubbo/common/Version.class, file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo/2.6.8/dubbo-2.6.8.jar!/com/alibaba/dubbo/common/Version.class], dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:28:52:052 CST] main  WARN extension.SpringExtensionFactory:  [DUBBO] No spring extension (bean) named:defaultCompiler, try to find an extension (bean) of type java.lang.String, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:28:52:052 CST] main  WARN extension.SpringExtensionFactory:  [DUBBO] No spring extension (bean) named:defaultCompiler, type:java.lang.String found, stop get bean., dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:28:53:053 CST] main  INFO config.AbstractConfig:  [DUBBO] Export dubbo service me.zy.std.dubbo.spi.api.EchoService to local registry, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:28:53:053 CST] main  INFO config.AbstractConfig:  [DUBBO] Export dubbo service me.zy.std.dubbo.spi.api.EchoService to url dubbo://192.168.0.108:20880/me.zy.std.dubbo.spi.api.EchoService?anyhost=true&application=echo-provider&bind.ip=192.168.0.108&bind.port=20880&dubbo=2.0.2&generic=false&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=56070&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33333&serialization=protostuff&side=provider&timestamp=1593012533112, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:28:53:053 CST] main  INFO config.AbstractConfig:  [DUBBO] Register dubbo service me.zy.std.dubbo.spi.api.EchoService url dubbo://192.168.0.108:20880/me.zy.std.dubbo.spi.api.EchoService?anyhost=true&application=echo-provider&bind.ip=192.168.0.108&bind.port=20880&dubbo=2.0.2&generic=false&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=56070&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33333&serialization=protostuff&side=provider&timestamp=1593012533112 to registry registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=echo-provider&dubbo=2.0.2&pid=56070&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33333&registry=zookeeper&timestamp=1593012533095, dubbo version: 2.6.8, current host: 192.168.0.108
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
[24/06/20 11:28:53:053 CST] main  INFO server.Server:  [DUBBO] qos-server bind localhost:33333, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:28:53:053 CST] main ERROR common.Version:  [DUBBO] Duplicate class com/alibaba/dubbo/remoting/exchange/Exchangers.class in 2 jar [file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo/2.6.8/dubbo-2.6.8.jar!/com/alibaba/dubbo/remoting/exchange/Exchangers.class, file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo-remoting-api/2.6.8/dubbo-remoting-api-2.6.8.jar!/com/alibaba/dubbo/remoting/exchange/Exchangers.class], dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:28:53:053 CST] main ERROR common.Version:  [DUBBO] Duplicate class com/alibaba/dubbo/remoting/Transporters.class in 2 jar [file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo-remoting-api/2.6.8/dubbo-remoting-api-2.6.8.jar!/com/alibaba/dubbo/remoting/Transporters.class, file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo/2.6.8/dubbo-2.6.8.jar!/com/alibaba/dubbo/remoting/Transporters.class], dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:28:53:053 CST] main ERROR common.Version:  [DUBBO] Duplicate class com/alibaba/dubbo/remoting/RemotingException.class in 2 jar [file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo/2.6.8/dubbo-2.6.8.jar!/com/alibaba/dubbo/remoting/RemotingException.class, file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo-remoting-api/2.6.8/dubbo-remoting-api-2.6.8.jar!/com/alibaba/dubbo/remoting/RemotingException.class], dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:28:53:053 CST] main  INFO transport.AbstractServer:  [DUBBO] Start NettyServer bind /0.0.0.0:20880, export /192.168.0.108:20880, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:28:54:054 CST] main  INFO zookeeper.ZookeeperRegistry:  [DUBBO] Load registry store file /Users/unclezhao/.dubbo/dubbo-registry-echo-provider-127.0.0.1:2181.cache, data: {me.zy.std.dubbo.spi.api.EchoService=empty://192.168.0.108:20880/me.zy.std.dubbo.spi.api.EchoService?anyhost=true&application=echo-provider&category=configurators&check=false&dubbo=2.0.2&generic=false&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=55396&serialization=protostuff&side=provider&timestamp=1593011993807, com.alibaba.dubbo.samples.echo.api.EchoService=empty://192.168.3.151:20880/com.alibaba.dubbo.samples.echo.api.EchoService?anyhost=true&application=echo-provider&bean.name=com.alibaba.dubbo.samples.echo.api.EchoService&category=configurators&check=false&dubbo=2.0.2&generic=false&interface=com.alibaba.dubbo.samples.echo.api.EchoService&methods=echo&pid=27439&side=provider&timestamp=1591923292603}, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:28:54:054 CST] main  INFO zookeeper.ZookeeperRegistry:  [DUBBO] Register: dubbo://192.168.0.108:20880/me.zy.std.dubbo.spi.api.EchoService?anyhost=true&application=echo-provider&dubbo=2.0.2&generic=false&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=56070&serialization=protostuff&side=provider&timestamp=1593012533112, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:28:54:054 CST] main  INFO zookeeper.ZookeeperRegistry:  [DUBBO] Subscribe: provider://192.168.0.108:20880/me.zy.std.dubbo.spi.api.EchoService?anyhost=true&application=echo-provider&category=configurators&check=false&dubbo=2.0.2&generic=false&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=56070&serialization=protostuff&side=provider&timestamp=1593012533112, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:28:54:054 CST] main  INFO zookeeper.ZookeeperRegistry:  [DUBBO] Notify urls for subscribe url provider://192.168.0.108:20880/me.zy.std.dubbo.spi.api.EchoService?anyhost=true&application=echo-provider&category=configurators&check=false&dubbo=2.0.2&generic=false&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=56070&serialization=protostuff&side=provider&timestamp=1593012533112, urls: [empty://192.168.0.108:20880/me.zy.std.dubbo.spi.api.EchoService?anyhost=true&application=echo-provider&category=configurators&check=false&dubbo=2.0.2&generic=false&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=56070&serialization=protostuff&side=provider&timestamp=1593012533112], dubbo version: 2.6.8, current host: 192.168.0.108
dubbo service started

再运行EchoConsumer类,输出如下:

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/bin/java "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=58442:/Applications/IntelliJ IDEA.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/lib/tools.jar:/Users/unclezhao/Raymond/work/workspaces/stdspaces/architecture-std/dubbo-spi-consumer/target/classes:/Users/unclezhao/.m2/repository/com/alibaba/dubbo/2.6.8/dubbo-2.6.8.jar:/Users/unclezhao/.m2/repository/org/springframework/spring-context/5.2.3.RELEASE/spring-context-5.2.3.RELEASE.jar:/Users/unclezhao/.m2/repository/org/springframework/spring-aop/5.2.3.RELEASE/spring-aop-5.2.3.RELEASE.jar:/Users/unclezhao/.m2/repository/org/springframework/spring-beans/5.2.3.RELEASE/spring-beans-5.2.3.RELEASE.jar:/Users/unclezhao/.m2/repository/org/springframework/spring-core/5.2.3.RELEASE/spring-core-5.2.3.RELEASE.jar:/Users/unclezhao/.m2/repository/org/springframework/spring-jcl/5.2.3.RELEASE/spring-jcl-5.2.3.RELEASE.jar:/Users/unclezhao/.m2/repository/org/springframework/spring-expression/5.2.3.RELEASE/spring-expression-5.2.3.RELEASE.jar:/Users/unclezhao/.m2/repository/org/javassist/javassist/3.20.0-GA/javassist-3.20.0-GA.jar:/Users/unclezhao/.m2/repository/org/jboss/netty/netty/3.2.5.Final/netty-3.2.5.Final.jar:/Users/unclezhao/.m2/repository/org/apache/curator/curator-framework/2.12.0/curator-framework-2.12.0.jar:/Users/unclezhao/.m2/repository/org/apache/curator/curator-client/2.12.0/curator-client-2.12.0.jar:/Users/unclezhao/.m2/repository/org/apache/zookeeper/zookeeper/3.4.8/zookeeper-3.4.8.jar:/Users/unclezhao/.m2/repository/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar:/Users/unclezhao/.m2/repository/log4j/log4j/1.2.16/log4j-1.2.16.jar:/Users/unclezhao/.m2/repository/jline/jline/0.9.94/jline-0.9.94.jar:/Users/unclezhao/.m2/repository/com/google/guava/guava/16.0.1/guava-16.0.1.jar:/Users/unclezhao/.m2/repository/com/alibaba/dubbo-remoting-netty4/2.6.8/dubbo-remoting-netty4-2.6.8.jar:/Users/unclezhao/.m2/repository/com/alibaba/dubbo-remoting-api/2.6.8/dubbo-remoting-api-2.6.8.jar:/Users/unclezhao/.m2/repository/com/alibaba/dubbo-common/2.6.8/dubbo-common-2.6.8.jar:/Users/unclezhao/.m2/repository/commons-logging/commons-logging/1.2/commons-logging-1.2.jar:/Users/unclezhao/.m2/repository/com/alibaba/hessian-lite/3.2.5/hessian-lite-3.2.5.jar:/Users/unclezhao/.m2/repository/com/alibaba/fastjson/1.2.60/fastjson-1.2.60.jar:/Users/unclezhao/.m2/repository/com/esotericsoftware/kryo/4.0.1/kryo-4.0.1.jar:/Users/unclezhao/.m2/repository/com/esotericsoftware/reflectasm/1.11.3/reflectasm-1.11.3.jar:/Users/unclezhao/.m2/repository/org/ow2/asm/asm/5.0.4/asm-5.0.4.jar:/Users/unclezhao/.m2/repository/com/esotericsoftware/minlog/1.3.0/minlog-1.3.0.jar:/Users/unclezhao/.m2/repository/org/objenesis/objenesis/2.5.1/objenesis-2.5.1.jar:/Users/unclezhao/.m2/repository/de/javakaffee/kryo-serializers/0.42/kryo-serializers-0.42.jar:/Users/unclezhao/.m2/repository/de/ruedigermoeller/fst/2.48-jdk-6/fst-2.48-jdk-6.jar:/Users/unclezhao/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.10.2/jackson-core-2.10.2.jar:/Users/unclezhao/.m2/repository/com/cedarsoftware/java-util/1.9.0/java-util-1.9.0.jar:/Users/unclezhao/.m2/repository/com/cedarsoftware/json-io/2.5.1/json-io-2.5.1.jar:/Users/unclezhao/.m2/repository/com/alibaba/dubbo-serialization-api/2.6.8/dubbo-serialization-api-2.6.8.jar:/Users/unclezhao/.m2/repository/io/netty/netty-all/4.1.45.Final/netty-all-4.1.45.Final.jar:/Users/unclezhao/.m2/repository/io/protostuff/protostuff-core/1.7.2/protostuff-core-1.7.2.jar:/Users/unclezhao/.m2/repository/io/protostuff/protostuff-api/1.7.2/protostuff-api-1.7.2.jar:/Users/unclezhao/.m2/repository/io/protostuff/protostuff-runtime/1.7.2/protostuff-runtime-1.7.2.jar:/Users/unclezhao/.m2/repository/io/protostuff/protostuff-collectionschema/1.7.2/protostuff-collectionschema-1.7.2.jar:/Users/unclezhao/Raymond/work/workspaces/stdspaces/architecture-std/dubbo-serialization-protostuff/target/classes:/Users/unclezhao/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.8.6/jackson-core-2.8.6.jar:/Users/unclezhao/Raymond/work/workspaces/stdspaces/architecture-std/dubbo-spi-api/target/classes:/Users/unclezhao/.m2/repository/io/netty/netty-all/4.1.25.Final/netty-all-4.1.25.Final.jar me.zy.std.dubbo.spi.consumer.EchoConsumer
[24/06/20 11:44:50:050 CST] main  INFO logger.LoggerFactory: using logger: com.alibaba.dubbo.common.logger.log4j.Log4jLoggerAdapter
[24/06/20 11:44:50:050 CST] main ERROR common.Version:  [DUBBO] Duplicate class com/alibaba/dubbo/common/Version.class in 2 jar [file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo-common/2.6.8/dubbo-common-2.6.8.jar!/com/alibaba/dubbo/common/Version.class, file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo/2.6.8/dubbo-2.6.8.jar!/com/alibaba/dubbo/common/Version.class], dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:50:050 CST] main  WARN extension.SpringExtensionFactory:  [DUBBO] No spring extension (bean) named:defaultCompiler, try to find an extension (bean) of type java.lang.String, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:50:050 CST] main  WARN extension.SpringExtensionFactory:  [DUBBO] No spring extension (bean) named:defaultCompiler, type:java.lang.String found, stop get bean., dubbo version: 2.6.8, current host: 192.168.0.108
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
[24/06/20 11:44:50:050 CST] main  INFO server.Server:  [DUBBO] qos-server bind localhost:33332, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:50:050 CST] main  INFO zookeeper.ZookeeperRegistry:  [DUBBO] Load registry store file /Users/unclezhao/.dubbo/dubbo-registry-echo-service-127.0.0.1:2181.cache, data: {me.zy.std.dubbo.spi.api.EchoService=empty://192.168.0.108/me.zy.std.dubbo.spi.api.EchoService?application=echo-service&category=configurators&dubbo=2.0.2&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=56089&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&side=consumer&timestamp=1593012543817 empty://192.168.0.108/me.zy.std.dubbo.spi.api.EchoService?application=echo-service&category=routers&dubbo=2.0.2&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=56089&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&side=consumer&timestamp=1593012543817 dubbo://192.168.0.108:20880/me.zy.std.dubbo.spi.api.EchoService?anyhost=true&application=echo-provider&dubbo=2.0.2&generic=false&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=56070&serialization=protostuff&side=provider&timestamp=1593012533112}, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:50:050 CST] main  INFO zookeeper.ZookeeperRegistry:  [DUBBO] Register: consumer://192.168.0.108/me.zy.std.dubbo.spi.api.EchoService?application=echo-service&category=consumers&check=false&dubbo=2.0.2&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=57176&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&side=consumer&timestamp=1593013490461, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:50:050 CST] main  INFO zookeeper.ZookeeperRegistry:  [DUBBO] Subscribe: consumer://192.168.0.108/me.zy.std.dubbo.spi.api.EchoService?application=echo-service&category=providers,configurators,routers&dubbo=2.0.2&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=57176&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&side=consumer&timestamp=1593013490461, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:50:050 CST] main  INFO zookeeper.ZookeeperRegistry:  [DUBBO] Notify urls for subscribe url consumer://192.168.0.108/me.zy.std.dubbo.spi.api.EchoService?application=echo-service&category=providers,configurators,routers&dubbo=2.0.2&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=57176&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&side=consumer&timestamp=1593013490461, urls: [dubbo://192.168.0.108:20880/me.zy.std.dubbo.spi.api.EchoService?anyhost=true&application=echo-provider&dubbo=2.0.2&generic=false&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=57093&serialization=protostuff&side=provider&timestamp=1593013422447, empty://192.168.0.108/me.zy.std.dubbo.spi.api.EchoService?application=echo-service&category=configurators&dubbo=2.0.2&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=57176&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&side=consumer&timestamp=1593013490461, empty://192.168.0.108/me.zy.std.dubbo.spi.api.EchoService?application=echo-service&category=routers&dubbo=2.0.2&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=57176&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&side=consumer&timestamp=1593013490461], dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:50:050 CST] main ERROR common.Version:  [DUBBO] Duplicate class com/alibaba/dubbo/remoting/exchange/Exchangers.class in 2 jar [file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo/2.6.8/dubbo-2.6.8.jar!/com/alibaba/dubbo/remoting/exchange/Exchangers.class, file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo-remoting-api/2.6.8/dubbo-remoting-api-2.6.8.jar!/com/alibaba/dubbo/remoting/exchange/Exchangers.class], dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:50:050 CST] main ERROR common.Version:  [DUBBO] Duplicate class com/alibaba/dubbo/remoting/Transporters.class in 2 jar [file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo-remoting-api/2.6.8/dubbo-remoting-api-2.6.8.jar!/com/alibaba/dubbo/remoting/Transporters.class, file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo/2.6.8/dubbo-2.6.8.jar!/com/alibaba/dubbo/remoting/Transporters.class], dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:50:050 CST] main ERROR common.Version:  [DUBBO] Duplicate class com/alibaba/dubbo/remoting/RemotingException.class in 2 jar [file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo/2.6.8/dubbo-2.6.8.jar!/com/alibaba/dubbo/remoting/RemotingException.class, file:/Users/unclezhao/.m2/repository/com/alibaba/dubbo-remoting-api/2.6.8/dubbo-remoting-api-2.6.8.jar!/com/alibaba/dubbo/remoting/RemotingException.class], dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] main  INFO transport.AbstractClient:  [DUBBO] Successed connect to server /192.168.0.108:20880 from NettyClient 192.168.0.108 using dubbo version 2.6.8, channel is NettyChannel [channel=[id: 0x8a287b71, L:/192.168.0.108:58449 - R:/192.168.0.108:20880]], dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] main  INFO transport.AbstractClient:  [DUBBO] Start NettyClient /192.168.0.108 connect to the server /192.168.0.108:20880, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] main  INFO config.AbstractConfig:  [DUBBO] Refer dubbo service me.zy.std.dubbo.spi.api.EchoService from url zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?anyhost=true&application=echo-service&check=false&dubbo=2.0.2&generic=false&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=57176&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&register.ip=192.168.0.108&remote.timestamp=1593013422447&serialization=protostuff&side=consumer&timestamp=1593013490461, dubbo version: 2.6.8, current host: 192.168.0.108
protostuff serialization initialized
dubbo
[24/06/20 11:44:51:051 CST] DubboShutdownHook  INFO config.DubboShutdownHook:  [DUBBO] Run shutdown hook now., dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] DubboShutdownHook  INFO support.AbstractRegistryFactory:  [DUBBO] Close all registries [zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=echo-service&dubbo=2.0.2&interface=com.alibaba.dubbo.registry.RegistryService&pid=57176&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&timestamp=1593013490516], dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] DubboShutdownHook  INFO zookeeper.ZookeeperRegistry:  [DUBBO] Destroy registry:zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=echo-service&dubbo=2.0.2&interface=com.alibaba.dubbo.registry.RegistryService&pid=57176&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&timestamp=1593013490516, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] DubboShutdownHook  INFO zookeeper.ZookeeperRegistry:  [DUBBO] Unregister: consumer://192.168.0.108/me.zy.std.dubbo.spi.api.EchoService?application=echo-service&category=consumers&check=false&dubbo=2.0.2&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=57176&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&side=consumer&timestamp=1593013490461, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] DubboShutdownHook  INFO zookeeper.ZookeeperRegistry:  [DUBBO] Destroy unregister url consumer://192.168.0.108/me.zy.std.dubbo.spi.api.EchoService?application=echo-service&category=consumers&check=false&dubbo=2.0.2&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=57176&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&side=consumer&timestamp=1593013490461, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] DubboShutdownHook  INFO zookeeper.ZookeeperRegistry:  [DUBBO] Unsubscribe: consumer://192.168.0.108/me.zy.std.dubbo.spi.api.EchoService?application=echo-service&category=providers,configurators,routers&dubbo=2.0.2&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=57176&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&side=consumer&timestamp=1593013490461, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] DubboShutdownHook  INFO zookeeper.ZookeeperRegistry:  [DUBBO] Destroy unsubscribe url consumer://192.168.0.108/me.zy.std.dubbo.spi.api.EchoService?application=echo-service&category=providers,configurators,routers&dubbo=2.0.2&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=57176&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&side=consumer&timestamp=1593013490461, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] DubboShutdownHook  INFO dubbo.DubboProtocol:  [DUBBO] Close dubbo connect: /192.168.0.108:58449-->/192.168.0.108:20880, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] DubboShutdownHook  INFO netty4.NettyChannel:  [DUBBO] Close netty channel [id: 0x8a287b71, L:/192.168.0.108:58449 - R:/192.168.0.108:20880], dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] DubboShutdownHook  INFO dubbo.DubboProtocol:  [DUBBO] Close dubbo connect: 192.168.0.108:0-->192.168.0.108:20880, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] DubboShutdownHook  INFO dubbo.DubboProtocol:  [DUBBO] Destroy reference: dubbo://192.168.0.108:20880/me.zy.std.dubbo.spi.api.EchoService?anyhost=true&application=echo-service&check=false&dubbo=2.0.2&generic=false&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=57176&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33332&register.ip=192.168.0.108&remote.timestamp=1593013422447&serialization=protostuff&side=consumer&timestamp=1593013490461, dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] DubboShutdownHook  INFO server.Server:  [DUBBO] qos-server stopped., dubbo version: 2.6.8, current host: 192.168.0.108

Process finished with exit code 0

EchoProvider的日志输出增加了:

protostuff serialization initialized
[23:44:51] Hello dubbo, request from consumer: /192.168.0.108:58449
[24/06/20 11:44:51:051 CST] NettyServerWorker-5-1  WARN transport.AbstractServer:  [DUBBO] All clients has discontected from /192.168.0.108:20880. You can graceful shutdown now., dubbo version: 2.6.8, current host: 192.168.0.108
[24/06/20 11:44:51:051 CST] DubboServerHandler-192.168.0.108:20880-thread-3  INFO dubbo.DubboProtocol:  [DUBBO] disconnected from /192.168.0.108:58449,url:dubbo://192.168.0.108:20880/me.zy.std.dubbo.spi.api.EchoService?anyhost=true&application=echo-provider&bind.ip=192.168.0.108&bind.port=20880&channel.readonly.sent=true&codec=dubbo&dubbo=2.0.2&generic=false&heartbeat=60000&interface=me.zy.std.dubbo.spi.api.EchoService&methods=echo&pid=57093&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33333&serialization=protostuff&side=provider&timestamp=1593013422447, dubbo version: 2.6.8, current host: 192.168.0.108

由此可见,我们新实现的基于protostuffDubbo序列化扩展可以正常工作了。

0x05 总结

至此,DubboSPI实现就分析完成了,同时,也针对JDK的实现和Spring的实现进行了比较,并且,还动手基于DubboSerialization扩展,实现了基于protostuff的扩展实现。

在整个分析的过程中,我们清楚了Dubbo框架的整个扩展机制的规范,了解到了SPIAdaptiveActivate三个注解的作用,通过分析服务加载器类ExtensionLoader的三个关键方法,明白了三个注解之间是如何协作来实现Dubbo扩展的四大特性的。

DubboSPI是三者(JDKSpring)中实现最复杂的,相对的,也是功能最强大,最灵活的。当然,DubboSPI机制的规范还有一些细节这里没有覆盖到,我们后续针对Dubbo源码的其它分析会覆盖到。

通过分析这些优秀的框架,我们可以受到很大的启发,比如,当我们需要实现一套框架时,就可以借鉴Dubbo的这种微内核+插件式的架构,这样可以保证框架有足够的扩展性(当然了,扩展性并不仅仅只是靠这一点来获得,合理的使用设计模式也是框架实现时保证扩展性的一种有效手段,以及贯穿面向对象设计的五大原则等等),借鉴并不是照搬,我们要吸取优秀的思想,再结合自己的需求进行裁剪和平衡做出合适的取舍。

SPI机制的分析到此就结束了,欢迎各种反馈和交流!

References
  1. Apache Dubbo
  2. Dubbo 文档
  3. Protostuff
  4. 示例代码仓库