美文网首页
dubbo系列之-SPI(2)-2021-01-09

dubbo系列之-SPI(2)-2021-01-09

作者: five_year | 来源:发表于2021-01-09 17:44 被阅读0次

    背景

    接下去我们分析下自适应扩展点也就是代码中所对应的

    if (clazz.isAnnotationPresent(Adaptive.class)) {
            cacheAdaptiveClass(clazz);
    
    

    这个Adaptive 注解可以加在类上也可以加在方法上,当然根据权限范围来说类的作用和控制范围大于方法

    类自适应扩展点

    还是回到我们最初的demo

    public static void main(String[] args) {
        ExtensionLoader<Job> extensionLoader = ExtensionLoader.getExtensionLoader(Job.class);
        Job program = extensionLoader.getExtension("program");
        program.play();
    }
    
    

    我们假设一个场景,在分布式场景中,我们要获取某个动态下发的value值,但是注册中心存在多种,我们的代码尝尝会这样写,当然我模拟的场景还是比较简单的,实际应用中还有更复杂的场景

    //file:com.poizon.study.provider.spi.ConfigCenter
    apollo=com.poizon.study.provider.spi.ApolloConfigCenter
    nacos=com.poizon.study.provider.spi.NacosConfigCenter
    //SPITest.java
    public static void main(String[] args) {
        ExtensionLoader<ConfigCenter> extensionLoader = ExtensionLoader.getExtensionLoader(ConfigCenter.class);
        ConfigCenter configCenter = null;
        if (apollo) {
            configCenter = extensionLoader.getExtension("apollo");
        } else {
            configCenter = extensionLoader.getExtension("nacos");
        }
        configCenter.get("key");
    }
    
    

    好,我们继续升级,将代码抽取成util

    public static void main(String[] args) {
        ExtensionLoader<ConfigCenter> extensionLoader = ExtensionLoader.getExtensionLoader(ConfigCenter.class);
        String value = ConfigCenterUtil.get("key", extensionLoader);
    }
    //ConfigCenterUtil.java
    public class ConfigCenterUtil {
        private static boolean apollo;
        public static String get(String key, ExtensionLoader<ConfigCenter> extensionLoader) {
            ConfigCenter configCenter = null;
            if (apollo) {
                configCenter = extensionLoader.getExtension("apollo");
            } else {
                configCenter = extensionLoader.getExtension("nacos");
            }
            return configCenter.get("key");
        }
    }
    
    

    我们再升级下,能否这个工具类也注册成为 ConfigCenter 接口的实现类

    //file:com.poizon.study.provider.spi.ConfigCenter
    apollo=com.poizon.study.provider.spi.ApolloConfigCenter
    nacos=com.poizon.study.provider.spi.NacosConfigCenter
    util=com.poizon.study.provider.spi.ConfigCenterUtil
    
    public class ConfigCenterUtil implements ConfigCenter{
        private static boolean apollo;
        @Override
        public String get(String key) {
            ExtensionLoader<ConfigCenter> extensionLoader = ExtensionLoader.getExtensionLoader(ConfigCenter.class);
            ConfigCenter configCenter = null;
            if (apollo) {
                configCenter = extensionLoader.getExtension("apollo");
            } else {
                configCenter = extensionLoader.getExtension("nacos");
            }
            return configCenter.get("key");
        }
    }
    
    public static void main(String[] args) {
        ExtensionLoader<ConfigCenter> extensionLoader = ExtensionLoader.getExtensionLoader(ConfigCenter.class);
        ConfigCenter util = extensionLoader.getExtension("util");
        util.get("key");
    }
    
    

    这样一来,我们把获取配置的实现细节全部都屏蔽到了ConfigCenterUtil这个实现类中,上层不需要关系实现,像这个类的作用就可以叫做自适应扩展类,然后在dubbo里面有一种另外的写法,将类中加上@Adaptive 标志注解,ExtensionLoader 也专门提供了获取方法getAdaptiveExtension,并且这个值是就是通过这句代码赋值的,并且一个接口只能一个自适应扩展点,多个会报错,当然也不会被抛出,因为dubbo封装了错误到map,中只有最后找不到实现合适的实现类才会吐出错误栈

    if (clazz.isAnnotationPresent(Adaptive.class)) {//☆先跳过后面分析
            cacheAdaptiveClass(clazz);
     }
    private void cacheAdaptiveClass(Class<?> clazz) {
        if (cachedAdaptiveClass == null) {
            cachedAdaptiveClass = clazz;
        } else if (!cachedAdaptiveClass.equals(clazz)) {
            throw new IllegalStateException();
        }
    }
    
    

    最后我们的版本变为

    @Adaptive
    public class ConfigCenterUtil implements ConfigCenter{
    //.........
    
    public static void main(String[] args) {
        ExtensionLoader<ConfigCenter> extensionLoader = ExtensionLoader.getExtensionLoader(ConfigCenter.class);
        ConfigCenter util = extensionLoader.getAdaptiveExtension();
        String key = util.get("key");
        System.out.println(key);//打印:nacos key
    }
    
    

    dubbo 框架里面也有类扩展点实现(ExtensionFactory),我会在后面做详细介绍。

    方法自适应扩展点

    像上面这样实现,的确很复杂,我们要扩展很多的类,是不是都要这样实现,答案肯定不是,我们在举一个场景,登录,现在互联网软件如雨后春笋一样多,登录的方式也不断扩从,我亲身经历了,从手机验证码登录到微信facebook等sns登录,再到后面的手机号本地登录,当然未来还会有更多的登录方式我们的代码如何更好的做扩展?

    //com.poizon.study.provider.spi.Login
    weixin=com.poizon.study.provider.spi.WeixinLogin
    phone=com.poizon.study.provider.spi.PhoneLogin
    
    public static void main(String[] args) {
        String loginType = "weixin";
        ExtensionLoader<Login> extensionLoader = ExtensionLoader.getExtensionLoader(Login.class);
        if (loginType.equals("weixin")) {
            extensionLoader.getExtension("weixin").doLogin();
        } else if (loginType.equals("phone")) {
            extensionLoader.getExtension("phone").doLogin();
        }
    }
    
    

    上面的代码很简单,扩展起来也很方便 大不了在加if else;这种方式对于有经验的开发会选择升级为工厂模式的写法,我们试试,在试之前我们介绍下URI(统一资源定位符号),将登陆方式用URI的形式传给工厂,用这种形式来消除分支。

    public static void main(String[] args) {
        String loginType = "weixin";
        URL url = new URL(loginType, null, (Integer) null);
        doLogin(url);
    }
    
    public static void doLogin(URL url) {
        ExtensionLoader<Login> extensionLoader = ExtensionLoader.getExtensionLoader(Login.class);
        String protocol = url.getProtocol();
        if (StringUtils.isEmpty(protocol)) {
            protocol = "hupu";
        }
        extensionLoader.getExtension(protocol).doLogin();
    }
    
    

    我们观察doLogin 中的代码,如果不设置默认值“hupu”的话,是否可以认为和接口没关系,只是一种查找实现类的规范,没错dubbo 也是就是这种方式来查找实现类,将@Adaptive暴露给用户来设置,我们来看看采用dubbo自适应扩展点方法的写法

    @SPI
    public interface Login {
        @Adaptive("protocol")
        boolean doLogin(URL url);
    }
    
    public static void main(String[] args) {
        String loginType = "weixin";
        URL url = new URL(loginType, "8888", 80);
        ExtensionLoader<Login> extensionLoader = ExtensionLoader.getExtensionLoader(Login.class);
        extensionLoader.getAdaptiveExtension().doLogin(url);
        //打印:do weixin login
    }
    
    

    也一样实现了自动选择登陆方式的功能,对比起来少了一个工厂类,那么dubbo是怎么实现的呢,我们深入源码之前先debug看看实例名称,“Login$Adapter” 这个类我们没有定义过,推测实现方式应该是动态代理,一探究竟

    image

    顺着 getAdaptiveExtension() 进去

    public T getAdaptiveExtension() {
       //省略..
       instance = createAdaptiveExtension();
       return (T) instance;
    }
    
    private T createAdaptiveExtension() {
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    }
    
    private Class<?> getAdaptiveExtensionClass() {
        getExtensionClasses();
        return cachedAdaptiveClass = 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);
    }
    
    

    果然在最后找到了compiler.compile();将code代码编译为java对象,compiler也是扩展点,默认实现为javassist(),可以通过 <dubbo:application compiler="jdk" /> 进行设置

    @SPI("javassist") //默认扩展点javassist
    public interface Compiler {
        Class<?> compile(String code, ClassLoader classLoader);
    }
    
    

    Dubbo 也是推荐使用javassist 字节码的方式效率更好,jdk大家可以看看,编译还在1.6的版本

    image

    编译的过程就不深入了,我们主要看看这个code 变量的值是怎么生成的。

    //org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generate
    public String generate() {
        StringBuilder code = new StringBuilder();
        code.append(generatePackageInfo());
        code.append(generateImports());
        code.append(generateClassDeclaration());
        Method[] methods = type.getMethods();
        for (Method method : methods) {
            code.append(generateMethod(method));
        }
        code.append("}");
        return code.toString();
    }
    //org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generateMethod
    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);
    }
    //org....common.extension.AdaptiveClassCodeGenerator#generateMethodContent
    private String generateMethodContent(Method method) {
        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
        StringBuilder code = new StringBuilder(512);
        if (adaptiveAnnotation == null) {
            return generateUnsupported(method);
        } else {
            //解释在下面
            int urlTypeIndex = getUrlTypeIndex(method);
            if (urlTypeIndex != -1) {
                code.append(generateUrlNullCheck(urlTypeIndex));
            } else {//解释在下面
                code.append(generateUrlAssignmentIndirectly(method));
            }
            //获取@Adaptive("protocol")中的value 没有则用接口名去除驼峰
            String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
    
            boolean hasInvocation = hasInvocationArgument(method);
    
            code.append(generateInvocationArgumentNullCheck(method));
             //解释在下面
            code.append(generateExtNameAssignment(value, hasInvocation));
            // 封装报错信息 类似我们代码中写的throw new Exception()这样
            code.append(generateExtNameNullCheck(value));
            //解释在下面
            code.append(generateExtensionAssignment());
            // 封装返回
            code.append(generateReturnAndInvocation(method));
        }
    
        return code.toString();
    }
    
    private int getUrlTypeIndex(Method method) {            
        int urlTypeIndex = -1;
        //功能很简单,找到方法参数中是否是URL类型的参数,有返回参数位置索引
        //我们回忆下doLogin 中我们把URL 参数最为第一个参数传了进来
        Class<?>[] pts = method.getParameterTypes();
        for (int i = 0; i < pts.length; ++i) {
            if (pts[i].equals(URL.class)) {
                urlTypeIndex = I;
                break;
            }
        }
        return urlTypeIndex;
    }
    
    //org....common.extension.AdaptiveClassCodeGenerator#generateUrlAssignmentIndirectly
    private String generateUrlAssignmentIndirectly(Method method) {
        Class<?>[] pts = method.getParameterTypes();
        //如果方法没带URL类型的参数,就遍历参数的getXxx方法看是否有
        //这段代码看着还是挺有意思的,和我们写的很逊色
        for (int i = 0; i < pts.length; ++i) {
            for (Method m : pts[i].getMethods()) {
                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) {
                    return generateGetUrlNullCheck(i, pts[i], name);
                }
            }
        }
    }
    
    #这个方法太重要了高亮显示
    org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generateExtNameAssignment
    private String generateExtNameAssignment(String[] value, boolean hasInvocation) {
       //接口中我们传入的是protocol
        String getNameCode = null;
        for (int i = value.length - 1; i >= 0; --i) {
            //defaultExtName 留意下就是我们在@SPI("defaultExtName") 中设置的
            if (null != defaultExtName) {
                if (!"protocol".equals(value[i])) {
                    if (hasInvocation) {
                        getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                    } else {
                    //如果扩展点不是protocol 则通过url.getParameter去获取
                    //这种写法也是支持的我们在创建对象的时候可以这样去设置
    
                    //url.addParameter("loginType", loginType);
                    //@Adaptive("loginType")
                    //boolean doLogin(URL url);
                        getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
                    }
                } else {
                //如果key是protocol ,则通过 url.getProtocol() 获取实现类的扩展点名称
                    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()";
                }
            }
    
        }
        //最后通过正则将获取到的扩展点名称赋值给 extName 变量
        return String.format(CODE_EXT_NAME_ASSIGNMENT, getNameCode);
    }
    private static final String CODE_EXT_NAME_ASSIGNMENT = "String extName = %s;\n";
    
    //这边还是通过正则拼装调用ExtensionLoader.getExtensionLoader(extName) 获取真正要调用的扩展点
    private String generateExtensionAssignment() {
        return String.format(CODE_EXTENSION_ASSIGNMENT, type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
    }
    static String CODE_EXTENSION_ASSIGNMENT = "%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);\n";
    
    

    最后我们dubug 看看Login 类生成的code代码,过过眼瘾

    image

    一步步验证自己的猜想。

    总结

    源码这块主要是多调试,一遍不行就十遍,自适应扩展点就写到这里,后面接着分析dubbo中的依赖注入。

    相关文章

      网友评论

          本文标题:dubbo系列之-SPI(2)-2021-01-09

          本文链接:https://www.haomeiwen.com/subject/wancaktx.html