资讯专栏INFORMATION COLUMN

Dubbo 自定义异常,你是怎么处理的?

dingding199389 / 3602人阅读

摘要:前言记录对于自定义异常的处理方式实现目标服务层异常,直接向上层抛出,层统一捕获处理如果是系统自定义异常,则返回其中对应为错误码,对应为异常信息如果非系统自定义异常,返回未知错误,同时将异常堆栈信息输出到日志便于定位问题项目架构先来张系统架

前言

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

实现目标

服务层异常,直接向上层抛出,web层统一捕获处理

如果是系统自定义异常,则返回{"code":xxx,"msg":yyy} 其中code对应为错误码msg对应为异常信息

如果非系统自定义异常,返回{"code":-1,"msg":"未知错误"},同时将异常堆栈信息输出到日志,便于定位问题

项目架构

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

简要说明下分层架构:

通常情况下会有专门一台堡垒机做统一的代理转发,客户端(pc,移动端等)访问由nginx统一暴露的入口

nginx反向代理,负载均衡到web服务器,由tomcat组成的集群,web层仅仅是作为接口请求的入口,没有实际的业务逻辑

web层再用rpc远程调用注册到zookeeperdubbo服务集群,dubbo服务与数据层交互,处理业务逻辑

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

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

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

/**
 * @program: easywits
 * @description: http请求 返回的最外层对象
 * @author: zhangshaolin
 * @create: 2018-04-27 10:43
 **/
@Data
@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
public class BaseResult implements Serializable{

    private static final long serialVersionUID = -6959952431964699958L;

    /**
     * 状态码:200成功,其他为失败
     */
    public Integer code;

    /**
     * 成功为success,其他为失败原因
     */
    public String msg;

    /**
     * 具体的内容
     */
    public T data;
}

返回结果工具类封装:

/**
 * @program: easywits
 * @description: http返回结果工具类
 * @author: zhangshaolin
 * @create: 2018-07-14 13:38
 **/
public class ResultUtil {

    /**
     * 访问成功时调用 包含data
     * @param object
     * @return
     */
    public static BaseResult success(Object object){
        BaseResult result = new BaseResult();
        result.setCode(200);
        result.setMsg("success");
        result.setData(object);
        return result;
    }

    /**
     * 访问成功时调用 不包含data
     * @return
     */
    public static BaseResult success(){
        return success(null);
    }

    /**
     * 返回异常情况 不包含data
     * @param code
     * @param msg
     * @return
     */
    public static BaseResult error(Integer code,String msg){
        BaseResult result = new BaseResult();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }
    
     /**
     * 返回异常情况 包含data
     * @param resultEnum 结果枚举类 统一管理 code msg
     * @param object
     * @return
     */
    public static BaseResult error(ResultEnum resultEnum,Object object){
        BaseResult result = error(resultEnum);
        result.setData(object);
        return result;
    }

    /**
     * 全局基类自定义异常 异常处理
     * @param e
     * @return
     */
    public static BaseResult error(BaseException e){
        return error(e.getCode(),e.getMessage());
    }

    /**
     * 返回异常情况 不包含data
     * @param resultEnum 结果枚举类 统一管理 code msg
     * @return
     */
    public static BaseResult error(ResultEnum resultEnum){
        return error(resultEnum.getCode(),resultEnum.getMsg());
    }
}

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

web层接口

@RestController
@RequestMapping(value = "/user")
public class UserController {
    @Autowired
    UserService mUserService;
    @Loggable(descp = "用户个人资料", include = "")
    @GetMapping(value = "/info")
    public BaseResult userInfo() {
        return mUserService.userInfo();
    }
}

服务层接口

 @Override
public BaseResult userInfo() {
    UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
    UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId());
    return ResultUtil.success(userInfoVo);
}

自定义系统异常

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

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

    private Integer code;

    public BaseException() {
    }

    public BaseException(ResultEnum resultEnum) {
        super(resultEnum.getMsg());
        this.code = resultEnum.getCode();
    }
    ...省略set get方法
}

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

public enum ResultEnum {
    UNKNOWN_ERROR(-1, "o(╥﹏╥)o~~系统出异常啦!,请联系管理员!!!"),
    SUCCESS(200, "success");
    
    private Integer code;
    
    private String msg;

    ResultEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}
web层统一捕获异常

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

public abstract class BaseController {
    private final static Logger LOGGER = LoggerFactory.getLogger(BaseController.class);
    
       /**
     * 统一异常处理
     *
     * @param e
     */
    @ExceptionHandler()
    public Object exceptionHandler(Exception e) {
        if (e instanceof BaseException) {
            //全局基类自定义异常,返回{code,msg}
            BaseException baseException = (BaseException) e;
            return ResultUtil.error(baseException);
        } else {
            LOGGER.error("系统异常: {}", e);
            return ResultUtil.error(ResultEnum.UNKNOWN_ERROR);
        }
    }
}
验证

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

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

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

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

Dubbo异常处理

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

 @Override
    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();

                    // directly throw if it"s checked exception
                    // 如果是checked异常,直接抛出
                    if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
                        return result;
                    }
                    // directly throw if the exception appears in the signature
                    // 在方法签名上有声明,直接抛出
                    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 日志
                    // for the exception not found in method"s signature, print ERROR message in server"s log.
                    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 包里,直接抛出
                    // directly throw if exception class and interface class are in the same jar file.
                    String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
                    String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
                    if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
                        return result;
                    }
                    // 是JDK自带的异常,直接抛出
                    // directly throw if it"s JDK exception
                    String className = exception.getClass().getName();
                    if (className.startsWith("java.") || className.startsWith("javax.")) {
                        return result;
                    }
                    // 是Dubbo本身的异常,直接抛出
                    // directly throw if it"s dubbo exception
                    if (exception instanceof RpcException) {
                        return result;
                    }

                    // 否则,包装成RuntimeException抛给客户端
                    // otherwise, wrap with RuntimeException and throw back to the client
                    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;
        }
    }

简要说明:

有异常,并且非泛化调用时,如果是受检异常,则直接抛出

有异常,并且非泛化调用时,在方法签名上有声明,则直接抛出

有异常,并且非泛化调用时,异常类和接口类在同一 jar 包里,则直接抛出

有异常,并且非泛化调用时,是Dubbo本身的异常(RpcException),则直接抛出

有异常,并且非泛化调用时,剩下的情况,全部都会包装成RuntimeException抛给客户端

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

解决办法

异常类BaseException和接口类在同一 jar 包里,但是这种方式要在每个jar中放置一个异常类,不好统一维护管理

在接口方法签名上显式声明抛出BaseException,这种方式相对简单一些,比较好统一维护,只是每个接口都要显式声明一下异常罢了,这里我选择这种方式解决

问题

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

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

@Transactional(rollbackFor = Exception.class)
public BaseResult handleData(){
    
    //1. 操作数据库,新增数据表A一条数据,返回新增数据主键id
    
    //2. 操作数据库,新增数据库B一条数据,以数据表A主键id为外键关联
    
    //3. 执行成功 返回结果
}

该接口声明了异常事物回滚,发送异常时会全部回滚

步骤1数据入库失败,理论上是拿不到主键id的,此时应当抛出自定义异常,提示操作失败

如果步骤1数据入库成功,步骤2中数据入库失败,那么理论上步骤1中的数据应当也要回滚,如果此时强制返回异常结果,那么步骤1入库数据则成为脏数据,此时抛出自定义异常是最合理的

最后的思考

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72830.html

相关文章

  • 前后端分离应用——用户信息传递

    摘要:前言记录前后端分离的系统应用下应用场景用户信息传递需求缘起照例先看看系统的一张经典架构图,这张图参考自网络在自定义异常,你是怎么处理的中已经对该架构做了简单说明,这里不再描述。 showImg(https://segmentfault.com/img/remote/1460000017839927?w=1024&h=768); 前言 记录前后端分离的系统应用下应用场景————用户信息传...

    PAMPANG 评论0 收藏0
  • 浏览器如何解析html、css、js

    摘要:全局变量局部变量全局函数一段也是一块域。此时打印的自然是,要记住相当于,所以这时候改变的是局部变量,并没有影响到全局变量,所以第二次打印的依然是。 在熟悉了浏览器的工作原理之后,今天我们来讲讲浏览器在从服务器获取到网页文件之后是如何解析的。了解了这个基础知识,对敲出来的代码,质量会有不小的提升。 一、浏览器如何解析html html文件在没有写入html标签之前和txt文本是一个性质的...

    Awbeci 评论0 收藏0
  • 浏览器如何解析html、css、js

    摘要:全局变量局部变量全局函数一段也是一块域。此时打印的自然是,要记住相当于,所以这时候改变的是局部变量,并没有影响到全局变量,所以第二次打印的依然是。 在熟悉了浏览器的工作原理之后,今天我们来讲讲浏览器在从服务器获取到网页文件之后是如何解析的。了解了这个基础知识,对敲出来的代码,质量会有不小的提升。 一、浏览器如何解析html html文件在没有写入html标签之前和txt文本是一个性质的...

    Panda 评论0 收藏0
  • 浏览器如何解析html、css、js

    摘要:全局变量局部变量全局函数一段也是一块域。此时打印的自然是,要记住相当于,所以这时候改变的是局部变量,并没有影响到全局变量,所以第二次打印的依然是。 在熟悉了浏览器的工作原理之后,今天我们来讲讲浏览器在从服务器获取到网页文件之后是如何解析的。了解了这个基础知识,对敲出来的代码,质量会有不小的提升。 一、浏览器如何解析html html文件在没有写入html标签之前和txt文本是一个性质的...

    Salamander 评论0 收藏0
  • 深度学习研究综述

    摘要:此原因在一定程度上阻碍了深度学习的发展,并将大多数机器学习和信号处理研究,从神经网络转移到相对较容易训练的浅层学习结构。深度学习算法可以看成核机器学习中一个优越的特征表示方法。 摘要:深度学习是一类新兴的多层神经网络学习算法。因其缓解了传统训练算法的局部最小性, 引起机器学习领域的广泛关注。首先论述了深度学习兴起渊源, 分析了算法的优越性, 并介绍了主流学习算法及应用现状,最后总结当前存在的...

    jokester 评论0 收藏0

发表评论

0条评论

dingding199389

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<