dubbo泛化调用使用及原理解析

本文案例代码见 git@github.com:shengchaojie/dubbo_best_practise.git

什么是泛化调用

通常我们想调用别人的dubbo服务时,我们需要在项目中引入对应的jar包。而泛化调用的作用是,我们无需依赖相关jar包,也能调用到该服务。

这个特性一般使用在网关类项目中,在业务开发中基本不会使用。

使用方式

假设我现在要调用下面的接口服务

package com.scj.demo.dubbo.provider.service.impl;

public interface ByeService {

    String bye(String name);

}

api

ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
referenceConfig.setApplication(new ApplicationConfig("test"));
referenceConfig.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));
referenceConfig.setInterface("com.scj.demo.dubbo.provider.service.impl.ByeService");
referenceConfig.setGeneric(true);
GenericService genericService = referenceConfig.get();

Object result = genericService.$invoke(
    "bye",
    new String[]{"java.lang.String"},
    new Object[]{"1234"});

System.out.println(result);

spring

在xml文件做以下配置

<dubbo:reference id="byeService" interface="com.scj.demo.dubbo.provider.service.impl.ByeService" generic="true" />

然后注入使用

@Service
public class PersonService {

    @Resource(name = "byeService")
    private GenericService genericService;

    public void sayBye(){
        Object result = genericService.$invoke(
                "bye",
                new String[]{"java.lang.String"},
                new Object[]{"1234"});
        System.out.println(result);
    }

}

在两种调用方式中,我们都需要使用被调用接口的字符串参数生成GenericService,通过GenericService的$invoke间接调用目标接口的接口。

public interface GenericService {

    /**
     * Generic invocation
     *
     * @param method         Method name, e.g. findPerson. If there are overridden methods, parameter info is
     *                       required, e.g. findPerson(java.lang.String)
     * @param parameterTypes Parameter types
     * @param args           Arguments
     * @return invocation return value
     * @throws Throwable potential exception thrown from the invocation
     */
    Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;

}

$invoke的三个参数分别为,方法名,方法参数类型数组,方法参数数组。

方法入参构造

可以看到泛化调用的一个复杂性在于$invoke的第三个参数的组装,下面介绍几种复杂入参的调用方式

首先丰富提供者接口

public interface ByeService {

    String bye(String name);

    String bye(String name, Long age, Date date);

    String bye(Person person);

    String bye(List<String> names);

    String bye(String[] names);

    String byePersons(List<Person> persons);

    String byePersons(Person[] persons);

    @Data
    public static class Person{
        private String name;

        private Long age;

        private Date birth;
    }

    public static void main(String[] args) {
        System.out.println(Person.class.getName());
    }

}

多参数

    @Test
    public void testMultiParam(){
        result = genericService.$invoke(
                "bye",
                new String[]{"java.lang.String","java.lang.Long","java.util.Date"},
                new Object[]{"scj",12L,new Date()});

        System.out.println(result);
    }

POJO

    Map<String,Object> personMap = new HashMap<>();
    {
        personMap.put("name","scj");
        personMap.put("age","12");
        personMap.put("birth",new Date());
    }

    @Test
    public void testPOJO(){

        result = genericService.$invoke(
                "bye",
                new String[]{"com.scj.demo.dubbo.provider.service.impl.ByeService$Person"},
                new Object[]{personMap});

        System.out.println(result);
    }

Map

    @Test
    public void testMap(){
        result = genericService.$invoke(
                "bye",
                new String[]{"java.util.Map"},
                new Object[]{personMap});

        System.out.println(result);
    }

集合

    @Test
    public void testList(){
        List<String> names = Lists.newArrayList("scj1","scj2");
        result = genericService.$invoke(
                "bye",
                new String[]{"java.util.List"},
                new Object[]{names});

        System.out.println(result);
    }

数组

    @Test
    public void testArray(){
        String[] nameArray = new String[]{"scj1","scj3"};
        result = genericService.$invoke(
                "bye",
                new String[]{"java.lang.String[]"},
                new Object[]{nameArray});

        System.out.println(result);
    }

集合+POJO

    @Test
    public void testPOJOList(){
        result = genericService.$invoke(
                "byePersons",
                new String[]{"java.util.List"},
                new Object[]{Lists.newArrayList(personMap,personMap)});

        System.out.println(result);
    }

数组+POJO

    @Test
    public void testPOJOArray(){
        result = genericService.$invoke(
                "byePersons",
                new String[]{"com.scj.demo.dubbo.provider.service.impl.ByeService$Person[]"},
                new Object[]{Lists.newArrayList(personMap,personMap)});

        System.out.println(result);
    }

结果返回

与入参相似,虽然$invoke的返回定义为Object,实际上针对不同类型有不同的返回。

别想着转换为POJO,你都泛化调用了,搞不到接口,如何转换。当然自己定义一个完全一样的当然也行。

接口返回类型 $invoke返回类型
基础类型 基础类型
POJO HashMap
Collection List返回ArrayList,Set返回HashSet
Array Array
组合类型 根据上述映射组合返回

原理介绍

消费者端

泛化调用和直接调用在消费者者端,在使用上的区别是,我们调用服务时使用的接口为GenericService,方法为$invoker。在底层的区别是,消费者端发出的rpc报文发生了变化。

使用方式上的改变

在使用上,不管哪种配置方式,我们都需要配置generic=true

设置generic=true后,RefereceConfig的interfaceClass会被强制设置为GenericService

if (ProtocolUtils.isGeneric(getGeneric())) {
    //如果是泛化调用
    interfaceClass = GenericService.class;
} else {
    try {
        interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
                                       .getContextClassLoader());
    } catch (ClassNotFoundException e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
    checkInterfaceAndMethods(interfaceClass, methods);
}

这也使得我们的RefereanceBean返回的是GenericService类型的代理。

invoker = refprotocol.refer(interfaceClass, urls.get(0));

生成的代理是GenericService的代理只是我们使用方式上的变化,更为核心的是,底层发送的rpc报文发生了什么变化。

底层报文变化

Dubbo的rpc报文分为header和body两部分。我们这边只需要关注body部分。构造逻辑如下

    @Override
    protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
        RpcInvocation inv = (RpcInvocation) data;

        out.writeUTF(version);//dubbo版本号
        out.writeUTF(inv.getAttachment(Constants.PATH_KEY));//path 就是接口全限定名
        out.writeUTF(inv.getAttachment(Constants.VERSION_KEY));// 接口版本号

        out.writeUTF(inv.getMethodName());//方法名
        out.writeUTF(ReflectUtils.getDesc(inv.getParameterTypes()));//方法参数类型
        Object[] args = inv.getArguments();
        if (args != null) {
            for (int i = 0; i < args.length; i++) {
                out.writeObject(encodeInvocationArgument(channel, inv, i));//方法参数
            }
        }
        out.writeObject(RpcUtils.getNecessaryAttachments(inv));//rpc上下文
    }

那么我们通过直接调用与泛化调用ByeService的bye方法在报文上有啥区别呢?

我一开始以为报文中的path是GenericeService,其实并没有,path就是我们调用的目标方法。

path来源???todo

而报文中的方法名,方法参数类型以及具体参数,还是按照GenericeService的$invoke方法入参传递的。

这么个二合一的报文,发送到提供者那边,它估计也会很懵逼,我应该怎么执行?

所以针对泛化调用报文还会把generic=true放在attchment中传递过去

具体逻辑在GenericImplFilter中。

GenericImplFilter中有很多其他逻辑,比如泛化调用使用的序列化协议,正常接口走泛化调用的模式,我们只需要设置attachment的那部分。

针对泛化调用,要进行2次序列化/反序列化。看下POJO的调用方式你就知道为啥了

((RpcInvocation) invocation).setAttachment(
    Constants.GENERIC_KEY, invoker.getUrl().getParameter(Constants.GENERIC_KEY));

知道消费者端报文发生了什么变化,那么接下来就去看提供者端如何处理这个改造后的报文。

interfaceClass和interfaceName的区别

总结一下ReferenceConfig中interfaceClass和interfaceName的区别?(这道面试题好像不错)

interfaceClass用于指定生成代理的接口
interfaceName用于指定发送rpc报文中的path(告诉服务端我要调用那个服务)

提供者端

消费者泛化调用的rpc报文传递到提供者还不能直接使用,虽然path是对的,但是实际的方法名,参数类型,参数要从rpc报文的参数中提取出来。

GenericFilter就是用来做这件事情。

在提供者这边,针对泛化调用的逻辑全部封装到了GenericFilter,解耦的非常好。

GenericFilter逻辑分析

1. 是否是泛化调用判断
if (inv.getMethodName().equals(Constants.$INVOKE)
                && inv.getArguments() != null
                && inv.getArguments().length == 3
                && !GenericService.class.isAssignableFrom(invoker.getInterface())){
    //...
}

注意第4个条件,一开始很疑惑,后来发现rpc报文中的path是目标接口的,这边invoker.getInterface()返回的肯定就是实际接口了

2. 方法参数提取
//从argument提取目标方法名 方法类型 方法参数
String name = ((String) inv.getArguments()[0]).trim();
String[] types = (String[]) inv.getArguments()[1];
Object[] args = (Object[]) inv.getArguments()[2];
3. 方法参数解析,进一步反序列化
//反射获取目标执行方法
Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
Class<?>[] params = method.getParameterTypes();
if (args == null) {
    args = new Object[params.length];
}
String generic = inv.getAttachment(Constants.GENERIC_KEY);

if (StringUtils.isBlank(generic)) {
    generic = RpcContext.getContext().getAttachment(Constants.GENERIC_KEY);
}

//一些反序列化
if (StringUtils.isEmpty(generic)
    || ProtocolUtils.isDefaultGenericSerialization(generic)) {
    args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
} else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
    for (int i = 0; i < args.length; i++) {
        if (byte[].class == args[i].getClass()) {
            try {
                UnsafeByteArrayInputStream is = new UnsafeByteArrayInputStream((byte[]) args[i]);
                args[i] = ExtensionLoader.getExtensionLoader(Serialization.class)
                    .getExtension(Constants.GENERIC_SERIALIZATION_NATIVE_JAVA)
                    .deserialize(null, is).readObject();
            } catch (Exception e) {
                throw new RpcException("Deserialize argument [" + (i + 1) + "] failed.", e);
            }
        } else {
            throw new RpcException(
                "Generic serialization [" +
                Constants.GENERIC_SERIALIZATION_NATIVE_JAVA +
                "] only support message type " +
                byte[].class +
                " and your message type is " +
                args[i].getClass());
        }
    }
} else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
    for (int i = 0; i < args.length; i++) {
        if (args[i] instanceof JavaBeanDescriptor) {
            args[i] = JavaBeanSerializeUtil.deserialize((JavaBeanDescriptor) args[i]);
        } else {
            throw new RpcException(
                "Generic serialization [" +
                Constants.GENERIC_SERIALIZATION_BEAN +
                "] only support message type " +
                JavaBeanDescriptor.class.getName() +
                " and your message type is " +
                args[i].getClass().getName());
        }
    }
}

这边有个疑问,为什么这边还要再次反序列化一次,netty不是有decoder么??

嗯,你别忘了,针对一个POJO你传过来是一个Map,从Map转换为POJO需要这边进一步处理。

4. 调用目标服务
Result result = invoker.invoke(new RpcInvocation(method, args, inv.getAttachments()));

这边的invoker就是实际服务提供者的invoker,因为我们的path是正确的,invoker获取在DubboProtocl的requestHandler回调中

5. 异常处理
if (result.hasException()
    && !(result.getException() instanceof GenericException)) {
    return new RpcResult(new GenericException(result.getException()));
}

这边需要注意一下!!针对接口的泛化调用,抛出的异常都会经过GenericException包装一下。

总结

从功能上来看,泛化调用提供了在没有接口依赖情况下进行的解决方案,丰富框架的使用场景。
从设计上来看,泛化调用的功能还是通过扩展的方式实现的,侵入性不强,值得学习借鉴。

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