全局异常处理的引入

springboot项目运行过程中,我们一般存在2种异常:

  • 业务异常:系统设计而引起的业务异常。比如不同权限用户对文章的修改权限不足,而权限低的用户在保存时,后台会对用户权限进行校验,当校验不通过时,后台则会抛出相应的权限不足异常,阻止往下继续修改。
  • 技术异常(代码异常):代码编写过程中,未考虑对象是否为空,对null对象直接使用相关方法从而导致异常。

在项目中,如果不对后台代码的异常进行捕捉,而直接抛到前端,那么则会产生whitelable Error Page页面,如下: alt

这种页面的生成会影响用户交互过程的体验。因此,我们需要通过全局异常捕捉,封装异常的信息,传递给前端,减少乃至避免whilelabel error page的产生。

实现过程

项目的业务异常通常分为很多种:1. 逻辑异常;2. 数据异常;3. 权限不足;4. 编码异常....针对这种情况,我们可以使用一个ErrorEnum枚举类来定义这些异常,并在后期使用。

错误码枚举

对于整个后端项目的异常,我们可以大致定义如下:

import lombok.Getter;

@Getter
public enum ErrorEnum {

    /**
     * 错误类型枚举
     */
    SUCCESS("0", "成功"),

    DATA_ERROR("AP0001", "数据异常"),

    NO_PERMISSION("AP0002", "当前用户无权限"),

    // ....

    SYSTEM_ERROR("AP9999", "系统异常,请稍后重试");

    /**
     * 错误码
     */
    private final String code;

    /**
     * 错误描述
     */
    private final String desc;

    ErrorEnum(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

这里,我们定义了成功码0,系统异常(AP9999,编码鲁棒性不足导致),权限不足(AP0002)....

自定义异常处理类

我们需要定制化的异常处理类,用于封装后端到前端的异常信息。该异常类至少需要包含2个字段:错误码(errorCode)、错误信息(errorMsg)。此外,该类同时需要拓展RuntimeException,表示这是个异常类,并可以通过代码主动抛出。

@Getter
public class AppTransException extends RuntimeException{

    private String errorCode;

    private String msg;


    public AppTransException(ErrorEnum e, String msg){
        super(msg);
        this.msg = msg;
        this.errorCode = e.getCode();
    }

    public AppTransException(ErrorEnum e){
        super(e.getDesc());
        this.msg = e.getDesc();
        this.errorCode = e.getCode();
    }
}

异常处理逻辑

在定义完交易异常类之后,我们可以定义一个ExceptionHandler类,用于处理交易过程中产生的异常。在异常处理中,代码会判断异常是否为业务异常,为业务异常,则将业务的信息、错误码封装,若为其他异常,则抛系统异常。

该类存放于/core/config文件夹下。这里主要的是@ControllerAdvice和@ExceptionHandler两个注解,其中@ControllerAdvice是让springboot管理,表示这是个异常捕获类,而@ExceptionHandler中具体跟随的则是要捕捉的异常类型,该类型以及该类型的子类都能被该注解所对应的方法进行捕获。

// 开启全局异常捕捉
@ControllerAdvice
@ResponseBody
@Slf4j
public class GlobalExceptionConfig {

    /**
     * 全局异常处理
     * @param e
     * @return
     */
    @ExceptionHandler(RuntimeException.class)
    public ResponseResult appExceptionHandle(Exception e){
        ResponseResult result = new ResponseResult();
        // 判断是否为业务异常(主动抛出)
        if(e instanceof AppTransException){
            appTransExceptionHandler(result, (AppTransException) e);
        }else if(e instanceof BeanInstantiationException){
            // 在新生成类时所使用
            Throwable cause = ((BeanInstantiationException) e).getCause();
            appTransExceptionHandler(result, (AppTransException) cause);
        }else {
            // 目前只有业务异常和其他异常
            otherExceptionHandler(result, new AppTransException(ErrorEnum.SYSTEM_ERROR));
        }
        return result;
    }

    /**
     * 业务异常时的处理
     * @param result
     * @param e
     */
    private void appTransExceptionHandler(ResponseResult result, AppTransException e){
        setErrorMessage(result, e.getMsg(), e.getErrorCode());
    }

    /**
     * 目前其非业务异常均使用这个AP9999异常码
     * @param result
     * @param e
     */
    private void otherExceptionHandler(ResponseResult result, AppTransException e){
        setErrorMessage(result, e.getErrorCode(), e.getMsg());
    }

    private void setErrorMessage(ResponseResult result, String errorCode, String msg){
        result.setErrorCode(errorCode);
        result.setMsg(msg);
    }
}

在代码中,业务异常,我们可以定制化错误信息,也可以使用预定义的错误信息。

使用效果

我们使用下面的代码就可以发起请求,查看全局异常处理的效果。

@PostMapping("/error1")
public ResponseResult error999(@RequestBody HelloDTO dto){
    throw new AppTransException(ErrorEnum.SYSTEM_ERROR);
}

alt