起因
最近想了解下dubbo一些功能,于是就翻阅dubbo官方文档,并写写demo练习练习。在练习到:用法示例->本地伪装 ,发现了一个小问题,问题就是在执行接口异步调用时,提供者挂了从而执行mock实现类相应的方法,出现方法实际返回对象类型和原方法返回对象类型不一致的情况
在这里就写了mock的service接口,然后对相应的几个接口进行了测试,启动provider和consumer服务,再关闭provider服务测试mock接口,本以为测试结果很完美,没想到在调用到asyncSayHello方法时就出问题了
DemoController.java
@GetMapping("/asnycHello")
public Object asnycHello(@RequestParam("name")String name){
RpcContext.getContext().setAttachment("consumer-key","just do it");
CompletableFuture<String> future = demoService.asyncSayHello(name);
logger.info("future task run out before");
String content = null;
try {
content = future.get();
logger.info("future task run out,content:{}",content);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return content;
}
DemoServiceImpl.java
@Override
public CompletableFuture<String> asyncSayHello(String name) {
RpcContext savedContext = RpcContext.getContext();
// 建议为supplyAsync提供自定义线程池,避免使用JDK公用线程池
return CompletableFuture.supplyAsync(() -> {
System.out.println(savedContext.getAttachment("consumer-key"));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "async response from provider.";
}, executor);
}
DemoServiceMock.java
@Override
public CompletableFuture<String> asyncSayHello(String name) {
logger.error("DemoService provider is useless,asyncSayHello method use mock");
return CompletableFuture.supplyAsync(() -> "");
}
方法asyncSayHello执行服务降级方法时出现异常,异常的返回结果:
看到这我就纳闷了,mock接口明明返回的是CompletableFuture对象,为什么这里会出现类型转换异常呢?难道是dubbo的bug?然后鉴于自己的求知欲就看了看源码,结果好像并不是bug,靓仔落泪哇~
问题排查
好了,不扯皮了,来看看是啥原因导致的:
1.正常逻辑调用
第一部分:断点调试环节,provider和comsumer服务都正常情况下接口的调用逻辑,DemoController##asyncSayHello->DemoService##asyncSayHello->InvokerInvocationHandler##invoke(接口调用的代理类),包装了一个RpcInvocation类,可以查看invoke执行后的recreate()方法
执行到AsyncRpcResult的recreate()方法中(下图),可以看到如果是异步调用,也就是等于InvokeModel.Future则会返回一个Futrue对象(这里和线程池的submit方法传入带有返回值的Callable对象然后返回Future对象类似,都是异步执行再取返回值,至于这invokeModel枚举类对象什么时候set进RpcInvocation里去的稍后会解释)
反之则会调用AppResponse的recreate()方法直接返回远程调用返回的结果对象(下图)
第二部分:然后是从DemoController##asyncSayHello->DemoService##asyncSayHello->InvokerInvocationHandler##invoke->MockClusterInvoker##invoke,可以看到在mock未设置 “force:” 参数时直接走的fail-mock默认条件,一个是正常invoke调用,一个是异常捕获后的方法调用,也就是降级后的doMockInvoke方法调用:
第三部分:正常invoke调用看下图可知道很长一条调用链路,各种invoker、wrapper、filter的invoke方法调用,然后到了AbstractInvoker这个类,可以看到这里设置了invokeModel属性值
下图invocation的returnType是CompletableFuture对象,所以isReturnTypeFuture(inv)返回true,返回InvokeMode.FUTURE枚举类对象
第四部分:继续接着看调用结果是怎么拿到的,可以看到第三部分第一张图在设置了invokeModel属性后执行了 doInvoke(invocation)即执行真正的远程方法调用,方法内(下图)可以看到断点到了这里用了一个ExchangeClient调用类进行接口调用返回结果CompletableFuture,设置到FutureContext中,并将结果包装到AsyncRpcResult中返回,至此整个调用链路执行完成
还记得第一部分的第二张图吗,AsyncRpcResult的recreate()根据invokeModel来判断是否是异步调用,是则调用RpcContext.getContext().getFuture(),RpcContext.getContext().getFuture()内部代码就是取的上图FutureContext中设置的appResponseFuture
2.异常调用逻辑
接着来看看异常调用逻辑,provider服务挂了,直接调用了服务降级的方法,
到了这一步开始执行真正的接口方法调用,执行doInvoke(proxy,...)
可以看到调用了代理方法后调用到了mock接口里面,返回了CompletableFuture对象
在执行了doInvoke(proxy,...)后返回了上图的CompletableFuture(也就是下图的value对象),包装下value对象,将包装后的future通过handle方法转换成AppResponse,将future的返回结果obj(上图内部类方法返回的String对象->"")设置到AppResponse的value里面,最后将转换完成的appResponseFuture设置到AsyncRpcResult中
可以看到整个调用链路invocation都没有设置invokeModel属性,所以走的是同步的调用逻辑,即getAppResponse().recreate()(见下图),而getAppResponse()拿到的是CompletableFuture,它的get()方法拿到的是AppResponse,AppResponse.recreate()则返回里面的result值,即上图AppResponse的setValue方法设置的值,所以最终返回的不是mock接口里return的CompletableFuture对象,而是mock接口里CompletableFuture.supplyAsync(() -> "")里面返回的这个空字符串的String对象,所以最终Controller中接口调用出现类型转换异常
3.结论
dubbo通过CompletableFuture进行接口异步调用在服务降级后返回值会出现与方法返回值类型不一致的问题,非异步调用不会出现此情况,若为了不出现类型转换异常,在降级的方法中可直接返回null,再在Controller代码中进行空判断即可。
花了一天多时间才把主要的逻辑梳理了出来,不记录下真对不起自己花费的时间,当然有很多源码都是一笔带过,没有分析,自己能力有限只能把大致流程梳理出来,想了解的同学可以断点调试仔细阅读阅读。最后感谢大家的阅读,有什么分析不对的地方欢迎指正(*^__^*)