Skip to content

异常处理设计

1. 概述

ZsAdmin 采用统一的异常处理机制,确保系统在发生异常时能够返回标准化的响应,提高系统的可用性和可维护性。本文档将详细介绍后端异常处理的设计与实现。

2. 异常处理架构

2.1 核心组件

  • ZsExceptionHandler:全局异常处理器
  • ZsException:业务异常类
  • Result:统一响应结果
  • ErrorCode:错误码枚举

2.2 架构图

mermaid
graph TD
A[客户端] --> B[Controller]
B --> C{业务逻辑}
C --> |正常| D[返回 Result.ok]
C --> |异常| E[抛出异常]
E --> |业务异常| F[ZsException]
E --> |系统异常| G[RuntimeException]
E --> |其他异常| H[Exception]
F --> I[ZsExceptionHandler]
G --> I
H --> I
I --> J[返回 Result.error]
J --> K[客户端]

3. 核心实现

3.1 错误码

java
public interface ErrorCodeConstants {

    ErrorCode CAPTCHA_ERROR = new ErrorCode(10001, "验证码错误");

    ErrorCode USER_NOT_EXIST = new ErrorCode(10002, "用户不存在");

    ErrorCode USER_PASSWORD_ERROR = new ErrorCode(10003, "用户密码错误");

    ErrorCode USER_NOT_LOGIN = new ErrorCode(10004, "用户未登录");

    ErrorCode USER_NOT_PERMISSION = new ErrorCode(10005, "用户没有权限");

    ErrorCode USER_NOT_ROLE = new ErrorCode(10006, "用户没有角色");

    ErrorCode USER_NOT_ENABLE = new ErrorCode(10007, "用户未启用");

    ErrorCode USER_LOGIN_ERROR = new ErrorCode(10008, "用户登录失败");

    ErrorCode USER_LOGOUT_ERROR = new ErrorCode(10009, "用户登出失败");

    ErrorCode USER_LOGIN_NAME_EXIST = new ErrorCode(10010, "用户登录名已存在");

    ErrorCode TENANT_NOT_EXIST = new ErrorCode(10011, "租户不存在");

    ErrorCode TENANT_NOT_ENABLE = new ErrorCode(10012, "租户未启用");

    ErrorCode TENANT_PACKAGE_NOT_EXIST = new ErrorCode(10013, "租户套餐不存在");

    ErrorCode TENANT_PACKAGE_NOT_ENABLE = new ErrorCode(10014, "租户套餐未启用");

    ErrorCode TENANT_NAME_EXIST = new ErrorCode(10015, "租户名称已存在");

    ErrorCode TENANT_SYSTEM_NOT_DELETE = new ErrorCode(10016, "系统租户,不能删除");

    ErrorCode TENANT_ADMIN_ROLE_NOT_EXIST = new ErrorCode(10017, "未找到租户管理员角色");
}

3.2 业务异常类

java
public class ZsException extends RuntimeException {

    private Integer code;
    private String msg;


    public ZsException(Integer code, String msg, Exception e) {
        super(e);
        this.code = code;
        this.msg = msg;
    }

    public ZsException(String msg) {
        this.msg = msg;
    }


    public ZsException(@NotNull HttpEnum httpEnum, Exception e) {
        super(e);
        this.code = httpEnum.getCode();
        this.msg = httpEnum.getMsg();
    }

    public ZsException(HttpEnum httpEnum) {
        this.code = httpEnum.getCode();
        this.msg = httpEnum.getMsg();
    }

    public ZsException(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public ZsException(HttpEnum httpEnum, String msg) {
        this.code = httpEnum.getCode();
        this.msg = msg;
    }

    public ZsException(ErrorCode errorCode) {
        this.code = errorCode.getCode();
        this.msg = errorCode.getMsg();
    }
}

3.3 全局异常处理器

java
@RestControllerAdvice
public class ZsExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(ZsExceptionHandler.class);


    /**
     * 自定义业务异常
     *
     * @return Result
     */
    @ExceptionHandler(ZsException.class)
    public Result<?> handleZsException(@NotNull ZsException e) {
        logger.error(e.getMessage(), e);
        return new Result<>().error(e.getCode(), e.getMsg(), e.getMessage());
    }

    @ExceptionHandler(MyBatisSystemException.class)
    public Result<?> handleMyBatisException(@NotNull MyBatisSystemException ex) {
        // 记录MyBatis-Plus的SQL异常信息到日志
        logger.error("MyBatis-Plus SQL Exception occurred", ex);
        logger.error(ex.getMessage(), ex);
        return new Result<>().error();
    }

    /**
     * 参数异常
     *
     * @param e a
     * @return Result
     */
    @ExceptionHandler(BindException.class)
    public Result<?> handleBindException(@NotNull BindException e) {
        logger.error(e.getMessage(), e);
        String message = e.getAllErrors().get(0).getDefaultMessage();

        return new Result<>().error(HttpEnum.VALIDATE_ERROR, message);
    }

    /**
     * 拦截未知的运行时异常
     */
    @ExceptionHandler(RuntimeException.class)
    public Result<?> handleRuntimeException(@NotNull RuntimeException e, @NotNull HttpServletRequest request) {
        logger.error(e.getMessage(), e);
        String requestUri = request.getRequestURI();
        logger.error(requestUri);
        return new Result<>().error(e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public Result<?> handleException(@NotNull Exception ex) {
        // 获取错误状态码
        logger.error(ex.getMessage(), ex);
        if (ex instanceof NoResourceFoundException){
            return new Result<>().error(HttpStatus.NOT_FOUND);
        } else if (ex instanceof BadRequestException) {
            return new Result<>().error( HttpStatus.BAD_REQUEST);
        } else if(ex instanceof HttpRequestMethodNotSupportedException){
            return new Result<>().error(HttpStatus.METHOD_NOT_ALLOWED);
        } else if (ex instanceof MethodArgumentNotValidException) {
            return new Result<>().error(HttpStatus.BAD_REQUEST);
        } else if (ex instanceof AccessDeniedException) {
            return new Result<>().error(HttpStatus.FORBIDDEN);
        }
        else {
            // 默认处理其他未预料的异常
            return new Result<>().error();
        }
    }

    @ExceptionHandler(AccessDeniedException.class)
    public void accessDeniedException(@NotNull AccessDeniedException e) throws AccessDeniedException {
        throw e;
    }

}

3.4 统一响应结果

java
@Data
@Schema(description = "统一返回结果")
public class Result<T> implements Serializable {

    @Schema(description = "状态码")
    private Integer code;

    @Schema(description = "返回信息")
    private String msg;

    @Schema(description = "返回数据")
    private T data;

    @Schema(description = "时间戳")
    private long timestamp = System.currentTimeMillis();

    public Result() {

    }

    public Result(@NotNull HttpEnum httpEnum) {
        this.code = httpEnum.getCode();
        this.msg = httpEnum.getMsg();
    }

    public Result(@NotNull HttpEnum httpEnum, String msg) {
        this.code = httpEnum.getCode();
        this.msg = msg;
    }

    public Result(String msg) {
        this.code = HttpEnum.INTERNAL_SERVER_ERROR.getCode();
        this.msg = msg;
        this.data = null;
    }

    public Result(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
        this.data = null;
    }

    public Result(@NotNull HttpEnum httpEnum, @Nullable T data) {
        this.code = httpEnum.getCode();
        this.msg = httpEnum.getMsg();
        this.data = data;
    }

    public Result(Integer code, String msg, @Nullable T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    @NotNull
    public Result<T> ok() {
        return new Result<>(HttpEnum.OK);
    }

    @NotNull
    public Result<T> ok(T data) {
        return new Result<>(HttpEnum.OK, data);
    }

    @NotNull
    public Result<T> ok(@NotNull HttpEnum httpEnum) {
        return new Result<>(httpEnum);
    }

    @NotNull
    public Result<T> ok(Integer code, String msg, T data) {
        return new Result<>(code, msg, data);
    }

    @NotNull
    public Result<T> error() {
        return new Result<>( HttpEnum.INTERNAL_SERVER_ERROR);
    }

    @NotNull
    public Result<T> error(T data) {
        return new Result<>(HttpEnum.INTERNAL_SERVER_ERROR, data);
    }

    @NotNull
    public Result<T> error(@NotNull HttpStatus httpStatus) {
        return new Result<>(httpStatus.value(), httpStatus.getReasonPhrase());
    }

    @NotNull
    public Result<T> error(String msg) {
        return new Result<>(msg);
    }

    @NotNull
    public Result<T> error(@NotNull HttpEnum httpEnum) {
        return new Result<>(httpEnum);
    }

    @NotNull
    public Result<T> error(Integer code, String msg) {
        return new Result<>(code, msg);
    }

    @NotNull
    public Result<T> error(@NotNull HttpEnum httpEnum, String msg) {
        return new Result<>(httpEnum, msg);
    }

    @NotNull
    public Result<T> error(Integer code, String msg, T data) {
        return new Result<>(code, msg, data);
    }

}

4. 异常处理流程

  1. 业务层:当业务逻辑出现异常时,抛出 ZsException 并指定错误码
  2. 控制器层:接收请求,调用业务层方法,不处理异常
  3. 全局异常处理器:捕获所有异常,根据异常类型返回标准化响应
  4. 客户端:接收统一格式的响应,根据状态码进行处理