Dubbo 自定义异常,你是怎么处理的?
前言
记录Dubbo对于自定义异常的处理方式.
实现目标
- 服务层异常,直接向上层抛出,web层统一捕获处理
- 如果是系统自定义异常,则返回{"code":xxx,"msg":yyy}其中code对应为错误码,msg对应为异常信息
- 如果非系统自定义异常,返回{"code":-1,"msg":"未知错误"},同时将异常堆栈信息输出到日志,便于定位问题
项目架构
先来张系统架构图吧,这张图来源自网络,相信现在大部分中小企业的分布式集群架构都是类似这样的设计:
简要说明下分层架构:
- 通常情况下会有专门一台堡垒机做统一的代理转发,客户端(pc,移动端等)访问由nginx统一暴露的入口
- nginx反向代理,负载均衡到- web服务器,由- tomcat组成的集群,- web层仅仅是作为接口请求的入口,没有实际的业务逻辑
- web层再用- rpc远程调用注册到- zookeeper的- dubbo服务集群,- 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}
验证
- 以上 - web层接口- UserController继承- BaseController,统一捕获异常
- 服务层假设抛出自定义系统异常 - 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 自定义异常,你是怎么处理的?的更多相关文章
- dubbo自定义异常传递信息丢失问题解决
		访问我的博客 目前计划对已有的单体项目进行组织架构拆分,调研了分布式系统中常用中间件 Dubbo 和 Spring Cloud,选择了 Dubbo,可以对当前现有项目进行平滑升级改造.但是一开始就遇到 ... 
- 面试官问我,使用Dubbo有没有遇到一些坑?我笑了。
		前言 17年的时候,因为一时冲动没把持住(当然最近也有粉丝叫我再冲动一把再更新一波),结合面试题写了一个系列的Dubbo源码解析.目前公众号大部分粉丝都是之前的粉丝,这里不过多介绍. 根据我的面试经验 ... 
- 面试官问我,使用Dubbo有没有遇到一些坑?我笑了
		17年的时候,因为一时冲动没把持住(当然最近也有粉丝叫我再冲动一把再更新一波),结合面试题写了一个系列的Dubbo源码解析.目前公众号大部分粉丝都是之前的粉丝,这里不过多介绍. 根据我的面试经验而言, ... 
- 13.1 dubbo服务降级源码解析
		从 9.1 客户端发起请求源码 的客户端请求总体流程图中,截取部分如下: //代理发出请求 proxy0.sayHello(String paramString) -->InvokerInvoc ... 
- Dubbo封装rest服务返回结果
		由于Dubbo服务考虑到一个是给其他系统通过RPC调用,另外一个是提供HTTP协议本身系统的后台管理页面,因此Dubbo返回参数在rest返回的时候配置拦截器进行处理. 在拦截器中,对返回参数封装成如 ... 
- dubbo异常处理
		dubbo异常处理 我们的项目使用了dubbo进行不同系统之间的调用. 每个项目都有一个全局的异常处理,对于业务异常,我们会抛出自定义的业务异常(继承RuntimeException). 全局的异常处 ... 
- 用dubbo时遇到的一个序列化的坑
		首先,这是标题党,问题并不是出现在序列化上,这是报错的一部分: Caused by: com.alibaba.dubbo.remoting.RemotingException: Failed to s ... 
- dubbo服务提供与消费
		一.前言 项目中用到了Dubbo,临时抱大腿,学习了dubbo的简单实用方法.现在就来总结一下dubbo如何提供服务,如何消费服务,并做了一个简单的demo作为参考. 二.Dubbo是什么 Dubbo ... 
- 分布式学习系列【dubbo入门实践】
		分布式学习系列[dubbo入门实践] dubbo架构 组成部分:provider,consumer,registry,monitor: provider,consumer注册,订阅类似于消息队列的注册 ... 
随机推荐
- 图片放大镜——jQuery插件Cloud Zoom
			下载地址:cloud_zoom.rar 图片放大镜效果是一种不错的效果,多应用于电子商务.图片展示等网站,给用户带来更好的体验.实现这种效果的代码不少,今天要给大家介绍的是 Cloud Zoom,它是 ... 
- SVG与HTML、JavaScript的三种调用方式
			一.在HTMl中访问SVG的DOM Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHig ... 
- 使用mybatis提供的各种标签方法实现动态拼接Sql。使用sql片段提取重复的标签内容
			Sql中可将重复的sql提取出来,使用时用include引用即可,最终达到sql重用的目的,如下: <select id="findUserByNameAndSex" par ... 
- C语言字符编码处理
			一.字符编码识别 1.简介 uchardet是一个开源的用于文本编码检测的C语言库,其功能模块是用C++实现的,通过一定数量的字符样本独立的分析出文本的编码,当前已经支持UTF-8/GB13080/B ... 
- java关于split分割字符串,空的字符串不能得到的问题
			java关于split分割字符串,空的字符串不能得到的问题 class T { public static void main(String args[]) { String num[] = ne ... 
- o7 文件和函数
			一:文件 1 控制文件内指针的移动 文件内指针移动,只有在t模式下的read(n),n代表的字符的个数 除此之外文件内指针的移动都是以字节为单位的 with open('a.txt',mode ='r ... 
- 关于int转char类型引发的一些思考
			signed char unsigned char 
- IntelliJ IDEA建立source同级的文件夹
			1.项目中一般都是将配置文档放入到config的source文件夹下,但是IDE没有直接建立source文件夹的方式,所以我们只做文件夹需要如下操作: 选中项目--->右键,选择new ---& ... 
- [转]ASP.NET Web API对OData的支持
			http://www.cnblogs.com/shanyou/archive/2013/06/11/3131583.html 在SOA的世界中,最重要的一个概念就是契约(contract).在云计算的 ... 
- Linq编译带来的诡异错误
			今天遇到一个很诡异的问题,初步猜测是Linq编译以及编译器自动优化带来的原因,对IL不是很熟悉,所以懒得去追了. 贴个代码出来,希望能抛砖引玉,得到正解. 注意到我用原始的foreach语句代替了li ... 
