前言

记录Dubbo对于自定义异常的处理方式.

实现目标

  • 服务层异常,直接向上层抛出,web层统一捕获处理
  • 如果是系统自定义异常,则返回{"code":xxx,"msg":yyy} 其中code对应为错误码msg对应为异常信息
  • 如果非系统自定义异常,返回{"code":-1,"msg":"未知错误"},同时将异常堆栈信息输出到日志,便于定位问题

项目架构

先来张系统架构图吧,这张图来源自网络,相信现在大部分中小企业的分布式集群架构都是类似这样的设计:

简要说明下分层架构:

  • 通常情况下会有专门一台堡垒机做统一的代理转发,客户端(pc,移动端等)访问由nginx统一暴露的入口
  • nginx反向代理,负载均衡到web服务器,由tomcat组成的集群,web层仅仅是作为接口请求的入口,没有实际的业务逻辑
  • web层再用rpc远程调用注册到zookeeperdubbo服务集群,dubbo服务与数据层交互,处理业务逻辑

前后端分离,使用json格式做数据交互,格式可以统一如下:

1    {
2        "code": 200,            //状态码:200成功,其他为失败
3        "msg": "success",       //消息,成功为success,其他为失败原因
4        "data": object     //具体的数据内容,可以为任意格式
5    }

映射为javabean可以统一定义为:

 1/**
2 * @program: easywits
3 * @description: http请求 返回的最外层对象
4 * @author: zhangshaolin
5 * @create: 2018-04-27 10:43
6 **/
7@Data
8@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
9public class BaseResult<T> implements Serializable{
10
11    private static final long serialVersionUID = -6959952431964699958L;
12
13    /**
14     * 状态码:200成功,其他为失败
15     */
16    public Integer code;
17
18    /**
19     * 成功为success,其他为失败原因
20     */
21    public String msg;
22
23    /**
24     * 具体的内容
25     */
26    public T data;
27}

返回结果工具类封装:

 1/**
2 * @program: easywits
3 * @description: http返回结果工具类
4 * @author: zhangshaolin
5 * @create: 2018-07-14 13:38
6 **/
7public class ResultUtil {
8
9    /**
10     * 访问成功时调用 包含data
11     * @param object
12     * @return
13     */
14    public static BaseResult success(Object object){
15        BaseResult result = new BaseResult();
16        result.setCode(200);
17        result.setMsg("success");
18        result.setData(object);
19        return result;
20    }
21
22    /**
23     * 访问成功时调用 不包含data
24     * @return
25     */
26    public static BaseResult success(){
27        return success(null);
28    }
29
30    /**
31     * 返回异常情况 不包含data
32     * @param code
33     * @param msg
34     * @return
35     */
36    public static BaseResult error(Integer code,String msg){
37        BaseResult result = new BaseResult();
38        result.setCode(code);
39        result.setMsg(msg);
40        return result;
41    }
42
43     /**
44     * 返回异常情况 包含data
45     * @param resultEnum 结果枚举类 统一管理 code msg
46     * @param object
47     * @return
48     */
49    public static BaseResult error(ResultEnum resultEnum,Object object){
50        BaseResult result = error(resultEnum);
51        result.setData(object);
52        return result;
53    }
54
55    /**
56     * 全局基类自定义异常 异常处理
57     * @param e
58     * @return
59     */
60    public static BaseResult error(BaseException e){
61        return error(e.getCode(),e.getMessage());
62    }
63
64    /**
65     * 返回异常情况 不包含data
66     * @param resultEnum 结果枚举类 统一管理 code msg
67     * @return
68     */
69    public static BaseResult error(ResultEnum resultEnum){
70        return error(resultEnum.getCode(),resultEnum.getMsg());
71    }
72}

因此,模拟一次前端调用请求的过程可以如下:

  • web层接口

     1@RestController
    2@RequestMapping(value = "/user")
    3public class UserController {
    4    @Autowired
    5    UserService mUserService;
    6    @Loggable(descp = "用户个人资料", include = "")
    7    @GetMapping(value = "/info")
    8    public BaseResult userInfo() {
    9        return mUserService.userInfo();
    10    }
    11}
  • 服务层接口

    1 @Override
    2public BaseResult userInfo() {
    3    UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
    4    UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId());
    5    return ResultUtil.success(userInfoVo);
    6}

自定义系统异常

定义一个自定义异常,用于手动抛出异常信息,注意这里基础RuntimeException未受检异常

简单说明,RuntimeException及其子类为未受检异常,其他异常为受检异常,未受检异常是运行时抛出的异常,而受检异常则在编译时则强则报错

 1public class BaseException extends RuntimeException{
2
3    private Integer code;
4
5    public BaseException() {
6    }
7
8    public BaseException(ResultEnum resultEnum) {
9        super(resultEnum.getMsg());
10        this.code = resultEnum.getCode();
11    }
12    ...省略set get方法
13}

为了方便对结果统一管理,定义一个结果枚举类:

 1public enum ResultEnum {
2    UNKNOWN_ERROR(-1, "o(╥﹏╥)o~~系统出异常啦!,请联系管理员!!!"),
3    SUCCESS(200, "success");
4
5    private Integer code;
6
7    private String msg;
8
9    ResultEnum(Integer code, String msg) {
10        this.code = code;
11        this.msg = msg;
12    }
13}

`web`层统一捕获异常

定义BaseController抽象类,统一捕获由服务层抛出的异常,所有新增Controller继承该类即可。

 1public abstract class BaseController {
2    private final static Logger LOGGER = LoggerFactory.getLogger(BaseController.class);
3
4       /**
5     * 统一异常处理
6     *
7     * @param e
8     */
9    @ExceptionHandler()
10    public Object exceptionHandler(Exception e) {
11        if (e instanceof BaseException) {
12            //全局基类自定义异常,返回{code,msg}
13            BaseException baseException = (BaseException) e;
14            return ResultUtil.error(baseException);
15        } else {
16            LOGGER.error("系统异常: {}", e);
17            return ResultUtil.error(ResultEnum.UNKNOWN_ERROR);
18        }
19    }
20}

验证

  1. 以上web层接口UserController继承BaseController,统一捕获异常

  2. 服务层假设抛出自定义系统异常BaseException,代码如下:

     1 @Override
    2 public BaseResult userInfo() {
    3    UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
    4    UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId());
    5      if (userInfoVo != null) {
    6        //这里假设抛个自定义异常,返回结果{code:10228 msg:"用户存在!"}
    7        throw new BaseException(ResultEnum.USER_EXIST);
    8    }
    9    return ResultUtil.success(userInfoVo);
    10}

然而调用结果后,上层捕获到的异常却不是BaseException,而被认为了未知错误抛出了.带着疑问看看Dubbo对于异常的处理

Dubbo异常处理

Dubbo对于异常有统一的拦截处理,以下是Dubbo异常拦截器主要代码:

 1 @Override
2    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
3        try {
4            // 服务调用
5            Result result = invoker.invoke(invocation);
6            // 有异常,并且非泛化调用
7            if (result.hasException() && GenericService.class != invoker.getInterface()) {
8                try {
9                    Throwable exception = result.getException();
10
11                    // directly throw if it's checked exception
12                    // 如果是checked异常,直接抛出
13                    if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
14                        return result;
15                    }
16                    // directly throw if the exception appears in the signature
17                    // 在方法签名上有声明,直接抛出
18                    try {
19                        Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
20                        Class<?>[] exceptionClassses = method.getExceptionTypes();
21                        for (Class<?> exceptionClass : exceptionClassses) {
22                            if (exception.getClass().equals(exceptionClass)) {
23                                return result;
24                            }
25                        }
26                    } catch (NoSuchMethodException e) {
27                        return result;
28                    }
29
30                    // 未在方法签名上定义的异常,在服务器端打印 ERROR 日志
31                    // for the exception not found in method's signature, print ERROR message in server's log.
32                    logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()
33                            + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
34                            + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);
35
36                    // 异常类和接口类在同一 jar 包里,直接抛出
37                    // directly throw if exception class and interface class are in the same jar file.
38                    String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
39                    String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
40                    if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
41                        return result;
42                    }
43                    // 是JDK自带的异常,直接抛出
44                    // directly throw if it's JDK exception
45                    String className = exception.getClass().getName();
46                    if (className.startsWith("java.") || className.startsWith("javax.")) {
47                        return result;
48                    }
49                    // 是Dubbo本身的异常,直接抛出
50                    // directly throw if it's dubbo exception
51                    if (exception instanceof RpcException) {
52                        return result;
53                    }
54
55                    // 否则,包装成RuntimeException抛给客户端
56                    // otherwise, wrap with RuntimeException and throw back to the client
57                    return new RpcResult(new RuntimeException(StringUtils.toString(exception)));
58                } catch (Throwable e) {
59                    logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost()
60                            + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
61                            + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
62                    return result;
63                }
64            }
65            // 返回
66            return result;
67        } catch (RuntimeException e) {
68            logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()
69                    + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
70                    + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
71            throw e;
72        }
73    }

简要说明:

  • 有异常,并且非泛化调用时,如果是受检异常,则直接抛出
  • 有异常,并且非泛化调用时,在方法签名上有声明,则直接抛出
  • 有异常,并且非泛化调用时,异常类和接口类在同一 jar 包里,则直接抛出
  • 有异常,并且非泛化调用时,是Dubbo本身的异常(RpcException),则直接抛出
  • 有异常,并且非泛化调用时,剩下的情况,全部都会包装成RuntimeException抛给客户端

到现在问题很明显了,我们自定义的BaseException未受检异常,况且不符合Dubbo异常拦截器中直接抛出的要求,Dubbo将其包装成了RuntimeException,所以在上层BaseController中统一捕获为系统未知错误了.

解决办法

  • 异常类BaseException和接口类在同一 jar 包里,但是这种方式要在每个jar中放置一个异常类,不好统一维护管理
  • 在接口方法签名上显式声明抛出BaseException,这种方式相对简单一些,比较好统一维护,只是每个接口都要显式声明一下异常罢了,这里我选择这种方式解决

问题

为什么一定要抛出自定义异常来中断程序运行,用return ResultUtil.error(ResultEnum resultEnum) 强制返回{code:xxx msg:xxx}结果,不是一样可以强制中断程序运行?

玩过Spring的肯定都知道,Spring哟声明式事物的概念,即在接口中添加事物注解,当发生异常时,全部接口执行事物回滚..看下方的伪代码:

1@Transactional(rollbackFor = Exception.class)
2public BaseResult handleData(){
3
4    //1. 操作数据库,新增数据表A一条数据,返回新增数据主键id
5
6    //2. 操作数据库,新增数据库B一条数据,以数据表A主键id为外键关联
7
8    //3. 执行成功 返回结果
9}
  • 该接口声明了异常事物回滚,发送异常时会全部回滚
  • 步骤1数据入库失败,理论上是拿不到主键id的,此时应当抛出自定义异常,提示操作失败
  • 如果步骤1数据入库成功,步骤2中数据入库失败,那么理论上步骤1中的数据应当也要回滚,如果此时强制返回异常结果,那么步骤1入库数据则成为脏数据,此时抛出自定义异常是最合理的

最后的思考

在实际问题场景中去阅读源码是最合适的,带着问题有目的的看指定源码会让人有豁然开朗的感觉.

更多原创文章会第一时间推送公众号【张少林同学】,欢迎关注!

Dubbo 自定义异常,你是怎么处理的?的更多相关文章

  1. dubbo自定义异常传递信息丢失问题解决

    访问我的博客 目前计划对已有的单体项目进行组织架构拆分,调研了分布式系统中常用中间件 Dubbo 和 Spring Cloud,选择了 Dubbo,可以对当前现有项目进行平滑升级改造.但是一开始就遇到 ...

  2. 面试官问我,使用Dubbo有没有遇到一些坑?我笑了。

    前言 17年的时候,因为一时冲动没把持住(当然最近也有粉丝叫我再冲动一把再更新一波),结合面试题写了一个系列的Dubbo源码解析.目前公众号大部分粉丝都是之前的粉丝,这里不过多介绍. 根据我的面试经验 ...

  3. 面试官问我,使用Dubbo有没有遇到一些坑?我笑了

    17年的时候,因为一时冲动没把持住(当然最近也有粉丝叫我再冲动一把再更新一波),结合面试题写了一个系列的Dubbo源码解析.目前公众号大部分粉丝都是之前的粉丝,这里不过多介绍. 根据我的面试经验而言, ...

  4. 13.1 dubbo服务降级源码解析

    从 9.1 客户端发起请求源码 的客户端请求总体流程图中,截取部分如下: //代理发出请求 proxy0.sayHello(String paramString) -->InvokerInvoc ...

  5. Dubbo封装rest服务返回结果

    由于Dubbo服务考虑到一个是给其他系统通过RPC调用,另外一个是提供HTTP协议本身系统的后台管理页面,因此Dubbo返回参数在rest返回的时候配置拦截器进行处理. 在拦截器中,对返回参数封装成如 ...

  6. dubbo异常处理

    dubbo异常处理 我们的项目使用了dubbo进行不同系统之间的调用. 每个项目都有一个全局的异常处理,对于业务异常,我们会抛出自定义的业务异常(继承RuntimeException). 全局的异常处 ...

  7. 用dubbo时遇到的一个序列化的坑

    首先,这是标题党,问题并不是出现在序列化上,这是报错的一部分: Caused by: com.alibaba.dubbo.remoting.RemotingException: Failed to s ...

  8. dubbo服务提供与消费

    一.前言 项目中用到了Dubbo,临时抱大腿,学习了dubbo的简单实用方法.现在就来总结一下dubbo如何提供服务,如何消费服务,并做了一个简单的demo作为参考. 二.Dubbo是什么 Dubbo ...

  9. 分布式学习系列【dubbo入门实践】

    分布式学习系列[dubbo入门实践] dubbo架构 组成部分:provider,consumer,registry,monitor: provider,consumer注册,订阅类似于消息队列的注册 ...

随机推荐

  1. uva10288 Coupons 【概率 分数】

    题目: 题意: 一共n种不同的礼券,每次得到每种礼券的概率相同.求期望多少次可以得到所有n种礼券.结果以带分数形式输出.1<= n <=33. 思路: 假设当前已经得到k种,获得新的一种的 ...

  2. 表示集合的数据结构:数组(Array),对象(Object),Map和Set

    Map和Set是ES6标准新增的数据类型 Map: 是一组键值对的结构,使用一个二维数组来初始化Map,例如: var m = new Map([['xiaohong',100],['xiaolan' ...

  3. 8-cin cout PK scanf printf(速度快慢问题对比)

    我们在c++ 中使用cin cout很方便但速度很慢,导致有些题目用cin就超时而用scanf则就ac了,那到底改用谁? cin慢是有原因的,其实默认的时候,cin与stdin总是保持同步的,也就是说 ...

  4. How to Restart Qt Application

    How to restart QtApplication As we know, Restarting Application means to exit current application, t ...

  5. mysql查询赋值、修改拼接字符串

    sql中修改字符串类型的字段可以这么拼接:update tbName set UserName='abc'+UserName; 但mysql中就不行了,需要这样:update tbName set U ...

  6. [转].NET 4.5+项目迁移.NET Core的问题记录 HTTP Error 502.5

    本文转自:http://www.cnblogs.com/ronli/p/5900001.html 这几天试着把目前的开发框架迁移到新的.net core平台,中间遇到的问题在这里简单记录一下. 迁移过 ...

  7. DataStage 七、在DS中使用配置文件分配资源

    DataStage序列文章 DataStage 一.安装 DataStage 二.InfoSphere Information Server进程的启动和停止 DataStage 三.配置ODBC Da ...

  8. Devexpress VCL Build v2013 vol 13.2.3 发布

    继续修修补补,大过年的,就不吐槽了. What's New in 13.2.3 (VCL Product Line)   New Major Features in 13.2 What's New i ...

  9. swift的可选值(optional)

    苹果那文档写了一大堆也没有好好的写一下可选值(optional)这个东西.就是在有一个“Optional Chaining”的章节,但是也不是很充分的说明.最后找了半天在“the basics”里墨迹 ...

  10. Linux 基础教程 36-查看系统性能

    uptime     uptime命令功能比较简单,主要功能如下所示: 查看服务器的开机时长 查看CPU负载 基本用法 uptime 用法示例 [root@localhost ~]# uptime 1 ...