创意电子

标题: Dubbo异常处置惩罚源码探究及其最佳实践 [打印本页]

作者: 程序猿阿嘴    时间: 2021-8-30 13:02
标题: Dubbo异常处置惩罚源码探究及其最佳实践
原文链接:https://mp.weixin.qq.com/s/-i9GIbf35Q9n_inSLhMorg
1 背景

在日常业务开发过程中,我们为了让业务代码更坚固,碰到错误时返回的提示更友好,一般会自定义一些业务异常。根据业务需要,分为自定义受检异常和非受检异常

知识点回首

Exception类及其子类,但不包括 RuntimeException 的子类,统称为受检异常。如果方法执行过程中有可能抛出此类异常,必须在方法签名上声明

RuntimeException 类及其子类,统称为非受检异常。如果方法执行过程中有可能抛出此类异常,可以不必在方法签名上声明

课代表所负责的项目使用SpringCloudAlibaba落地了微服务,开发中组内兄弟碰到一个问题:Dubbo RPC调用时,provider抛出的一个业务类非受检异常,consumer接到时却是RuntimeException 并且message被和堆栈信息拼接到了一起。

2 问题复现

Dubbo 微服务中,provider 分为apiserviceconsumer只需要引入 api从注册中心调用service 实例即可。

service 中抛出一个自定义的非受检异常,且其相应api包中没有这个异常类时,就会出现异常被包装为RuntimeException的情况。

其实问题分析到这里,基本就有端倪了:Dubbo是一个RPC框架,客户端调用的都是远程方法,参数和返回值都是经过序列化和反序列化为字节数组传输的。consumer必须认识这个异常才能反序列化成功。

很显着,我们抛的这个异常 Dubbo认为consumer不认识,为了避免反序列化失败,从而对异常举行了包装。

下面结合源码阐述Dubbo的异常处置惩罚机制。

3 源码分析

Dubbo 远程调用的异常由ExceptionFilter类处置惩罚

public Result invoke(Invoker invoker, Invocation invocation) throws RpcException {        try {            Result result = invoker.invoke(invocation);            if (result.hasException() && GenericService.class != invoker.getInterface()) {                try {                    Throwable exception = result.getException();                    // 如果是checked异常,直接抛出                    if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) {                        return result;                    }                    // 在方法签名上有声明,直接抛出                    try {                        Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());                        Class[] exceptionClassses = method.getExceptionTypes();                        for (Class exceptionClass : exceptionClassses) {                            if (exception.getClass().equals(exceptionClass)) {                                return result;                            }                        }                    } catch (NoSuchMethodException e) {                        return result;                    }                    // 未在方法签名上定义的异常,在服务器端打印ERROR日志                    logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()                            + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()                            + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);                    // 异常类和接口类在同一jar包里,直接抛出                    String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());                    String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());                    if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){                        return result;                    }                    // 是JDK自带的异常,直接抛出                    String className = exception.getClass().getName();                    if (className.startsWith("java.") || className.startsWith("javax.")) {                        return result;                    }                    // 是Dubbo本身的异常,直接抛出                    if (exception instanceof RpcException) {                        return result;                    }                    // 否则,包装成RuntimeException抛给客户端                    return new RpcResult(new RuntimeException(StringUtils.toString(exception)));                } catch (Throwable e) {                    logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost()                            + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()                            + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);                    return result;                }            }            return result;        } catch (RuntimeException e) {            logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()                    + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()                    + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);            throw e;        }
通过源码可以看到,该类的主要功能是返回接口抛出的异常,Dubbo将其定义为如下几种情况:

究竟上Dubbo作为RPC框架已经把各种抛异常的情况都考虑全了,末了如果Dubbo认为consumer不认识这个异常还会包装成RuntimeException兜底,防止反序列化失败。

如果发生了consumer找不到provider所抛异常的这种情况,不客气地讲,一定是开发者的问题,把这个归罪于Dubbo 那可就太冤枉它了!

4 最佳实践

Dubbo官网->Dubbo 2.7->用户文档->服务化最佳实践 中有如下描述:

分包
建议将服务接口、服务模型、服务异常等均放在 API 包中,因为服务模型和异常也是 API 的一部分,这样做也符合分包原则:重用发布等价原则(REP),共同重用原则(CRP)。

所以,符合Dubbo 最佳实践的provider-api中应该包含服务接口包,服务模型包,服务异常包。所有service中用到的异常,都应该在api包中声明,这样consumer调用时才会符合Dubbo 要求的:

异常类和接口类在同一个jar包里,直接抛出

从而避免被Dubbo 包装成RuntimeException抛给客户端。

所以,针对文章开头碰到的问题,我们只需要把provider-service中抛出自定义的非受检异常 在provider-api中定义,同时在相应的方法上throw出来就可以了,这样既可以防止被Dubbo包装,也不会因为方法签名中没声明异常而导致Dubboerror错误。而且,因为是非受检异常,所以也不强制客户端对方法举行try catch

一个可参考的分包实践:

  +- scr      |      +- demo          |          +- domain (业务域内传输数据用的 DTO)          |          +- service (API 中 service 接口的实现类)          |          +- exception (业务域中的自定义异常)5 弯路

如果 Google 关键字 [Dubbo 异常处置惩罚],你会发现几乎所有文章都是下面这几个思路:

当然,上面这些方法完全可以解决问题,但这是不是有杀鸡用牛刀的意思?

明明是代码开发不规范,没有遵循最佳实践,却要强行归罪于底层框架。Dubbo在努力做得通用,而上面的处置惩罚方式却在让代码变得紧耦合。

总结问题本质:Dubbo在认为consumer找不到异常类时,为了防止发生反序列化失败,对异常举行了一层包装。针对这一实质,我们用最简朴、高效,影响最小的办法解决就可以了。

课代表信赖读者结合Dubbo 异常处置惩罚的源码,应该会有本身的判断。

6 反思

遇事不决问Google,多数情况下我们碰到的问题都会搜到答案,对于同样一个问题,解决的方法可能多种多样,我们需要做的是找到问题的本质,举一反三,根据本身业务的现实情况选择最符合的解决方案。

切勿盲从,须知:尽信书不如无书。





欢迎光临 创意电子 (https://wxcydz.cc/) Powered by Discuz! X3.4