dubbo SPI机制与Protocol自适应扩展问题

1.@Adaptive使用规则疑云

        近期,在阅读dubbo服务暴露相关源码时,有一点引起了我的注意。

        在很多dubbo源码的解读文章中,都告诉我们,对于被@Adaptive接口修饰的方法,我们都需要在其方法参数中提供一个url参数,并且在url的参数列表中指定我们所需要的扩展实现,如:

        

@SPI("Mercedes")
public interface MyCar {
    //汽车运行
    @Adaptive
    void run(URL url);

    //汽车开门
    @Adaptive
    void openDoor(URL url);
}

        上述代码就是在被修饰方法中加了URL参数,那么我们在使用run或者openDoor方法时就需要构造URL对象,在参数列表中指定所需要的扩展实例:

        ExtensionLoader<MyCar> loader = ExtensionLoader.getExtensionLoader(MyCar.class);
        MyCar car = loader.getAdaptiveExtension();
        /**
         * 无参数使用方式
         */
        //将扩展点接口名按单词拆分,用.分隔,作为参数列表名
        car.run(URL.valueOf("http://localhost:9999/xxx?my.car=BMW"));
    

        这样我们就可以调用BMW对应扩展实现类的run方法了

        但是在服务暴露过程中,我发现对于Protocol类型的类,似乎有点不太一样,比如如下本地暴露的源码:

   private void exportLocal(URL url) {
        if (!"injvm".equalsIgnoreCase(url.getProtocol())) {
            URL local = URL.valueOf(url.toFullString()).setProtocol("injvm").setHost("127.0.0.1").setPort(0);
            ServiceClassHolder.getInstance().pushServiceClass(this.getServiceClass(this.ref));
            Exporter<?> exporter = protocol.export(proxyFactory.getInvoker(this.ref, this.interfaceClass, local));
            this.exporters.add(exporter);
            logger.info("Export dubbo service " + this.interfaceClass.getName() + " to local registry");
        }

    }

        我们可以看到protocol.export(Invoker<T> invoker)实际上接受的是一个invoker对象....

dubbo SPI机制与Protocol自适应扩展问题

        这似乎和很多文章说的不太一样,然鹅更离谱的还在后面,这是我们的Protocol扩展点定义:

@SPI("dubbo")
public interface Protocol {

    int getDefaultPort();

    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    void destroy();

    default List<ProtocolServer> getServers() {
        return Collections.emptyList();
    }

}

        这是一个无参的    @Adaptive 注解,这意味着url应该形如:http://xxx.xx.com?protocol=injvm这种格式,然鹅通过debug我们可以看到,我们的url参数中压根就没有protocol参数

 dubbo SPI机制与Protocol自适应扩展问题

dubbo SPI机制与Protocol自适应扩展问题

        这是咋回事儿?这小老弟怎么好像跟大佬们文章里的表现不太一样?

2.回顾@Adaptive

        我们先回顾一下@Adaptive注解的用法

2.1什么是dubbo SPI

        由于JDK SPI的诸多弊端,dubbo决定自定义实现一套SPI功能 

     JDK标准的SPI只能通过遍历来查找扩展点和实例化,有可能导致一次性加载所有的扩展点,如果不是所有的扩展点都被用到,就会导致资源的浪费。例如com.alibaba.dubbo.rpc.Protocol接口有InjvmProtocol、DubboProtocol、RmiProtocol、HttpProtocol、HessianProtocol等实现,如果只是用到其中一个实现,却要加载全部实现,会导致资源的浪费。

     dubbo把配置文件中扩展实现的格式修改为键值对格式,例如META-INF/dubbo/com.xxx.Protocol里的com.foo.XxxProtocol格式改为了xxx = com.foo.XxxProtocol这种以键值对的形式,这样做的目的是为了让我们更容易的定位到问题:比如由于第三方库不存在,无法初始化,导致无法加载扩展名(“A”),当用户配置使用A时,dubbo就会报无法加载扩展名的错误,而不是报哪些扩展名的实现加载失败以及错误原因,这是因为原来的配置格式没有把扩展名的id记录,导致dubbo无法抛出较为精准的异常,这会加大排查问题的难度。所以改成key-value的形式来进行配置。

     dubbo的SPI机制增加了对IOC、AOP的支持,一个扩展点可以直接通过setter注入到其他扩展点        

     dubbo支持三个目录下的配置文件搜寻:

  • "META-INF/dubbo/internal/"是dubbo内部提供的扩展的配置文件路径
  • "META-INF/services/"是dubbo为了兼容jdk的SPI扩展机制思想而设存在的 
  • "META-INF/dubbo/"是为了给用户自定义的扩展实现配置文件存放
  • 2.2几个重要概念

           

           1.扩展点(Extension Point):是一个Java的接口

           2.扩展名(Extension name):配置文件中键值对的key

           2.扩展(Extension):扩展点的实现类。一个扩展点可能对应多个扩展

           3.自适应扩展实例(Extension Adaptive Instance):在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展类。通过@Adaptive注解指定

    2.3@Adaptive注解介绍

           

         自适应扩展点,运行时根据实际的URL参数来决定使用哪一种扩展,注解在class和方法上。

         有时,我们往往不希望在框架启动阶段加载对应的扩展类,而是希望在运行过程中,通过传入的参数(往往是外部传入的URL)来决定加载哪一种扩展类,那么这就产生了矛盾,拓展类未被加载,那么拓展类的方法就无法被调用(静态方法除外)。拓展方法未被调用,拓展就无法被加载

         dubbo提供了@Adaptive注解帮助我们解决运行时根据参数自适应选择扩展类的问题,该注解一般使用在方法上,很少用在类上:

         当用在方法上时:Dubbo会为该接口自动生成一个$子类,并且按照一定的格式重写该方法,而其余没有标注@Adaptive注解的方法将会默认抛出异常

         当用在类上时:在代表这个类是人工实现的(不需要代理),当它被注释到类上,意味着扩展点只有这一个选择,最典型的应用如AdaptiveCompiler、AdaptiveExtensionFactory等

         注意:被@Adaptive注解标记的方法必须携带Url参数

2.4@Adaptive实例

       说这么多不如举个实际的例子

     

2.4.1有参数的@Adaptive

        @Adaptive注解可以接收一个String[] value的数组,数组中的字符串是key值,代码中要根据key值来获取对应的Value值,进而加载相应的extension实例。比如new String[]{“key1”,”key2”},表示会先在URL中寻找key1的值,

        如果找到则使用此值加载extension,如果key1没有,则寻找key2的值,如果key2也没有,则使用@SPI注解的默认值,如果SPI注解没有默认值,则报错

@SPI("Mercedes")
public interface MyCar {
    //汽车发动
    @Adaptive({"A_name","B_name","name"})
    void run(URL url);

    //汽车开门
    void openDoor(URL url);
}
public class MercedesCar implements MyCar{

    @Override
    public void run(URL url) {
        System.out.println("奔驰车发动");
    }

    @Override
    public void openDoor(URL url) {
        System.out.println("奔驰车开门");
    }

}

public class BMWCar implements MyCar {

    @Override
    public void run(URL url) {
        System.out.println("宝马车发动");
    }

    @Override
    public void openDoor(URL url) {
        System.out.println("宝马车开门");
    }
}
Mercedes=org.apache.dubbo.demo.api.MercedesCar
BMW=org.apache.dubbo.demo.api.BMWCar
public static void main(String[] args) {
    ExtensionLoader<MyCar> loader = ExtensionLoader.getExtensionLoader(MyCar.class);
    MyCar car = loader.getAdaptiveExtension();
    /**
     * 有参数使用方式
     */
    //有符合@Adaptive的参数,B_name优先级高于name,因此是宝马车发动
    car.run(URL.valueOf("http://localhost:9999/xxx?name=Mercedes&B_name=BMW"));
    //没有符合@Adaptive的参数,使用@SPI注解指定的默认值,因为@SPI注解没有指定默认值,在这里会报错
    car.run(URL.valueOf("http://localhost:9999/xxx"));
    //调用没有被@Adaptive标记的方法会抛出异常
    car.openDoor(url);
}

结果:

dubbo SPI机制与Protocol自适应扩展问题

2.4.2无参数的@Adaptive

        以’.’分隔,例如org.apache.dubbo.xxx.YyyInvokerWrapper接口名会变成yyy.invoker.wrapper,然后以此名称作为key到URL寻找,如果仍没有找到,使用@SPI注解默认值,如果仍然没有则抛出IllegalStateException异常。

eg:        

@SPI("Mercedes")
public interface MyCar {
    //汽车发动
    @Adaptive
    void run(URL url);

    //汽车开门
    void openDoor(URL url);
}
public static void main(String[] args) {
    ExtensionLoader<MyCar> loader = ExtensionLoader.getExtensionLoader(MyCar.class);
    MyCar car = loader.getAdaptiveExtension();
    /**
     * 无参数使用方式
     */
    //没有符合@Adaptive的参数,也没有@SPI注解指定的默认值,则将扩展点接口名按单词拆分,用.分隔
    URL url = URL.valueOf("http://localhost:9999/xxx?my.car=BMW");
    car.run(url);
    //调用没有被@Adaptive标记的方法会抛出异常
    car.openDoor(url);
}

运行结果

dubbo SPI机制与Protocol自适应扩展问题

3.源码探秘

3.1protocol竟然开小差?

        那么是什么造成上面protocol.export()命名没有按规矩来,结果还是正常扩展了呢?让我们把dubbo的日志级别调整到debug级别,这样dubbo就会在运行过程中将动态拼接的代码给打印出来,让我们来看一看 

dubbo SPI机制与Protocol自适应扩展问题

         嗯?乱成这样?

        让我们美化一下:


public class Protocol$Adaptive implements Protocol {
    public void destroy() {
        throw new UnsupportedOperationException("The method public abstract void Protocol.destroy() of interface Protocol is not adaptive method!");
    }

    public int getDefaultPort() {
        throw new UnsupportedOperationException("The method public abstract int Protocol.getDefaultPort() of interface Protocol is not adaptive method!");
    }

    public Exporter export(Invoker arg0) throws RpcException {
        if (arg0 == null) throw new IllegalArgumentException("Invoker argument == null");
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("Invoker argument getUrl() == null");
        URL url = arg0.getUrl();
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (Protocol) name from url (" + url.toString() + ") use keys([protocol])");
        Protocol extension = (Protocol) ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }

    public java.util.List getServers() {
        throw new UnsupportedOperationException("The method public default java.util.List Protocol.getServers() of interface Protocol is not adaptive method!");
    }

    public Invoker refer(java.lang.Class arg0, URL arg1) throws RpcException {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        URL url = arg1;
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (Protocol) name from url (" + url.toString() + ") use keys([protocol])");
        Protocol extension = (Protocol) ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }
}

         嗯?发现没有,  String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());这里居然是直接取了url的协议字段?这个Protocol接口好像有点东西,一般来说我们生成的都是url.getParameter()方法的代码,他这里居然是getProtocol()?

dubbo SPI机制与Protocol自适应扩展问题

        那就得好好扒一扒动态生成代码的源码了,dubbo动态生成Protocol$Adaptive的文件是 AdaptiveClassCodeGenerator.java中的generateMethodContent方法,让我们打好断点跟进去,发现获取扩展名key的是这一段代码

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

        // found parameter in URL type
        if (urlTypeIndex != -1) {
            // Null Point check
            code.append(generateUrlNullCheck(urlTypeIndex));
        } else {
            // did not find parameter in URL type
            code.append(generateUrlAssignmentIndirectly(method));
        }
        //todo 获取扩展名key
        String[] value = getMethodAdaptiveValue(adaptiveAnnotation);

        boolean hasInvocation = hasInvocationArgument(method);

        code.append(generateInvocationArgumentNullCheck(method));
        //todo 根据扩展名key获取到extName
        code.append(generateExtNameAssignment(value, hasInvocation));
        // check extName == null?
        code.append(generateExtNameNullCheck(value));

        code.append(generateExtensionAssignment());

        // return statement
        code.append(generateReturnAndInvocation(method));
    }

    return code.toString();
}

        跟进去        

    private String[] getMethodAdaptiveValue(Adaptive adaptiveAnnotation) {
        String[] value = adaptiveAnnotation.value();
        // value is not set, use the value generated from class name as the key
        if (value.length == 0) {
            String splitName = StringUtils.camelToSplitName(type.getSimpleName(), ".");
            value = new String[]{splitName};
        }
        return value;
    }

        确实和大佬们说的一样啊,没问题啊,如果注解上有参数,就用参数作为key,没参数驼峰转"."间隔

        往下走,看看extName的获取code.append(generateExtNameAssignment(value, hasInvocation));

        

private String generateExtNameAssignment(String[] value, boolean hasInvocation) {
        // TODO: refactor it
        String getNameCode = null;
        for (int i = value.length - 1; i >= 0; --i) {
            if (i == value.length - 1) {
                if (null != defaultExtName) {
                    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], 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);
                }
            }
        }

        return String.format(CODE_EXT_NAME_ASSIGNMENT, getNameCode);
    }

                嗯?居然给protocol开了小灶?专门处理?这样一来的话,对于扩展名的key为protocol的扩展,都会取url.getProtocol()而不是url.getParameter()?让我们试一下:

@SPI("Mercedes")
public interface MyCar {
    //通过adaptive注解加参数的方式使得扩展点的key名为protocol
    @Adaptive("protocol")
    void run(Ssr url);

    //汽车开门
    void openDoor(URL url);
}

public class Main {
    public static void main(String[] args) {
        
        //这部分不用管
        ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
        Protocol protocol = loader.getAdaptiveExtension();
        ProxyFactory factory=(ProxyFactory)ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
        
        //先试一下injvm
        protocol.export(factory.getInvoker(new Object(), Object.class, URL.valueOf("injvm://localhost:9999/xxx")));
        //再试一下把扩展点key写到parameter列表中会不会报错
        protocol.export(factory.getInvoker(new Object(), Object.class, URL.valueOf("sss://localhost:9999/xxx?protocol=dubbo")));
    }

        结果:

dubbo SPI机制与Protocol自适应扩展问题

         天哪,第一个正常调用了injvmProtocol扩展实现,而第二个报了错,说没有sss这种扩展实现类,可是按照大佬们文章里的,不论@Adaptive有无参数,这样写都应该能够得到dubboProtocol的扩展才对。

 3.2 url不一定必须放在方法的参数列表中

        可是我们还有一个疑问没有解决,就算url的参数列表可以没有参数protocol,直接从url的协议字段中取,那protocol.export()方法中并没有url参数啊,你连url参数都没有,何谈去协议字段中取值呢?

        我们直接让方法报个错,然后跟进去看源码,我们这样修改一下,直接把URL参数去掉:

@SPI("Mercedes")
public interface MyCar {
    //通过adaptive注解加参数的方式使得扩展点的key名为protocol
    @Adaptive
    void run();

    //汽车开门
    void openDoor(URL url);
}
public class Main {
    public static void main(String[] args) {
        ExtensionLoader<MyCar> loader = ExtensionLoader.getExtensionLoader(MyCar.class);
        MyCar car = loader.getAdaptiveExtension();
        car.run();
}

点击这一行报错

dubbo SPI机制与Protocol自适应扩展问题

我们会看到这样一个方法:

 private String generateUrlAssignmentIndirectly(Method method) {
        Class<?>[] pts = method.getParameterTypes();

        Map<String, Integer> getterReturnUrl = new HashMap<>();
        // find URL getter method
        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) {
                    getterReturnUrl.put(name, i);
                }
            }
        }

        if (getterReturnUrl.size() <= 0) {
            // getter method not found, throw
            throw new IllegalStateException("Failed to create adaptive class for interface " + type.getName()
                    + ": not found url parameter or url attribute in parameters of method " + method.getName());
        }

        Integer index = getterReturnUrl.get("getUrl");
        if (index != null) {
            return generateGetUrlNullCheck(index, pts[index], "getUrl");
        } else {
            Map.Entry<String, Integer> entry = getterReturnUrl.entrySet().iterator().next();
            return generateGetUrlNullCheck(entry.getValue(), pts[entry.getValue()], entry.getKey());
        }
    }

      看到没,人家要求参数列表中只要有一个参数存在这样一个方法就可以读取到url

        1.方法以get开头,且名字长度大于3

        2.必须是public方法

        3.不能是静态方法

        4.方法必须是无参的

        5.方法的返回值必须是URL类型

      哦?这么说,只要满足这些要求就行了?那我.....

@SPI("Mercedes")
public interface MyCar {
    //汽车发动(URL包裹在Ssr对象中)
    @Adaptive
    void run(Ssr surl);

    //汽车开门
    void openDoor(URL url);
}

public class Ssr {
    /**
     * 我是不是很骚?
     */
    public URL get我就是乱写的方法名() {
        return URL.valueOf("http://localhost:9999/xxx?my.car=Mercedes");
    }
}

 运行一下,居然可以!

dubbo SPI机制与Protocol自适应扩展问题

 好啦,让我们总结一下

4.总结

        @Adaptive总体上满足以下规则:

                

        1.有参数的@Adaptive注解:@Adaptive注解可以接收一个String[] value的数组,数组中的字符串是key值,代码中要根据key值来获取对应的Value值,进而加载相应的extension实例。比如new String[]{“key1”,”key2”},表示会先在URL中寻找key1的值,如果找到则使用此值加载extension,如果key1没有,则寻找key2的值,如果key2也没有,则使用@SPI注解的默认值,如果SPI注解没有默认值,则报错

         2.无参的@Adaptive注解:以’.’分隔,例如org.apache.dubbo.xxx.YyyInvokerWrapper接口名会变成yyy.invoker.wrapper,然后以此名称作为key到URL寻找,如果仍没有找到,使用@SPI注解默认值,如果仍然没有则抛出IllegalStateException异常。

        3.不论是@Adaptive注解有没有加参数,只要是最终生成的extenssion的key值为protocol,dubbo spi都会特殊处理。对于Protocol扩展点接口(或者通过其他方式导致key最后为protocol的扩展点接口)的SPI自适应扩展,都是通过根据URL的协议来的,而其他接口则是根据URL的parameter来的,

        4.URL不一定直接作为方法参数,也可以包裹在pojo对象中,然后将pojo对象作为参数传递,但是对应的pojo类型必须提供符合要求的返回url的方法,规则如下:     

                (1)方法以get开头,且名字长度大于3

                (2)必须是public方法

                (3)不能是静态方法

                (4)方法必须是无参的

                (5)方法的返回值必须是URL类型

dubbo SPI机制与Protocol自适应扩展问题

上一篇:gRPC入门指南


下一篇:protocol buffer应用场景方案想法