dubbo-泛化是怎么实现的

2,432 阅读2分钟

背景

一般我们在使用dubbo进行服务调用时,都会将服务提供者的api进行打jar包提供给消费者,这样消费者可以直接引用,但是考虑到如下两种情况。

  • 如果dubbo的使用者不是java客户端?
  • 服务如果具有网关层,网关转发请求到后端如果依赖后端jar,网关发布频繁,稳定性怎么保证? 为了解决上面的问题,dubbo设计了泛化能力,简单来说就是客户端可以不依赖生产者api。

使用介绍

我们先介绍个简单的例子,之后再来说下原理和实现。

生产者配置

//xml
<dubbo:service interface="com.poizon.study.api.service.HelloService" ref="helloService"/>
//服务实现类
@Service
public class HelloServiceImpl implements HelloService {
    @Override
    public WishRequest sayHelloToDubbo(String name) throws IOException {
        WishRequest wishRequest = new WishRequest();
        wishRequest.setMoney(1L);
        wishRequest.setMsg("hello to dubbo");
        return wishRequest;
    }
    @Override
    public String sayHappyNewYear(WishRequest wish) { return "thank you!"; }
}

服务消费者

//xml
<dubbo:reference generic="true" id="helloService" interface="com.poizon.study.api.service.HelloService" />

public static void main(String[] args) throws IOException, InterruptedException {        
  ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("dubbo-consumer.xml");
  ctx.start();
  GenericService helloService = ctx.getBean("helloService", GenericService.class);
  Object sayHelloToDubbo = helloService.$invoke("sayHelloToDubbo", new String[]{"java.lang.String"}, new Object[]{"jack"});
  System.out.println(sayHelloToDubbo);
  //print:{msg=hello to dubbo, money=1, class=com.poizon.study.api.service.request.WishRequest, age=null}
}

消费者代码中xml的interface 配置的string 接口名称在消费端是不存在的如果安装了idea会报红,我们无需理会,这里唯一的区别在generic=true参数,main函数中将bean都转换为GenericService 对象,然后通过他的$invoke($invokeAsync) 去调用我们期望调用的方法名,第二个参数为参数列表,第三个参数为传参,自定义对象可以通过map的形式。

原理分析

调用成功了,dubbo实现泛化主要通过两个filter(之前文章有分析过),我们这边单独把这两个过滤器拿出来分析下。

  • GenericFilter(生产者侧)
  • GenericImplFilter(消费者侧配置了generic=true 才会加载)

GenericImplFilter

//org.apache.dubbo.rpc.filter.GenericImplFilter#invoke
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String generic = invoker.getUrl().getParameter(GENERIC_KEY);
		 if ((invocation.getMethodName().equals($INVOKE) || invocation.getMethodName().equals($INVOKE_ASYNC))
                && invocation.getArguments() != null
                && invocation.getArguments().length == 3
                && ProtocolUtils.isGeneric(generic)) {
            Object[] args = (Object[]) invocation.getArguments()[2];
            if (ProtocolUtils.isJavaGenericSerialization(generic)) {
                for (Object arg : args) {
                    if (!(byte[].class == arg.getClass())) {
                        error(generic, byte[].class.getName(), arg.getClass().getName());
                    }
                }
            } else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
                for (Object arg : args) {
                    if (!(arg instanceof JavaBeanDescriptor)) {
                        error(generic, JavaBeanDescriptor.class.getName(), arg.getClass().getName());
                    }
                }
            }
            invocation.setAttachment(
                    GENERIC_KEY, invoker.getUrl().getParameter(GENERIC_KEY));//1
        }
        return invoker.invoke(invocation);
    }

过滤器的作用主要是判断客服端序列化类型,注释1处将序列化标识通过attachment(dubbo附加信息参数) 传递给了生产者。

GenericFilter

//org.apache.dubbo.rpc.filter.GenericFilter#invoke
public Result invoke(Invoker<?> invoker, Invocation inv)  {
if ((inv.getMethodName().equals($INVOKE) || inv.getMethodName().equals($INVOKE_ASYNC))
        && inv.getArguments() != null
        && inv.getArguments().length == 3
        && !GenericService.class.isAssignableFrom(invoker.getInterface())) {//1
    String name = ((String) inv.getArguments()[0]).trim();
    String[] types = (String[]) inv.getArguments()[1];
    Object[] args = (Object[]) inv.getArguments()[2];
    try {
        Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
        Class<?>[] params = method.getParameterTypes();
        if (args == null) {
            args = new Object[params.length];
        }
        String generic = inv.getAttachment(GENERIC_KEY);

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

        if (StringUtils.isEmpty(generic)
                || ProtocolUtils.isDefaultGenericSerialization(generic)) {//2
            args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
        } else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
            //...
        } else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
            //...
        } else if (ProtocolUtils.isProtobufGenericSerialization(generic)) {
            //...
        }
        return invoker.invoke(new RpcInvocation(method, args, inv.getAttachments()));//3
    } catch (ClassNotFoundException e) {
        throw new RpcException(e.getMessage(), e);
    }
}
return invoker.invoke(inv);
}

消费者filter中,注释1判断入参方法名是否为$invoke 类名,参数个数等,然后通过这些参数创建出后续需要的method对象,注释2处是默认的序列化方式,注释3处创建新的RpcInvocation(此时方法名已经修改为我们调用$invoke传过来的第一个参数既 sayHelloToDubbo)对象作为调用链的参数。

思考?

我们将消费者调用的方法名改成一个实际不存在的名字 sayHelloToDubbo2

    public static void main(String[] args) throws IOException, InterruptedException {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("dubbo-consumer.xml");
        ctx.start();
        GenericService helloService = ctx.getBean("helloService", GenericService.class);
        Object sayHelloToDubbo = helloService.$invoke("sayHelloToDubbo2", new String[]{"java.lang.String"}, new Object[]{"jack"});
        System.out.println(sayHelloToDubbo);
Exception in thread "main" org.apache.dubbo.rpc.RpcException: 
Failed to invoke the method sayHelloToDubbo2 in the 
service org.apache.dubbo.rpc.service.GenericService. 
Tried 3 times of the providers [192.168.2.172:20880] (1/1) from the registry localhost:2181 on the consumer 192.168.2.172 using the dubbo version 2.7.3. Last error is: Failed to invoke remote method: $invoke, provider: dubbo://192.168.2.172:20880/com.poizon.study.api.service.HelloService?anyhost=true&application=dubbo-provider&bean.name=com.poizon.study.api.service.HelloService&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=true&interface=com.poizon.study.api.service.HelloService&lazy=false&logger=slf4j&methods=sayHappyNewYear,sayHelloToDubbo&pid=81910&register=true&register.ip=192.168.2.172&release=2.7.3&remote.application=dubbo-provider&side=consumer&sticky=false&timestamp=1612017946014, cause: org.apache.dubbo.remoting.RemotingException: org.apache.dubbo.rpc.RpcException: com.poizon.study.api.service.HelloService.sayHelloToDubbo2(java.lang.String)
org.apache.dubbo.rpc.RpcException: com.poizon.study.api.service.HelloService.sayHelloToDubbo2(java.lang.String)
	at org.apache.dubbo.rpc.filter.GenericFilter.invoke(GenericFilter.java:143)
	at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:82)
	at org.apache.dubbo.rpc.filter.ClassLoaderFilter.invoke(ClassLoaderFilter.java:38)
//....

报错是意料之中的,但是笔者想表达的是Tried 3 times 这个错误,那么消费者为什么要调用三次呢?大家可以在idea中验证下,如果有兴趣把参数种类也换下比如用string 代替 integer 看看是否错误还是Tried 3 times,那么这种错误我们能否避免(可以考虑下RpcException.isBiz())?

总结

泛化目前在springboot + dubbo 生态中,网关使用还是很频繁的大家有必要深入学习。