通过前面两段我们可以发现,造成异常的原因很多,出现异常的地方很多,异常的处理手段也很多。基于以上三多的情况,我们需要一个地方来统一接收异常、统一处理异常,上面提到SpringBoot的@ControllerAdvice注解作为一个全局的异常处理器来统一处理异常。但@ControllerAdvice注解不是万能的,它有一个问题:
对于@ControllerAdvice注解来说,它主要用于处理Controller层的异常情况,即在控制器方法中发生的异常。因为它是基于Spring MVC的控制器层的异常处理机制。
而Filter层是位于控制器之前的一层过滤器,它可以用于对请求进行预处理和后处理。当请求进入Filter时,还没有进入到Controller层,所以@ControllerAdvice注解无法直接处理Filter层中的异常。
所以对于Filter中的异常,我们需要单独处理。
由于SpringBoot框架并没有定义业务相关的错误码,所以我们需要自定义业务错误码。该错误码可以根据业务复杂程度进行分类,每个错误码对应一个具体的异常情况。这样前后端统一处理异常时可以根据错误码进行具体的处理逻辑,提高异常处理的准确性和效率。同时,定义错误码还可以方便进行异常监控和日志记录,便于排查和修复问题。
a、定义常见的异常状态码ResponseCodeEnum.java
package com.summo.demo.model.response;
public enum ResponseCodeEnum {
/**
* 请求成功
*/
SUCCESS("0000", ErrorLevels.DEFAULT, ErrorTypes.SYSTEM, "请求成功"),
/**
* 登录相关异常
*/
LOGIN_USER_INFO_CHECK("LOGIN-0001", ErrorLevels.INFO, ErrorTypes.BIZ, "用户信息错误"),
/**
* 权限相关异常
*/
NO_PERMISSIONS("PERM-0001", ErrorLevels.INFO, ErrorTypes.BIZ, "用户无权限"),
/**
* 业务相关异常
*/
BIZ_CHECK_FAIL("BIZ-0001", ErrorLevels.INFO, ErrorTypes.BIZ, "业务检查异常"),
BIZ_STATUS_ILLEGAL("BIZ-0002", ErrorLevels.INFO, ErrorTypes.BIZ, "业务状态非法"),
BIZ_Query_EMPTY("BIZ-0003", ErrorLevels.INFO, ErrorTypes.BIZ, "查询信息为空"),
/**
* 系统出错
*/
SYSTEM_EXCEPTION("SYS-0001", ErrorLevels.ERROR, ErrorTypes.SYSTEM, "系统出错啦,请稍后重试"),
;
/**
* 枚举编码
*/
private final String code;
/**
* 错误级别
*/
private final String errorLevel;
/**
* 错误类型
*/
private final String errorType;
/**
* 描述说明
*/
private final String description;
ResponseCodeEnum(String code, String errorLevel, String errorType, String description) {
this.code = code;
this.errorLevel = errorLevel;
this.errorType = errorType;
this.description = description;
}
public String getCode() {
return code;
}
public String getErrorLevel() {
return errorLevel;
}
public String getErrorType() {
return errorType;
}
public String getDescription() {
return description;
}
public static ResponseCodeEnum getByCode(Integer code) {
for (ResponseCodeEnum value : values()) {
if (value.getCode().equals(code)) {
return value;
}
}
return SYSTEM_EXCEPTION;
}
}
b、自定义业务异常类
BizException.java
package com.summo.demo.exception.biz;
import com.summo.demo.model.response.ResponseCodeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class BizException extends RuntimeException {
/**
* 错误码
*/
private ResponseCodeEnum errorCode;
/**
* 自定义错误信息
*/
private String errorMsg;
}
(2) 全局异常处理器
BizGlobalExceptionHandler
package com.summo.demo.exception.handler;
import javax.servlet.http.HttpServletResponse;
import com.summo.demo.exception.biz.BizException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.ModelAndView;
@RestControllerAdvice(basePackages = {"com.summo.demo.controller", "com.summo.demo.service"})
public class BizGlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public ModelAndView handler(BizException ex, HttpServletResponse response) {
ModelAndView modelAndView = new ModelAndView();
switch (ex.getErrorCode()) {
case LOGIN_USER_INFO_CHECK:
// 重定向到登录页
modelAndView.setViewName("redirect:/login");
break;
case NO_PERMISSIONS:
// 设置错误信息和错误码
modelAndView.addObject("errorMsg", ex.getErrorMsg());
modelAndView.addObject("errorCode", ex.getErrorCode().getCode());
modelAndView.setViewName("403");
break;
case BIZ_CHECK_FAIL:
case BIZ_STATUS_ILLEGAL:
case BIZ_QUERY_EMPTY:
case SYSTEM_EXCEPTION:
default:
// 设置错误信息和错误码
modelAndView.addObject("errorMsg", ex.getErrorMsg());
modelAndView.addObject("errorCode", ex.getErrorCode().getCode());
modelAndView.setViewName("error");
}
return modelAndView;
}
}
(3) 测试效果
@RestControllerAdvice和@ExceptionHandler使用起来很简单,下面我们来测试一下(由于不写界面截图是在太丑,我麻烦ChatGPT帮我写了一套简单的界面)。
a、普通业务异常捕获第一步、打开登录页访问链接:http://localhost:8080/login
输入账号、密码,点击登录进入首页
再服务启动之前我写了一个根据用户名查询用户的方法,如果查询不到用户的话我会抛出一个异常,代码如下:
public ResponseEntity<String> query(String userName) {
//根据名称查询用户
List<UserDO> list = userRepository.list(
new QueryWrapper<UserDO>().lambda().like(UserDO::getUserName, userName));
if (CollectionUtils.isEmpty(list)) {
throw new BizException(ResponseCodeEnum.BIZ_QUERY_EMPTY, "根据用户名称查询用户为空!");
}
//返回数据
return ResponseEntity.ok(JSONObject.toJSONString(list));
}
这时,我们查询一个不存在的用户
访问接口:http://localhost:8080/user/query?userName=sss
因为数据库中没有用户名为sss的这个用户,会抛出一个异常
访问链接:http://localhost:8080/login
登录界面使用小B的账号登录