前言

异常处理其实一直都是项目开发中的大头,但关注异常处理的人一直都特别少。经常是简单的 try/catch 所有异常,然后简单的 printStackTrace ,最多使用 logger 来打印下日志或者重新抛出异常,还有的已经有自定义异常了,但是还是在 controller 捕获异常,需要 catch(异常1 )catch(异常2) 特别繁琐,而且容易漏。

其实 springmvc 在 ControllerAdvice 已经提供了一种全局处理异常的方式,并且我们还可以使用 aop 来统一处理异常,这样在任何地方我们都只需要关注自己的业务,而不用关注异常处理,而且抛出异常还可以利用 spring 的事务,它只有在检测到异常才会事务回滚。

重要说明

  1. 下面的相关代码用到了 lombok ,不知道的可以百度下 lombok 的用途
  2. 使用建造者模式

统一异常处理

这里使用 springmvc 的 ControllerAdvice 来做统一异常处理

import com.sanri.test.testmvc.dto.ResultEntity;import com.sanri.test.testmvc.exception.BusinessException;import com.sanri.test.testmvc.exception.RemoteException;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.ArrayUtils;import org.springframework.beans.factory.annotation.Value;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;import java.util.ArrayList;import java.util.List;@RestControllerAdvice@Slf4jpublic class GlobalExceptionHandler {    @Value("${project.package.prefix:com.sanri.test}")    protected String packagePrefix;    /**     * 处理业务异常     * @param e     * @return     */    @ExceptionHandler(BusinessException.class)    public ResultEntity businessException(BusinessException e){        printLocalStackTrack(e);        return e.getResultEntity();    }    @ExceptionHandler(RemoteException.class)    public ResultEntity remoteException(RemoteException e){        ResultEntity parentResult = e.getParent().getResultEntity();        ResultEntity resultEntity = e.getResultEntity();        //返回给前端的是业务错误,但是需要在控制台把远程调用异常给打印出来        log.error(parentResult.getReturnCode()+":"+parentResult.getMessage()                +" \n -| "+resultEntity.getReturnCode()+":"+resultEntity.getMessage());        printLocalStackTrack(e);        //合并两个结果集返回        ResultEntity merge = ResultEntity.err(parentResult.getReturnCode())                .message(parentResult.getMessage()+" \n  |- "+resultEntity.getReturnCode()+":"+resultEntity.getMessage());        return merge;    }    /**     * 打印只涉及到项目类调用的异常堆栈     * @param e     */    private void printLocalStackTrack(BusinessException e) {        StackTraceElement[] stackTrace = e.getStackTrace();        List<StackTraceElement> localStackTrack = new ArrayList<>();        StringBuffer showMessage = new StringBuffer();        if (ArrayUtils.isNotEmpty(stackTrace)) {            for (StackTraceElement stackTraceElement : stackTrace) {                String className = stackTraceElement.getClassName();                int lineNumber = stackTraceElement.getLineNumber();                if (className.startsWith(packagePrefix)) {                    localStackTrack.add(stackTraceElement);                    showMessage.append(className + "(" + lineNumber + ")\n");                }            }            log.error("业务异常:" + e.getMessage() + "\n" + showMessage);        } else {            log.error("业务异常,没有调用栈 " + e.getMessage());        }    }    /**     * 异常处理,可以绑定多个     * @return     */    @ExceptionHandler(Exception.class)    public ResultEntity result(Exception e){        e.printStackTrace();        return ResultEntity.err(e.getMessage());    }}

统一返回值

  • 一般我们都会定义统一的返回,这样前端好做返回值的解析,像这样
package com.sanri.test.testmvc.dto;import lombok.Data;import lombok.ToString;import java.io.Serializable;/** * 普通消息返回 * @param <T> */@Data@ToStringpublic class ResultEntity<T> implements Serializable {    private String returnCode = "0";    private String message;    private T data;    public ResultEntity() {        this.message = "ok";    }    public ResultEntity(T data) {        this();        this.data = data;    }    public static ResultEntity ok() {        return new ResultEntity();    }    public static ResultEntity err(String returnCode) {        ResultEntity resultEntity = new ResultEntity();        resultEntity.returnCode = returnCode;        resultEntity.message = "fail";        return resultEntity;    }    public static ResultEntity err() {        return err("-1");    }    public ResultEntity message(String msg) {        this.message = msg;        return this;    }    public ResultEntity data(T data) {        this.data = data;        return this;    }}

自定义异常

  • 自定义异常,就我目前的工作经历来看的话,异常一般就三种 。

    1. 第一种是业务异常,即给定的输入不能满足业务条件,如时间过期、姓名重复、身份证不对啊等等
    2. 第二种是调用第三方系统时的异常,其实也属于业务异常
    3. 第三种是系统的致命错误,一般是出错了,但这个要在测试和开发阶段就需要处理好,线上出错只能是给用户说系统出错了,然后开发查日志来看错误。

业务异常

对于业务异常来说,我们有时候需要对错误进行编号,因为前端需要拿到编号来做一些页面跳转的工作,而且客户在投诉错误的时候也可以告诉运营编号,然后可以做应对的措施;但绝大部分的时候是不需要错误编号的,这时可以随机生成一个编号。我们可以定一个号段来定义错误编号,比如 0 定义为正常,1~100 为通用错误, 101 ~1000 是 A 系统,1000 ~ 2000 是 B 系统,然后 10000 以上是随机代码等。

import com.sanri.test.testmvc.dto.ResultEntity;import org.apache.commons.lang3.ArrayUtils;import org.apache.commons.lang3.ObjectUtils;import org.apache.commons.lang3.StringUtils;import java.util.ArrayList;import java.util.List;/** * 系统业务异常(根异常),异常号段为 : * 0 : 成功 * 1 ~ 9999 内定系统异常段 * 10000 ~ 99999 自定义异常码段 * 100000 ~ Integer.MAX_VALUE 动态异常码段 */public class BusinessException extends RuntimeException {    protected ResultEntity resultEntity;    protected static final int  MIN_AUTO_CODE = 100000;    public static BusinessException create(String message) {        int value= (int) (MIN_AUTO_CODE + Math.round((Integer.MAX_VALUE - MIN_AUTO_CODE) * Math.random()));        return create(value + "",message);    }    public static BusinessException create(String returnCode,String message){        if(StringUtils.isBlank(returnCode)){            return create(message);        }         BusinessException businessException = new BusinessException();         businessException.resultEntity = ResultEntity.err(returnCode).message(message);         return businessException;    }    public static BusinessException create(ExceptionCause exceptionCause ,Object...args){        ResultEntity resultEntity = exceptionCause.result();        String message = resultEntity.getMessage();        if(ArrayUtils.isNotEmpty(args)){            String [] argsStringArray = new String [args.length];            for (int i=0;i<args.length;i++) {                Object arg = args[i];                argsStringArray[i] = ObjectUtils.toString(arg);            }            String formatMessage = String.format(message, argsStringArray);            resultEntity.setMessage(formatMessage);        }        BusinessException businessException = new BusinessException();        businessException.resultEntity = resultEntity;        return businessException;    }    @Override    public String getMessage() {        return resultEntity.getMessage();    }    public ResultEntity getResultEntity() {        return resultEntity;    }}

远程调用异常

远程调用异常一般别人也会返回错误码和错误消息给我们,这时我们可以定义一个远程异常,把业务异常做为父级异常,这时候呈现的错误结构会是这样,举个例子

投保业务出错   -| E007 生效日期必须大于当前日期 

代码如下,用到了建造者设计模式,如不知道这个设计模式,可以自行百度

import com.sanri.test.testmvc.dto.ResultEntity;import com.sun.deploy.net.proxy.RemoveCommentReader;public class RemoteException  extends BusinessException{    private BusinessException parent;    private RemoteException(BusinessException parent) {        this.parent = parent;    }    /**     * 创建远程异常     * @param parent     * @param remoteCode     * @param remoteMessage     * @return     */    public static RemoteException create(BusinessException parent,String remoteCode,String remoteMessage){        RemoteException remoteException = new RemoteException(parent);        remoteException.resultEntity = ResultEntity.err(remoteCode).message(remoteMessage);        return remoteException;    }    /**     * 简易创建远程信息     * @param parent     * @param remoteMessage     * @return     */    public static RemoteException create(BusinessException parent,String remoteMessage){        return create(parent,"remoteError",remoteMessage);    }    public static RemoteException create(String localMessage,String remoteCode,String remoteMessage){        return new Builder().localMessage(localMessage).remoteCode(remoteCode).remoteMessage(remoteMessage).build();    }    public static RemoteException create(String localMessage,String remoteMessage){        return new Builder().localMessage(localMessage).remoteMessage(remoteMessage).build();    }    public static class Builder{        private String localCode;        private String localMessage;        private String remoteCode;        private String remoteMessage;        public Builder localCode(String localCode){            this.localCode = localCode;            return this;        }        public Builder localMessage(String localMessage){            this.localMessage = localMessage;            return this;        }        public Builder remoteCode(String remoteCode){            this.remoteCode = remoteCode;            return this;        }        public Builder remoteMessage(String remoteMessage){            this.remoteMessage = remoteMessage;            return this;        }        public RemoteException build(){            BusinessException businessException = BusinessException.create(localCode, localMessage);            RemoteException remoteException = RemoteException.create(businessException,remoteCode,remoteMessage);            return remoteException;        }    }    public BusinessException getParent() {        return parent;    }}

优雅的抛出异常

见过很多项目抛出新异常时使用了这样的方式 throw new BusinessException(...) 感觉特别不雅观。

  • 我们不需要暴露异常的构造函数,可以这样子
BusinessException.create("姓名重复,请重新输入");
  • 或者我们可以使用枚举,在枚举类中添加一个方法来创建异常,这针对需要错误编号的异常。

使用方法:

throw SystemMessage.NOT_LOGIN.exception();

代码定义:

import com.sanri.test.testmvc.dto.ResultEntity;public interface ExceptionCause<T extends Exception> {    T exception(Object... args);    ResultEntity result();}
import com.sanri.test.testmvc.dto.ResultEntity;public enum  SystemMessage implements ExceptionCause<BusinessException> {      NOT_LOGIN(4001,"未登录或 session 失效"),    PERMISSION_DENIED(4002,"没有权限"),    DATA_PERMISSION_DENIED(4007,"无数据权限"),    SIGN_ERROR(4003,"签名错误,你的签名串为 [%s]")    ;    ResultEntity resultEntity = new ResultEntity();    private SystemMessage(int returnCode,String message){        resultEntity.setReturnCode(returnCode+"");        resultEntity.setMessage(message);    }    @Override    public BusinessException exception(Object...args) {        return BusinessException.create(this,args);    }    @Override    public ResultEntity result() {        return resultEntity;    }    /**     * 自定义消息的结果返回     * @param args     * @return     */    public ResultEntity result(Object ... args){        String message = resultEntity.getMessage();        resultEntity.setMessage(String.format(message,args));        return resultEntity;    }    public String getReturnCode(){        return resultEntity.getReturnCode();    }}
  • 我们可以进一步封装,将其转换成断言,这个就看个人喜好了,将可以这样使用,只是写个例子,一般登录都在过滤器就拦截了。
assertLogin();/** * 断言用户是否为登录状态   */public void assertLogin(){    // 获取当前用户,从 session 或 redis 或 auth2 或 shiro 或 SSO 中获取     User user = xxx.get();    if(user == null){        throw SystemMessage.NOT_LOGIN.exception();    }}

演示使用方法

@RestControllerpublic class ExceptionController {    /**     * 静态异常展示,固定错误码     */    @GetMapping("/staticException")    public void staticException(){        throw SystemMessage.ACCESS_DENIED.exception("无权限");    }    /**     * 动态异常,前端不关注错误码     */    @GetMapping("/dynamicException")    public void dynamicException(){        throw BusinessException.create("名称重复,请使用别的名字");    }    /**     * 第三方调用异常,需显示层级异常     */    @GetMapping("/remoteException")    public void remoteException(){        //模拟远端错误        String remoteCode = "E007";        String remoteMessage = "生效日期必须大于当前日期";        throw RemoteException.create("某某业务调用错误",remoteCode,remoteMessage);    }}

github 项目代码

以上代码可以到我的 github 上下载相关项目,可以直接运行,拆箱即用。
https://gitee.com/sanri/example/tree/master/test-mvc

sanri-tools 工具

创作不易,希望可以推广下我的小工具,很实用的解决项目中的一些麻烦的事情,欢迎来 github 点星,fork
github 地址:https://gitee.com/sanri/sanri-tools-maven
博客地址:https://blog.csdn.net/sanri1993/article/details/98664034