Apache Dubbo 系列之注解 @Adaptive 实现原理

1,932 阅读7分钟

🌟 欢迎关注个人技术公众号:野区杰西

前言

在上篇文章 中,我分析了 Dubbo SPI 的源码,同时也在文章埋了一些点,专门是为了希望在后面学习 Dubbo 的时候如果需要完善的话继续去完善它的知识。今天我主要想讲的一个知识是 注解 @Adaptive 实现原理。这个注解有什么特别的呢?它这个注解其实也是扩展内容中的一部分,是 Dubbo 利用 SPI 的机制用于动态的加载扩展类,在运行时动态的时候通过一种机制来进行动态的匹配扩展类,然后通过扩展类提供服务

@Adaptive

@Adaptive 是一种扩展机制。我们可以将这个注解作用于对象或方法上面。如果方法上有 @Adaptive,如果是在方法上 Dubbo 会为这个接口生成一个子类,其他没标注的方法则默认抛出异常,我们可以看 Protocol 接口的描述; 如果 @Adaptive 作用于类上,Dubbo 会直接选择这个类为适配器的实现类,我们可以 AdaptiveCompilerAdaptiveExtensionFactory

举个例子,我们来看 Dubbo 服务暴露的主要配置类 ServiceConfig。我们可以看到它的成员变量 protocol 其实是由 ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension() 进行赋值的。从方法名字 getAdaptiveExtension 的字面意思上看,其实我们仅仅是知道这个是获取一个适配插件。但是里面究竟做了写什么?它是怎么获取到适配类的呢?我们重新从源码出发!我快速重温一下它调用的源码:

  1. 调用 getExtensionLoader 加载 ProtocolExtensionLoader
  2. 调用 getAdaptiveExtension 开始加载适配器
    • 从 cachedAdaptiveInstance 缓存获取对应的适配器
      • 如果没有,调用 createAdaptiveExtension 生成 Adaptive
        • 继续调用 getAdaptiveExtensionClass 生成 Adaptive
          • 调用 getExtensionClasses 开始加载所有插件(包括 cachedAdaptiveClass
            • 如果 cachedAdaptiveClass 也没有加载到 Adaptive
              • 调用 createAdaptiveExtensionClass 调用编译器生成一个代理适配器类
                • 调用 createAdaptiveExtensionClassCode 方法声称对应的类的代码字符串
                • 通过 Compiler 来进行编译生成
            • 如果有,就直接返回
      • 如果有,返回 Adaptive 的实例对象

上面的步骤其实很清晰,createAdaptiveExtensionClassCode 方法就是我想讲的核心方法。但是在讲这个方法之前,我想说说我对 Dubbo 要做这件事情的一个思考,如有错误,欢迎大家砸砖更正

思想

这个方法主要是服务于被 @Adaptive 注解的方法。Dubbo 会扫描到,然后帮其生成一个代理方法。可能你会发出疑问:ExtensionLoader 不是已经提供了很多方法来支持扩展了吗?例如说 getExtensionClass(String name)getExtension(String name) 等。但是有没有发现,其实这样的灵活度还是不够高,我们只能按照 Dubbo 的这几个方法来调用扩展,我们无法有一些自己自定义的扩展方法。这样显得不够灵活!

那么我们怎么可以利用 DubboSPI 来进一步扩展呢?其实 Dubbo 是这么干的。URL (RFC1738――Uniform Resource Locators) 在 Dubbo 中是非常重要的东西。因为 Dubbo 中无论是服务消费方,或者服务提供方,或者注册中心。都是通过 URL 进行定位资源的。然后 Dubbo 就是通过 URL 来作为扩展的点。例如说,在 URL 定义一个属性,可以通过 URL 的属性来选择在 Dubbo 究竟用那个实现类来处理。这听起来就很简单了,但是实践出真知!我们上一个栗子 show 一下吧。

首先我们的业务场景是,有一个用户登录的接口,它有两个对应不同实现登录的子类。那么我们分别进行实现吧。

UserService.java

@SPI("normal")
public interface UserService {
    @Adaptive
    public void login();
}

然后实现常规用户登录 NormalUserServiceImpl.java

public class NormalUserServiceImpl implements UserService{
    @Override
    public void login() {
        System.out.println("正常用户登录");
    }
}

然后实现 VIP 用户登录 VipUserServiceImpl.java

public class VipUserServiceImpl implements UserService{
    @Override
    public void login() {
        System.out.println("VIP 用户登录");
    }
}

因为我们需要被 Dubbo 的扩展机制检测到,所以我们在 resources 目录新建一个文件夹 META-INF/dubbo,然后新建一个 File,命名是以 UserService 的包路径。我这里是 com.dubbo.demo.provider.Demo,所以我的文件命名为 com.dubbo.demo.provider.Demo.UserService。然后填入内容

normal=com.dubbo.demo.provider.Demo.NormalUserServiceImpl
vip=com.dubbo.demo.provider.Demo.VipUserServiceImpl

ok,下面让我们写一个 Test 来检测。

public class Test {
    public static void main(String[] args) {
        // 创建一个 URL 对象,模拟 Dubbo 的 URL
        URL url = URL.valueOf("dubbo://192.168.0.101:20880?user.service=normal");
        // 获取一个UserService对象
        UserService service = ExtensionLoader.getExtensionLoader(UserService.class)
                .getAdaptiveExtension();
        // 调用方法
        service.login();
    }
}

由于我们在 UserServiceSPI 注解上有 normal,所以我们的运行结果会调用 NormalUserServiceImpl.java

正常用户登录

当我们需要切换实现可以通过 URLuser.service 指向 vip,那么我们就会调用 VipUserServiceImpl.java,这个可以自己的去尝试。

或许有人说,是不是一定要 @Adaptive 实现的方法的形参都是要有 URL?这个问题的回答是“一定要有”!那么是不是一定是 URL 对象呢?这个问题的回答是“不一定哦”!至于为什么,我们开始看 createAdaptiveExtensionClassCode 的源码解析吧!

createAdaptiveExtensionClassCode 解析

我们直接看 ExtensionLoader.javacreateAdaptiveExtensionClassCode 方法。这段代码挺长的,但是围绕的核心就是:对带有注解的方法生成一个代理类的代码字符串。所以围绕这个核心就非常简单了。

    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;
            }
        }
        // 如果没有对应的注解修饰就不需要生成
        if (!hasAdaptiveAnnotation)
            throw new IllegalStateException("No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!");
        //添加 package、import信息
        //将新生成代理的类以 “接口名称$Adaptive” 形式命名
        codeBuilder.append("package ").append(type.getPackage().getName()).append(";");
        codeBuilder.append("\nimport ").append(ExtensionLoader.class.getName()).append(";");
        codeBuilder.append("\npublic 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 注解
            Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
            StringBuilder code = new StringBuilder(512);
            //如果为空抛异常
            if (adaptiveAnnotation == null) {
                code.append("throw new UnsupportedOperationException(\"method ")
                        .append(method.toString()).append(" of interface ")
                        .append(type.getName()).append(" is not adaptive method!\");");
            } else {
                int urlTypeIndex = -1;
                for (int i = 0; i < pts.length; ++i) {
                    if (pts[i].equals(URL.class)) {
                        urlTypeIndex = i;
                        break;
                    }
                }
                // !!!这里是从方法的形参中找到 URL
                if (urlTypeIndex != -1) {
                    // Null Point check
                    String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");",
                            urlTypeIndex);
                    code.append(s);

                    s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex);
                    code.append(s);
                }
                // !!!这里如果找不到 URL 就看形参方法里面的对象,有没有对象有 getUrl 的方法!!!
                // 注意这里已经解答了上面的疑问了!!!
                else {
                    String attribMethod = null;

                    // find URL getter method
                    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;
                            }
                        }
                    }
                    //如果这里都没有,说明真没有了,抛异常吧
                    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());
                    }

                    // 没有被 @Adaptive 修饰的方法自动生成一个“不支持调用”的异常
                    String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");",
                            urlTypeIndex, pts[urlTypeIndex].getName());
                    code.append(s);
                    s = String.format("\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");",
                            urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod);
                    code.append(s);

                    s = String.format("%s url = arg%d.%s();", URL.class.getName(), urlTypeIndex, attribMethod);
                    code.append(s);
                }
                //获取 @Adaptive 的 value 属性
                String[] value = adaptiveAnnotation.value();
                //如果没有设置,就生成一个值作为该接口在 URL 中的 key 
                if (value.length == 0) {
                    char[] charArray = type.getSimpleName().toCharArray();
                    StringBuilder sb = new StringBuilder(128);
                    for (int i = 0; i < charArray.length; i++) {
                        if (Character.isUpperCase(charArray[i])) {
                            if (i != 0) {
                                sb.append(".");
                            }
                            sb.append(Character.toLowerCase(charArray[i]));
                        } else {
                            sb.append(charArray[i]);
                        }
                    }
                    value = new String[]{sb.toString()};
                }

                boolean hasInvocation = false;
                //这里又循环方法形式参数
                for (int i = 0; i < pts.length; ++i) {
                    if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) {
                        // Null Point check
                        String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");", i);
                        code.append(s);
                        s = String.format("\nString methodName = arg%d.getMethodName();", i);
                        code.append(s);
                        hasInvocation = true;
                        break;
                    }
                }
                //cachedDefaultName 是在 @SPI 缓存的名字
                String defaultExtName = cachedDefaultName;
                String getNameCode = null;
                //这个循环主要是生成代码里面的一个判断,就是如果没有指定实现哪个子类取默认的实现
                //例如 userService 这里会生成 url.getParameter("user.service", "normal")
                for (int i = value.length - 1; i >= 0; --i) {
                    if (i == value.length - 1) {
                        if (null != defaultExtName) {
                            //判断是否等于 protocal(这个是默认的协议的名字)
                            if (!"protocol".equals(value[i]))
                                //查看有没有 invocation
                                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("\nString extName = ").append(getNameCode).append(";");
                //检查变量 extName 是否为空
                String s = String.format("\nif(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);
                //这里通过 ExtensionLoader.getExtension 方法获取
                s = String.format("\n%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);",
                        type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
                code.append(s);

                // 生成返回值
                if (!rt.equals(void.class)) {
                    code.append("\nreturn ");
                }

                s = String.format("extension.%s(", method.getName());
                code.append(s);
                for (int i = 0; i < pts.length; i++) {
                    if (i != 0)
                        code.append(", ");
                    code.append("arg").append(i);
                }
                code.append(");");
            }

            codeBuilder.append("\npublic ").append(rt.getCanonicalName()).append(" ").append(method.getName()).append("(");
            for (int i = 0; i < pts.length; i++) {
                if (i > 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("\n}");
        }
        codeBuilder.append("\n}");
        return codeBuilder.toString();
    }

因为上面代码需要严谨但是可能读起来可能会很繁杂,所以我以 UserService 为例子,贴出实际上生成的代码给你们看

package com.dubbo.demo.provider.Demo;

import com.alibaba.dubbo.common.extension.ExtensionLoader;

public class UserService$Adaptive implements com.dubbo.demo.provider.Demo.UserService {
    public void login(com.alibaba.dubbo.common.URL arg0) {
        if (arg0 == null) {
            throw new IllegalArgumentException("url == null");
        }

        com.alibaba.dubbo.common.URL url = arg0;
        String extName = url.getParameter("user.service", "normal");

        if (extName == null) {
            throw new IllegalStateException(
                "Fail to get extension(com.dubbo.demo.provider.Demo.UserService) name from url(" +
                url.toString() + ") use keys([user.service])");
        }

        com.dubbo.demo.provider.Demo.UserService extension = (com.dubbo.demo.provider.Demo.UserService) ExtensionLoader.getExtensionLoader(com.dubbo.demo.provider.Demo.UserService.class)
                                                                                                                       .getExtension(extName);
        extension.login(arg0);
    }
}

是不是恍然大悟?相信读完上面的代码你们知道这是这个机制的实际原理了!

结语

这篇文章仅仅是讲解了 @Adaptive 的一个作用,没有特别高深的知识。下节我会将 Dubbo 服务暴露的内容

下期见!