dubbo源码分析之SPI 自适应拓展(非dubbo spi)


  • 源码版本

  • 需求

  • 原理

  • 源码分析

  • 总结


源码版本

2.7.9-SNAPSHOT

需求

假设要你实现这样一种需求你能实现吗?有些拓展并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。简单理解就是 有一个接口

public interface Protocol {
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
}

这个方法 要通过 他的参数 Invoker<?> originInvoker,去动态的指定他的实现类 这里就有一个类似死锁的概念: 接口方法要被调用就需要加载接口的实现类(扩展),而接口的实现类初始化又需要接口的方法被调用才加载(扩展)。想想要你,你能实现这个功能嘛?

解释:为什么不加载后再获取代理对象呢?本质也是为了性能。可见开源框架对性能的追求

原理

dubbo的自适应拓展机制就解决了上面的问题。自适应扩展机制的实现特别复杂,需要大家耐心看完,看完后你才会发现源码之美。

首先说一些核心实现思路:这里首先会通过反射获取接口的方法,然后通过接口的方法拿到类名,如果此时实现类在类似Spring容器中会比较简单,直接通过类名拿出来即可,可惜实现的需求是在使用的时候才加载扩展,所以需要拿到了类名后通过自己实现去拼接整个java类的源代码,然后通过编译器动态编译出来。

源码分析

首先源码分析的入口是方法getAdaptiveExtension()

这里我们就从Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();这行代码开始看吧

首先我们从核心方法分析吧

  • getAdaptiveExtension
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()

所以下面我们就重点分析 方法

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

上面代码保护三个逻辑

  1. 通过getAdaptiveExtensionClass()方法获取class
  2. 反射初始化实例
  3. 调用injectExtension()方法向扩展实例注入依赖。

Dubbo 中有两种类型的自适应拓展,一种是手工编码的,一种是自动生成的。手工编码的自适应拓展中可能存在着一些依赖,而自动生成的 Adaptive 拓展则不会依赖其他类。这里调用 injectExtension 方法的目的是为手工编码的自适应拓展注入依赖

下面我们来分析方法

  • getAdaptiveExtensionClass()
private Class<?> getAdaptiveExtensionClass() {
  // 通过 SPI 获取所有的拓展类
        getExtensionClasses();
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

getAdaptiveExtensionClass()也保护三个逻辑:

  1. 调用 getExtensionClasses 获取所有的拓展类
  2. 检查缓存,若缓存不为空,则返回缓存
  3. 若缓存为空,则调用 createAdaptiveExtensionClass 创建自适应拓展类

这里需要注意getExtensionClasses()获取所有实现类,如果实现类中有类包含注解@Adaptive 则将该类放入cachedAdaptiveClass中,然后直接返回了,不再创建自适应扩展类

具体代码:

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

这里我们还是回归主线看方法

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

这段代码就特别有意思。首先会构建自适应拓展代码的源代码即 class,然后找到类加载器,通过编译器编译源码获取到class

这里我们重点关注方法

  • generate()
public String generate() {
        // no need to generate adaptive class since there's no adaptive method found.
        // 若所有的方法上均无 Adaptive 注解,则抛出异常
        if (!hasAdaptiveMethod()) {
            throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
        }

        StringBuilder code = new StringBuilder();
        // 生成 package + type 所在包
        // 类似 package org.apache.dubbo.rpc;
        code.append(generatePackageInfo());
        // 生成 import 代码:import + ExtensionLoader 全限定名
        // 类似 import org.apache.dubbo.common.extension.ExtensionLoader;
        code.append(generateImports());
        // 生成类代码:public class + type简单名称 + $Adaptive + implements + type全限定名 + {
  // 类似 public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
        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();
    }

这里我们再分析下

  • generateMethod(method)方法
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);
    }

可以看到就是简单的字符串拼接。generateMethodContent(method) 方法就是 测方法是否有注解Adaptive,没有注解直接抛出类似下面的异常dubbo源码分析之SPI 自适应拓展(非dubbo spi)其次就是 获取URL数据,为其生成判空和赋值代码。以 Protocol 的 refer 和 export 方法为例,上面的代码为它们生成如下内容(代码已格式化):

public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
 if (arg1 == nullthrow new IllegalArgumentException("url == null");
 org.apache.dubbo.common.URL url = arg1;
 String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
 if(extName == nullthrow new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
 org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
 return extension.refer(arg0, arg1);
}
dubbo源码分析之SPI 自适应拓展(非dubbo spi)
在这里插入图片描述

可以看看源码

  • generateMethodContent(method)
private String generateMethodContent(Method method) {
        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
        StringBuilder code = new StringBuilder(512);
        if (adaptiveAnnotation == null) {
            return generateUnsupported(method);
        } else {
            // 遍历参数列表,确定 URL 参数位置
            int urlTypeIndex = getUrlTypeIndex(method);

            // found parameter in URL type
            // urlTypeIndex != -1,表示参数列表中存在 URL 参数
            if (urlTypeIndex != -1) {
                // Null Point check
                code.append(generateUrlNullCheck(urlTypeIndex));
            } else {
                // did not find parameter in URL type
                code.append(generateUrlAssignmentIndirectly(method));
            }

            String[] value = getMethodAdaptiveValue(adaptiveAnnotation);

            boolean hasInvocation = hasInvocationArgument(method);

            code.append(generateInvocationArgumentNullCheck(method));
            // 生成代码:
            // String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol()); 或
            // String extName = url.getMethodParameter(methodName, "loadbalance", "random"); 或
            // String extName = url.getParameter("client", url.getParameter("transporter", "netty")); 或其他
            code.append(generateExtNameAssignment(value, hasInvocation));
            // check extName == null?
            code.append(generateExtNameNullCheck(value));
            // 生成拓展获取代码,格式如下:
            // type全限定名 extension = (type全限定名)ExtensionLoader全限定名
            //     .getExtensionLoader(type全限定名.class).getExtension(extName);
            // Tips: 格式化字符串中的 %<s 表示使用前一个转换符所描述的参数,即 type 全限定名
            code.append(generateExtensionAssignment());

            // return statement  如果方法返回值类型非 void,则生成 return 语句。
            // 生成目标方法调用逻辑   extension.方法名(arg0, arg2, ..., argN);
            code.append(generateReturnAndInvocation(method));
        }

        return code.toString();
    }

以 Protocol 接口 debug 看看这段代码生成的代码 首先是 Method 里面的东西dubbo源码分析之SPI 自适应拓展(非dubbo spi)

然后就是方法返回值(代码已格式化)

if (arg1 == nullthrow new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg1;
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == nullthrow new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.refer(arg0, arg1);

总结

自此dubbo自适应spi源码就分析完了。整体来说核心就是通过参数获取class Name,然后就是自己通过类名,拼接一个java类出来,然后再调用类加载器和编译器动态编译出class来。以前没看过源码这种操作是敢都不敢想的,算是拓展视野吧。如果以后有面试官问你dubbo SPI 自适应拓展源码,你就可以吊打他了

后续如果需要流程图可以留言说明,后续需要就补上。虽然这次画了流程图,但我觉得并不是很详细,所以就不贴出来了。由于本人技术有限,如果上面有什么错误请及时指出。


原文始发于微信公众号(小奏技术):dubbo源码分析之SPI 自适应拓展(非dubbo spi)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/30468.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!