资讯专栏INFORMATION COLUMN

dubbo之SPI自适应扩展机制

vvpale / 3092人阅读

摘要:对于这个矛盾的问题,通过自适应拓展机制很好的解决了。自适应拓展机制的实现逻辑比较复杂,首先会为拓展接口生成具有代理功能的代码。

1、背景

在 Dubbo 中,很多拓展都是通过 SPI 机制进行加载的,比如 Protocol、Cluster、LoadBalance 等。有时,有些拓展并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。这听起来有些矛盾。拓展未被加载,那么拓展方法就无法被调用(静态方法除外)。拓展方法未被调用,拓展就无法被加载。对于这个矛盾的问题,Dubbo 通过自适应拓展机制很好的解决了。自适应拓展机制的实现逻辑比较复杂,首先 Dubbo 会为拓展接口生成具有代理功能的代码。然后通过 javassist 或 jdk 编译这段代码,得到 Class 类。最后再通过反射创建代理类,整个过程比较复杂。

2、原理

为了很好的理解,下面结合实例进行分析,在Dubbbo暴露服务中,ServiceConfig类中doExportUrlsFor1Protocol方法中有如下这样一条语句:

Exporter exporter = protocol.export(wrapperInvoker);

接下来咱们就根据这条语句进行深入分析Dubbo SPI自适应扩展机制。

根据源码查询得知,protocol对象是通过以下语句创建:

private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

根据上篇文章,咱们得知getExtensionLoader只是获取ExtensionLoader对象,所以自适应扩展的核心在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;
    }

在getAdaptiveExtension方法中先从缓存中获取,缓存中不存在在创建实例,并存入缓存中,逻辑比较简单,咱们在来分析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);
        }
    }

createAdaptiveExtension 方法的代码比较少,但却包含了三个逻辑,分别如下:

调用 getAdaptiveExtensionClass 方法获取自适应拓展 Class 对象

通过反射进行实例化

调用 injectExtension 方法向拓展实例中注入依赖

前两个逻辑比较好理解,第三个逻辑用于向自适应拓展对象中注入依赖。这个逻辑看似多余,但有存在的必要,这里简单说明一下。前面说过,Dubbo 中有两种类型的自适应拓展,一种是手工编码的,一种是自动生成的。手工编码的自适应拓展中可能存在着一些依赖,而自动生成的 Adaptive 拓展则不会依赖其他类。这里调用 injectExtension 方法的目的是为手工编码的自适应拓展注入依赖,这一点需要大家注意一下。关于 injectExtension 方法,前文已经分析过了,这里不再赘述。接下来,分析 getAdaptiveExtensionClass 方法的逻辑。

private Class getAdaptiveExtensionClass() {
    // 通过 SPI 获取所有的拓展类
    getExtensionClasses();
    // 检查缓存,若缓存不为空,则直接返回缓存
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    // 创建自适应拓展类
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

getAdaptiveExtensionClass 方法同样包含了三个逻辑,如下:

调用 getExtensionClasses 获取所有的拓展类

检查缓存,若缓存不为空,则返回缓存

若缓存为空,则调用 createAdaptiveExtensionClass 创建自适应拓展类

这三个逻辑看起来平淡无奇,似乎没有多讲的必要。但是这些平淡无奇的代码中隐藏了着一些细节,需要说明一下。首先从第一个逻辑说起,getExtensionClasses 这个方法用于获取某个接口的所有实现类。比如该方法可以获取 Protocol 接口的 DubboProtocol、HttpProtocol、InjvmProtocol 等实现类。在获取实现类的过程中,如果某个某个实现类被 Adaptive 注解修饰了,那么该类就会被赋值给 cachedAdaptiveClass 变量。此时,上面步骤中的第二步条件成立(缓存不为空),直接返回 cachedAdaptiveClass 即可。如果所有的实现类均未被 Adaptive 注解修饰,那么执行第三步逻辑,创建自适应拓展类。相关代码如下:

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();
    // 编译代码,生成 Class
    return compiler.compile(code, classLoader);
}

createAdaptiveExtensionClass 方法用于生成自适应拓展类,该方法首先会生成自适应拓展类的源码,然后通过 Compiler 实例(Dubbo 默认使用 javassist 作为编译器)编译源码,得到代理类 Class 实例。接下来,我们把重点放在代理类代码生成的逻辑上,其他逻辑大家自行分析。

    private String createAdaptiveExtensionClassCode() {
        StringBuilder codeBuilder = new StringBuilder();
        Method[] methods = type.getMethods();
        boolean hasAdaptiveAnnotation = false;
        // 循环遍历方法
        for (Method m : methods) {
            // 判断类中的方法是否有Adaptive注解
            if (m.isAnnotationPresent(Adaptive.class)) {
                hasAdaptiveAnnotation = true;
                break;
            }
        }
        
        // 如果类中方法没有Adaptive注解,则直接抛出异常
        if (!hasAdaptiveAnnotation) {
            throw new IllegalStateException("No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!");
        }

        // 引入类的包名、类的依赖
        codeBuilder.append("package ").append(type.getPackage().getName()).append(";");
        codeBuilder.append("
import ").append(ExtensionLoader.class.getName()).append(";");
        codeBuilder.append("
public class ").append(type.getSimpleName()).append("$Adaptive").append(" implements ").append(type.getCanonicalName()).append(" {");

        for (Method method : methods) {
            // 获取方法返回类型
            Class rt = method.getReturnType();
            // 获取方法参数类型
            Class[] pts = method.getParameterTypes();
            // 获取方法抛出的异常类型
            Class[] ets = method.getExceptionTypes();

            Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
            StringBuilder code = new StringBuilder(512);
            // 方法没有Adaptive注解,方法体就是一条抛出异常的语句
            if (adaptiveAnnotation == null) {
                code.append("throw new UnsupportedOperationException("method ")
                        .append(method.toString()).append(" of interface ")
                        .append(type.getName()).append(" is not adaptive method!");");
            } else {
                // 标记方法中参数类型为URL的是第几个参数
                int urlTypeIndex = -1;
                for (int i = 0; i < pts.length; ++i) {
                    if (pts[i].equals(URL.class)) {
                        urlTypeIndex = i;
                        break;
                    }
                }
                
                // 如果方法参数中有URL类型的,则需要校验参数是否为空
                if (urlTypeIndex != -1) {
                
                    String s = String.format("
if (arg%d == null) throw new IllegalArgumentException("url == null");",
                            urlTypeIndex);
                    code.append(s);

                    // 并创建一个URL变量,将参数赋值给变量:URL url = url
                    s = String.format("
%s url = arg%d;", URL.class.getName(), urlTypeIndex);
                    code.append(s);
                }
                else {
                    // 参数中没有URL类型的参数
                    String attribMethod = null;
                    
                    // 查找参数所属类中是否有返回URL的方法,如:Invoker.getUrl()方法,则attribMethod = getUrl()
                    LBL_PTS:
                    for (int i = 0; i < pts.length; ++i) {
                        Method[] ms = pts[i].getMethods();
                        for (Method m : ms) {
                            String name = m.getName();
                            if ((name.startsWith("get") || name.length() > 3)
                                    && Modifier.isPublic(m.getModifiers())
                                    && !Modifier.isStatic(m.getModifiers())
                                    && m.getParameterTypes().length == 0
                                    && m.getReturnType() == URL.class) {
                                urlTypeIndex = i;
                                attribMethod = name;
                                break LBL_PTS;
                            }
                        }
                    }
                    // 如果参数中没有返回URL的方法,则抛出异常
                    if (attribMethod == null) {
                        throw new IllegalStateException("fail to create adaptive class for interface " + type.getName()
                                + ": not found url parameter or url attribute in parameters of method " + method.getName());
                    }
                    
                    // 校验有返回URL类型方法的参数是否为空
                    String s = String.format("
if (arg%d == null) throw new IllegalArgumentException("%s argument == null");",
                            urlTypeIndex, pts[urlTypeIndex].getName());
                    code.append(s);
                    // 校验参数调用返回URL方法返回值是否为空,如:if Invoker.getUrl() == null
                    s = String.format("
if (arg%d.%s() == null) throw new IllegalArgumentException("%s argument %s() == null");",
                            urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod);
                    code.append(s);

                    // 创建URL变量,赋值为返回值,如 URL url = Invoker.getUrl()
                    s = String.format("%s url = arg%d.%s();", URL.class.getName(), urlTypeIndex, attribMethod);
                    code.append(s);
                }

                String[] value = adaptiveAnnotation.value();
                if (value.length == 0) {
                    String splitName = StringUtils.camelToSplitName(type.getSimpleName(), ".");
                    value = new String[]{splitName};
                }

                // 判断方法参数中是否有Invocation类型的参数
                boolean hasInvocation = false;
                for (int i = 0; i < pts.length; ++i) {
                    if (("org.apache.dubbo.rpc.Invocation").equals(pts[i].getName())) {
                        // Null Point check
                        String s = String.format("
if (arg%d == null) throw new IllegalArgumentException("invocation == null");", i);
                        code.append(s);
                        s = String.format("
String methodName = arg%d.getMethodName();", i);
                        code.append(s);
                        hasInvocation = true;
                        break;
                    }
                }

                String defaultExtName = cachedDefaultName;
                String getNameCode = null;
                // 如果Adaptive注解时Protocol,则根据参数url所属协议来适配加载,如dubbo:// 则调用返回DubboProtocol扩展类实例
                for (int i = value.length - 1; i >= 0; --i) {
                    if (i == value.length - 1) {
                        if (null != defaultExtName) {
                            if (!"protocol".equals(value[i])) {
                                if (hasInvocation) {
                                    getNameCode = String.format("url.getMethodParameter(methodName, "%s", "%s")", value[i], defaultExtName);
                                } else {
                                    getNameCode = String.format("url.getParameter("%s", "%s")", value[i], defaultExtName);
                                }
                            } else {
                                getNameCode = String.format("( url.getProtocol() == null ? "%s" : url.getProtocol() )", defaultExtName);
                            }
                        } else {
                            if (!"protocol".equals(value[i])) {
                                if (hasInvocation) {
                                    getNameCode = String.format("url.getMethodParameter(methodName, "%s", "%s")", value[i], defaultExtName);
                                } else {
                                    getNameCode = String.format("url.getParameter("%s")", value[i]);
                                }
                            } else {
                                getNameCode = "url.getProtocol()";
                            }
                        }
                    } else {
                        if (!"protocol".equals(value[i])) {
                            if (hasInvocation) {
                                getNameCode = String.format("url.getMethodParameter(methodName, "%s", "%s")", value[i], defaultExtName);
                            } else {
                                getNameCode = String.format("url.getParameter("%s", %s)", value[i], getNameCode);
                            }
                        } else {
                            getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
                        }
                    }
                }
                code.append("
String extName = ").append(getNameCode).append(";");
                
                String s = String.format("
if(extName == null) " +
                                "throw new IllegalStateException("Fail to get extension(%s) name from url(" + url.toString() + ") use keys(%s)");",
                        type.getName(), Arrays.toString(value));
                code.append(s);

                // 根据上面获取的type调用ExtensionLoader中的getExtension(type)方法
                s = String.format("
%s extension = (% 0) {
                    codeBuilder.append(", ");
                }
                codeBuilder.append(pts[i].getCanonicalName());
                codeBuilder.append(" ");
                codeBuilder.append("arg").append(i);
            }
            codeBuilder.append(")");
            if (ets.length > 0) {
                codeBuilder.append(" throws ");
                for (int i = 0; i < ets.length; i++) {
                    if (i > 0) {
                        codeBuilder.append(", ");
                    }
                    codeBuilder.append(ets[i].getCanonicalName());
                }
            }
            codeBuilder.append(" {");
            codeBuilder.append(code.toString());
            codeBuilder.append("
}");
        }
        codeBuilder.append("
}");
        if (logger.isDebugEnabled()) {
            logger.debug(codeBuilder.toString());
        }
        return codeBuilder.toString();
    }

createAdaptiveExtensionClassCode方法比较复杂,要静下心来慢慢研究,最好结合实例进行分析,例子protocol.export()自适应扩展类如下所示:
类名是Protocol$Adaptive:

/*
 * Decompiled with CFR 0_132.
 */
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$Adpative
implements Protocol {
    @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!");
    }

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

在调用方法时,根据方法参数进行动态自适应扩展,加载扩展类。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73238.html

相关文章

  • 聊聊Dubbo - Dubbo扩展机制实战

    摘要:今天我想聊聊的另一个很棒的特性就是它的可扩展性。的扩展机制在的官网上,描述自己是一个高性能的框架。接下来的章节中我们会慢慢揭开扩展机制的神秘面纱。扩展扩展点的实现类。的定义在配置文件中可以看到文件中定义了个的扩展实现。 摘要: 在Dubbo的官网上,Dubbo描述自己是一个高性能的RPC框架。今天我想聊聊Dubbo的另一个很棒的特性, 就是它的可扩展性。 Dubbo的扩展机制 在Dub...

    techstay 评论0 收藏0
  • 聊聊Dubbo - Dubbo扩展机制源码解析

    摘要:什么是类那什么样类的才是扩展机制中的类呢类是一个有复制构造函数的类,也是典型的装饰者模式。代码如下有一个参数是的复制构造函数有一个构造函数,参数是扩展点,所以它是一个扩展机制中的类。 摘要: 在Dubbo可扩展机制实战中,我们了解了Dubbo扩展机制的一些概念,初探了Dubbo中LoadBalance的实现,并自己实现了一个LoadBalance。是不是觉得Dubbo的扩展机制很不错呀...

    lmxdawn 评论0 收藏0
  • Dubbo SPI机制和IOC

    摘要:要构建自适应实例,先要有自适应的实现类,实现类有两种方式一种通过配置文件,一种是通过是字节码的方式动态生成。 SPI机制 SPI,即(service provider interface)机制,有很多组件的实现,如日志、数据库访问等都是采用这样的方式,一般通用组件为了提升可扩展性,基于接口编程,将操作接口形成标准规范,但是可以开放多种扩展实现,这种做法也符合开闭设计原则,使组件具有可插...

    Scorpion 评论0 收藏0
  • dubboSPI

    摘要:简介全称为,是一种服务发现机制。的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。不过,并未使用原生的机制,而是对其进行了增强,使其能够更好的满足需求。并未使用,而是重新实现了一套功能更强的机制。 1、SPI简介 SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中...

    UnixAgain 评论0 收藏0
  • dubbo源码解析(二)Dubbo扩展机制SPI

    摘要:二注解该注解为了保证在内部调用具体实现的时候不是硬编码来指定引用哪个实现,也就是为了适配一个接口的多种实现,这样做符合模块接口设计的可插拔原则,也增加了整个框架的灵活性,该注解也实现了扩展点自动装配的特性。 Dubbo扩展机制SPI 前一篇文章《dubbo源码解析(一)Hello,Dubbo》是对dubbo整个项目大体的介绍,而从这篇文章开始,我将会从源码来解读dubbo再各个模块的实...

    DirtyMind 评论0 收藏0

发表评论

0条评论

vvpale

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<