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

背景

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

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中的依赖注入。

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

推荐阅读更多精彩内容