深入理解Dubbo中的SPI(CTO看了都说好)

dubbo优秀的扩展性表现在诸如注册中心、通讯协议、序列化协议等组件的插件多样性以及二次扩展开发的便捷性上。dubbo大部分的组件都以插件的方式提供,插件式架构也被称为微内核架构。在继续研究各个功能插件的细节之前,剖析下dubbo内核扩展机制显得尤为重要。

正式开始之前假设读者群体已经了解知道了Java中SPI机制,不了解的同学请先阅读Java SPI介绍使用。虽然Java语言自带的SPI也拥有不错的扩展性,但是在以下几个方面表现就有所欠缺:

  • 启动时即加载所有扩展插件的实例对象,这对拥有大量扩展插件框架来说在使用到时再去加载显然会更加合适。(没有绝对的事情,如实应用追求运行时绝对的性能,那么懒加载可能并不是一个好选择,当然也可以通过启动预热等措施解决,这又是另外的话题了。总而言之,核心开发者经过取舍之后这就这么设计了)
  • 插件对应用环境没有感知能力,当应用程序同时拥有多个扩展插件时,原生SPI无法根据上下文环境动态适配合适的插件。
  • 另外dubbo的SPI也提供了有限的自动装配及代理功能(稍后会进行剖析)。

那么dubbo在重新实现自定义的SPI是如何做到的呢,下面我们就以ExtensionLoader类加载扩展插件为入口,一步步分析dubbo实现的具体细节。

源码中比较典型用法示例如下所示:

public static Serialization getSerialization(URL url) {
    return ExtensionLoader.getExtensionLoader(Serialization.class).getExtension(
        url.getParameter(Constants.SERIALIZATION_KEY, Constants.DEFAULT_REMOTING_SERIALIZATION));
}

第一步传入要被加载实现类的接口类,其实就是给每个类型的接口分配了单独的ExtensionLoader

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    // 省略无关代码
    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;
}

第二步传入要被加载实现类的别名,返回实现类的实例对象。

public T getExtension(String name) {
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    // 如果指定扩展类别名是"true",则加载默认的扩展类返回
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    // 获取之前缓存的对象,如果没有则创建一个新的
    final Holder<Object> holder = getOrCreateHolder(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;
}

以上代码步骤拆开来看:

  • 如果符合默认插件规则,优先返回默认对象。
  • 从缓存中获取匹配的类实例对象,如果不存在则通过createExtension加载放入缓存然后返回。

进一步剖析createExtension方法

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);
        }
        // setter注入
        injectExtension(instance);
        // 初始构造参数为当前类的wrapperClass,替换为wrapperClass的实例对象
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (CollectionUtils.isNotEmpty(wrapperClasses)) {
            for (Class<?> wrapperClass : wrapperClasses) {
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        // 如果实现Lifecycle接口,则调用初始化方法
        initExtension(instance);
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                                        type + ") couldn't be instantiated: " + t.getMessage(), t);
    }
}

以上代码主要涉及一下几个步骤:

  • 首先确保类文件已经从配置文件中加载
  • 获取已经加载过的缓存对象,如果没有则通过默认构造方法创建一个并加入缓存。
  • 遍历获取到的对象所有方法,过滤出满足条件的set方法,并剔除声明了DisableInject注解的方法。
  • 检查cachedWrapperClasses集合中所有以当前类作为构造方法参数的类,初始化并进行set注入。
  • 如果当前对象还实现了Lifecycle,则调用initi(alize()方法。

读取配置文件

dubbo中约定的配置文件存放的路径

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

dubbo按照从上到下的顺序依次读取目录下含有指定类全限定名的文件,文件内容格式如key=value,key用来表示插件在dubbo中的名称,value则表示插件类的全限定名,将类载入放入到cachedClasses缓存中。

自动注入及动态代理实现

set注入实现

着重分析injectExtension(instance);方法内容:

private T injectExtension(T instance) {
    if (objectFactory == null) {
        return instance;
    }
    for (Method method : instance.getClass().getMethods()) {
        // 方法必须以set开头、必须是public、方法参数必须是一个
        if (!isSetter(method)) {
            continue;
        }
        // 方法如果有DisableInject注解,则跳过不处理
        if (method.getAnnotation(DisableInject.class) != null) {
            continue;
        }
        // 方法参数如果基础或者包装数据类型跳过
        Class<?> pt = method.getParameterTypes()[0];
        if (ReflectUtils.isPrimitives(pt)) {
            continue;
        }

        try {
            // 截取set方法名set后的字符串
            String property = getSetterProperty(method);
            // 获取set方法依赖的对象
            Object object = objectFactory.getExtension(pt, property);
            if (object != null) {
                // 反射调用将依赖对象注入
                method.invoke(instance, object);
            }
        } catch (Exception e) {
            logger.error("Failed to inject via method " + method.getName()
                         + " of interface " + type.getName() + ": " + e.getMessage(), e);
        }

    }
    return instance;
}

大体思路就是判断对象是否为set方法,如果满足一定的条件则通过objectFactory(下面介绍,先理解为Spring中的beanFactory)获取依赖对象,在通过反射调用set方法,完成set装配的功能。

构造方法注入及代理实现

在上面set注入完成之后开始进行构造方法注入的工作:

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

cachedWrapperClasses这个对象是在加载类的时候创建的,判断扩展类是否WrapperClass逻辑如下:

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

以dubbo中RegistryFactoryZookeeperRegistryFactoryRegistryFactoryWrapper为例进一步说明两者之间关系。

public class RegistryFactoryWrapper implements RegistryFactory {
    private RegistryFactory registryFactory;

    public RegistryFactoryWrapper(RegistryFactory registryFactory) {
        this.registryFactory = registryFactory;
    }

    @Override
    public Registry getRegistry(URL url) {
        return new ListenerRegistryWrapper(registryFactory.getRegistry(url),
                Collections.unmodifiableList(ExtensionLoader.getExtensionLoader(RegistryServiceListener.class)
                        .getActivateExtension(url, "registry.listeners")));
    }
}

当我们使用zookeeper作为注册中心时,会发生以下步骤:

  1. 通过ExtensionLoader.getExtensionLoader(RegistryFactory.class)获取插件实例对象。
  2. ExtensionLoader分别读取dubbo-registry-zookeeper模块及dubbo-registry-api模块下文件名为org.apache.dubbo.registry.RegistryFactory的配置。
  3. 这里RegistryFactoryWrapper构造方法参数为RegistryFactory,所以RegistryFactoryWrapper会被加载cachedWrapperClasses缓存中。
  4. 当创建ZookeeperRegistryFactory对象之后,会遍历cachedWrapperClasses,将对象通过构造方法注入。

这么做好处显而易见,可以通过RegistryFactoryWrapper完成代理的功能。

插件自动适配实现

典型的用法示例:

RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();

可以看到与上面用法的区别在于一个是getExtension(String name)getAdaptiveExtension(),前者是显示指定需要加载的插件的名称,后者通过方法名称中"adaptive"一词的中文含义"适应性"也能推断出该方法自适应的特性。

dubbo的插件自动适配功能需要搭配注解Adaptive使用,在dubbo中该注解表现行为有两种:

  • 如果该注解加在类上dubbo不会为该类生成代理类,代理功能由被注解类自己实现(参见AdaptiveExtensionFactoryAdaptiveCompiler)。
  • 如果该注解加在方法上则dubbo会为该方法生成代理方法。

跟进getAdaptiveExtension()方法,通过双重检查锁调用createAdaptiveExtension()方法

public T getAdaptiveExtension() {
    Object instance = cachedAdaptiveInstance.get();
    if (instance == null) {
        if (createAdaptiveInstanceError != null) {
            throw new IllegalStateException("Failed to create adaptive instance: " +
                    createAdaptiveInstanceError.toString(),
                    createAdaptiveInstanceError);
        }

        synchronized (cachedAdaptiveInstance) {
            instance = cachedAdaptiveInstance.get();
            if (instance == null) {
                try {
                    // 创建自适应插件
                    instance = createAdaptiveExtension();
                    cachedAdaptiveInstance.set(instance);
                } catch (Throwable t) {
                    createAdaptiveInstanceError = t;
                    throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
                }
            }
        }
    }

    return (T) instance;
}

继续跟进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);
    }
}

核心代码拆开来看分为以下步骤:

  • 通过getAdaptiveExtensionClass获取插件类。
  • 通过调用newInstance()创建对象实例。
  • 通过调用injectExtension完成set注入功能。

继续跟进getAdaptiveExtensionClass

private Class<?> getAdaptiveExtensionClass() {
    // 加载配置文件中的类
    getExtensionClasses();
    // 在加载类过程中,如果插件类被注解Adaptive则被赋值给当前对象。
    if (cachedAdaptiveClass != null) {
        // 返回之后直接通过createAdaptiveExtension方法中的newInstance完成对象实例的创建
        return cachedAdaptiveClass;
    }
    // 如果Adaptive注解在方法上走这个分支逻辑
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

注解在类上

通过getAdaptiveExtensionClass可知如果类被Adaptive注解标注,则直接返回该类完成对象实例创建过程。应用到代码中AdaptiveExtensionFactoryAdaptiveCompiler这两个类直接调用默认的无参构造方法即为完成了初始化的过程。

注解在方法上

继续跟进createAdaptiveExtensionClass方法:

private Class<?>  createAdaptiveExtensionClass() {
    // 动态生成类的文本内容
    String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
    // 将文本编译为类对象
    ClassLoader classLoader = findClassLoader();
    org.apache.dubbo.common.compiler.Compiler compiler =                   ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    return compiler.compile(code, classLoader);
}

核心内容在于动态生成类,在展开分析具体过程之前,先来看个简单的示例,现有如下用法:

RouterFactory routerFactory = ExtensionLoader.getExtensionLoader(RouterFactory.class).getAdaptiveExtension();

其中RouterFactory内容如下:

@SPI
public interface RouterFactory {

    /**
     * Create router.
     * Since 2.7.0, most of the time, we will not use @Adaptive feature, so it's kept only for compatibility.
     *
     * @param url url
     * @return router instance
     */
    @Adaptive("protocol")
    Router getRouter(URL url);
}

生成的代理内容简化如下:

public class RouterFactory$Adaptive implements RouterFactory {
    public Router getRouter(URL arg0) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        URL url = arg0;
        String extName = url.getProtocol();
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (RouterFactory) name from url (" + url.toString() + ") use keys([protocol])");
        RouterFactory extension = (RouterFactory) ExtensionLoader.getExtensionLoader(RouterFactory.class).getExtension(extName);
        return extension.getRouter(arg0);
    }
}

可以初步了解到代理类,实现了RouterFactory中的getRouter方法。@Adaptive("protocol")中的"protocol"属性被解析成url.getProtocol()方法的一部分,通过这种方式动态获取应用配置的插件类型,继而通过getExtension(extName)获取真正的插件实现类。

下面开始继续分析生成代理类的过程:

public String generate() {
    // no need to generate adaptive class since there's no adaptive method found.
    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());
    // import ExtensionLoader
    code.append(generateImports());
    // 生成类签名,形如 public class %s$Adaptive implements %s {
    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();
}

一行行分析RouterFactory$Adaptive过程。

generatePackageInfo()获取RouterFactory包名,拼接之后结果为package org.apache.dubbo.rpc.cluster

generateClassDeclaration()获取ExtensionLoader类型,拼接之后结果为import org.apache.dubbo.common.extension. ExtensionLoader

generateClassDeclaration(),拼接之后结果为public class RouterFactory$Adaptive implements org.apache.dubbo.rpc.cluster.RouterFactory {

继续跟进generateMethod(method),因为RouterFactory只有一个方法Router getRouter(URL url),下面分析以该方法作为示例

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);
}

method.getReturnType().getCanonicalName()结果为org.apache.dubbo.rpc.cluster.Router

method.getName()结果为getRouter

generateMethodArguments(method)结果为org.apache.dubbo.common.URL arg0

generateMethodThrows(method),因为方法没有抛出异常,所以这里是空的。

继续跟进generateMethodContent方法

private String generateMethodContent(Method method) {
    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    StringBuilder code = new StringBuilder(512);
    if (adaptiveAnnotation == null) {
        return generateUnsupported(method);
    } else {
        // 找到方法参数为URL是第几个参数,这里显示是0
        int urlTypeIndex = getUrlTypeIndex(method);

        // 如果找到了URL参数
        if (urlTypeIndex != -1) {
            // Null Point check
            // 生成 if (arg0 == null) throw new IllegalArgumentException("url == null"); org.apache.dubbo.common.URL url = arg0;
            code.append(generateUrlNullCheck(urlTypeIndex));
        } else {
            // did not find parameter in URL type
            // 如果没有找到URL参数,则循环该方法参数的所有方法是有满足以下条件的方法:
            // 以get开头、public修饰符、非静态、入参为0、返回值类型为URL的
            code.append(generateUrlAssignmentIndirectly(method));
        }
        // 返回"protocol"
        String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
        // 是否含有Invocation参数
        boolean hasInvocation = hasInvocationArgument(method);
         // 这里getRouter(URL url)没有Invocation参数,所有返回值是空
        // if (arg%d == null) throw new IllegalArgumentException(\"invocation == null\"); String methodName = arg%d.getMethodName();
        code.append(generateInvocationArgumentNullCheck(method));
        // 这里分支比较多,最后会生成 String extName = url.getProtocol();
        code.append(generateExtNameAssignment(value, hasInvocation));
        // 非空校验 if (extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.cluster.RouterFactory) name from url (" + url.toString() + ") use keys([protocol])");
        code.append(generateExtNameNullCheck(value));
        // 生成获取插件对象代码 org.apache.dubbo.rpc.cluster.RouterFactory extension = (org.apache.dubbo.rpc.cluster.RouterFactory) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.cluster.RouterFactory.class).getExtension(extName);
        code.append(generateExtensionAssignment());

        // 生成调用代理对象getRouter方法 return extension.getRouter(arg0);
        code.append(generateReturnAndInvocation(method));
    }

    return code.toString();
}

导致完整代理类内容就生成了:

package org.apache.dubbo.rpc.cluster
import org.apache.dubbo.common.extension. ExtensionLoader
    
public class RouterFactory$Adaptive implements org.apache.dubbo.rpc.cluster.RouterFactory {
    public org.apache.dubbo.rpc.cluster.Router getRouter(org.apache.dubbo.common.URL arg0) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg0;
        String extName = url.getProtocol();
        if (extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.cluster.RouterFactory) name from url (" + url.toString() + ") use keys([protocol])");
        org.apache.dubbo.rpc.cluster.RouterFactory extension = (org.apache.dubbo.rpc.cluster.RouterFactory) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.cluster.RouterFactory.class).getExtension(extName);
        return extension.getRouter(arg0);
    }
}

这部分代码比较琐碎,毕竟要编码完成生成代码以及编译等工作,可以作为设计的一种思路,生成的代码的源码部分实在是没有仔细研读的必要。

通过本篇部分,完成了对dubbo自定义SPI实现原理及源码的解析,希望可以给大家带来一点收获。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容