Spring Boot(十一) 优雅的异常处理

2,320 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情

大家好! 我是慕歌,一只想教你学习 Spring Boot的野生coder! 欢迎来到慕歌的 Sping boot系列教程,希望通过这个教程带大家搭建基础的 Spring Boot项目,该教程所有知识点均来源于本人的真实开发!

前言

在前一节的学习中,慕歌带大家使用了全局结果集返回,通过使用全局结果集配置,优雅的返回后端数据,为前端的数据拿取提供了非常好的参考。同时通过不同的状态码返回,我们能够清晰的了解报错的位置,排除错误。如果大家有需要,可以使用我提供的的同一结果集以及状态码,并且可以使用全局异常拦截,实现异常的标准返回。接下来,我们一起来了解如何使用全局异常处理吧!

异常工具:

先定义一个合适 的异常处理类,在之后的异常都会以这种格式返回前端,前端根据我们的异常进行自己的返回,以一种优雅的方式呈现错误,优化用户体验。
异常结果集:

/**
 * 返回结果封装
 */

@Data
public class ResultVo {
    // 状态码
    private int code;

    // 状态信息
    private String msg;

    // 返回对象
    private Object data;

    // 手动设置返回vo
    public ResultVo(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    // 手动设置返回vo
    public ResultVo(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    // 只返回状态码
    public ResultVo(StatusCode statusCode) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
    }

    // 默认返回成功状态码,数据对象
    public ResultVo(Object data) {
        this.code = ResultCode.SUCCESS.getCode();
        this.msg = ResultCode.SUCCESS.getMsg();
        this.data = data;
    }

    // 返回指定状态码,数据对象
    public ResultVo(StatusCode statusCode, Object data) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
        this.data = data;
    }

    public ResultVo(StatusCode statusCode,String msg, Object data) {
        this.code = statusCode.getCode();
        this.msg = msg;
        this.data = data;
    }
}

异常状态码,通过返回的状态码,以及状态信息,能够高效反映错误,并且可以全局统一管理,方便快捷:

@Getter
public enum ExceptionCode implements StatusCode {

    // 系统级别错误码
    ERROR(-1, "操作异常"),
    NOT_LOGIN(102, "请先登录!"),
    NO_Role(102,"无权限"),
    NO_PERMISSION(102,"无权限"),
    OUT_TIME(102,"登录信息过期"),
    DISABLE_ACCOUNT(102,"帐号已被禁用!"),
    EMAIL_DISABLE_LOGIN(102,"该邮箱账号已被管理员禁止登录!"),
    IP_REPEAT_SUBMIT(102,"访问次数过多,请稍后重试"),
    ERROR_DEFAULT(105,"系统繁忙,请稍后重试");

    //异常码
    private int code;
    //异常信息
    private String msg;

    //自定义方法
    ExceptionCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

当我们对异常通过以上工具类进行封装之后,所有异常将以一种固定的格式返回,不会导致错乱:

{3 items
	"code":105
	"msg":"系统繁忙,请稍后重试"
	"data":NULL
}

异常处理:

在spring boot中需要使用异常拦截器,拦截全局的异常,不直接将异常返回,而是在我们进行处理之后,以一种清晰可读的方式返回。并且前端能够清晰解读我们的异常,呈现给用户。

//捕获校验器异常
@RestControllerAdvice
public class ControllerExceptionAdvice {
    @ExceptionHandler({BindException.class})
    public ResultVo ValidExceptionHandler(BindException e) {
        // 从异常对象中拿到ObjectError对象
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);

        return new ResultVo(ResultCode.VALIDATE_ERROR.getCode(),objectError.getDefaultMessage());
    }
}

对特定异常进行拦截,并包装异常:

/**
 * 对返回结果进行包装
 */
@RestControllerAdvice(basePackages = {"channel.cert"})
public class ControllerResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        // response是ResultVo类型,或者注释了NotControllerResponseAdvice都不进行包装
        return !methodParameter.getParameterType().isAssignableFrom(ResultVo.class);
    }

    @Override
    public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
        // String类型不能直接包装
        if (returnType.getGenericParameterType().equals(String.class)) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                // 将数据包装在ResultVo里后转换为json串进行返回
                return objectMapper.writeValueAsString(new ResultVo(data));
            } catch (JsonProcessingException e) {
                throw new APIException(ResultCode.RESPONSE_PACK_ERROR, e.getMessage());
            }
        }
        // 否则直接包装成ResultVo返回
        return new ResultVo(data);
    }
}

异常捕捉:

自定义异常:

@Getter
public class APIException extends RuntimeException {
    private int code;
    private String msg;

    //自定义异枚举
    public APIException(StatusCode statusCode){
        super(statusCode.getMsg());
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
    }

    // 手动设置异常
    public APIException(StatusCode statusCode, String message) {
        // message用于用户设置抛出错误详情,例如:当前价格-5,小于0
        super(message);
        // 状态码
        this.code = statusCode.getCode();
        // 状态码配套的msg
        this.msg = statusCode.getMsg();
    }

    // 默认异常使用APP_ERROR状态码
    public APIException(String errorMsg) {
        super(errorMsg);
        this.code = ExceptionCode.ERROR_DEFAULT.getCode();
        this.msg = ExceptionCode.ERROR_DEFAULT.getMsg();
    }

    //自定义参数 错误码 错误信息
    public APIException(int errorCode, String errorMsg) {
        super(errorMsg);
        this.code = errorCode;
        this.msg = ExceptionCode.ERROR_DEFAULT.getMsg();
    }

    //自定义参数 错误码 错误信息 异常
    public APIException(int errorCode, String errorMsg, Throwable cause) {
        super(errorMsg);
        this.code = errorCode;
        this.msg = errorMsg;
    }
}

对自定义异常进行捕获,通过定义好的异常的结果集返回。

/**
 * 全局异常处理
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionAdvice {

    // Assert业务异常
    @ExceptionHandler(IllegalArgumentException.class)
    public ResultVo AssertExceptionHandler(IllegalArgumentException ex) {
        log.error( " msg : " + ex.getMessage(), ex);
        if(StringUtils.isBlank(ex.getLocalizedMessage())){
            return new ResultVo(ExceptionCode.ERROR_DEFAULT);
        }
        return new ResultVo(ex.getMessage());
    }

    // 登录失效异常
    @ExceptionHandler(SaTokenException.class)
    public ResultVo LoginOutExceptionHandler(SaTokenException ex) {
        log.error( " msg : " + ex.getMessage(), ex);
        return new ResultVo(ExceptionCode.OUT_TIME);
    }

    // 登录异常
    @ExceptionHandler(NotLoginException.class)
    public ResultVo NotLoginExceptionHandler(NotLoginException ex) {
        log.error( " msg : " + ex.getMessage(), ex);
        return new ResultVo(ExceptionCode.NOT_LOGIN);
    }

    // 权限异常
    @ExceptionHandler(NotPermissionException.class)
    public ResultVo NotPermissionExceptionHandler(NotPermissionException ex) {
        log.error( " msg : " + ex.getMessage(), ex);
        return new ResultVo(ExceptionCode.NO_PERMISSION);
    }

    //角色异常
    @ExceptionHandler(NotRoleException.class)
    public ResultVo NotRoleExceptionHandler(NotRoleException ex) {
        log.error( " msg : " + ex.getMessage(), ex);
        return new ResultVo(ExceptionCode.NO_Role);
    }

    //处理自定义异常
    @ExceptionHandler(APIException.class)
    public ResultVo APIExceptionHandler(APIException e) {
        log.error(e.getMessage(), e);
        return new ResultVo(e.getCode(), e.getMsg());
    }

    //处理运行异常
    @ExceptionHandler(RuntimeException.class)
    public ResultVo RuntimeExceptionHandler(RuntimeException e) {
        log.error(e.getMessage(), e);
        return new ResultVo(ExceptionCode.ERROR_DEFAULT);
    }
}

通过以上自定义,我们就能在项目中,使用自定义异常,在我们认为可能的报错处插入,当发生错误时,我们更快定位是之前记录的错误点导致。

//查询数字证书
    @Override
    public GroupUser searchCert(String certCode) {
        try {
            //查询证书编号
            GroupUser user = queryCert(certCode);
            if(ObjectUtil.isNotNull(user)){
                return user;
            }
        }catch (Exception e){
            throw new APIException("区块链调用失败"+e);
        }
        return null;
    }

结语

这一章的分享到这里就结束了,下一节中还将带来优雅处理跨域问题的分享!
如果您觉得本文不错,欢迎点赞支持,您的关注是我坚持的动力!