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

背景

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

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

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

类自适应扩展点

还是回到我们最初的demo

public static void main(String[] args) {
    ExtensionLoader 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 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 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 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 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 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 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 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 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 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(),可以通过 进行设置

@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 = (%

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

image

一步步验证自己的猜想。

总结

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

你可能感兴趣的:(dubbo系列之-SPI(2)-2021-01-09)