大家在使用SpringBoot开发项目的时候肯定都需要处理异常吧,没有处理异常那么异常信息直接显示给用户这是非常不雅观的,同时还可能造成用户误会,那么今天我们就来简单的写一下如何在SpringBoot项目中实现统一的异常处理。
1.自定义异常类
我们先定义一个自定义业务异常类,这个异常类继承了 RuntimeException,并添加了一个 code 属性,用于标识错误码,以及一个 msg 属性,用于标识错误信息。
package cn.youhaveme.exception;
import cn.youhaveme.entity.result.ResultEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BusinessException extends RuntimeException {
private Integer code;
private String msg;
public BusinessException(String msg) {
this.code = ResultEnum.BUSINESS_EXCEPTION.getCode();
this.msg = msg;
}
}
2.定义全局异常处理器
接下来,我们需要定义一个全局异常处理器,这个主要是用于捕获Controller的异常并处理异常。在 SpringBoot 中,我们可以使用@RestControllerAdvice
注解来定义一个异常处理器,这个异常处理器可以处理所有的异常,在每个不同的处理器中,还可以通过@ExceptionHandler(异常.class)
来区分不同的异常,以此来做出不同的响应信息。
package cn.youhaveme.exception;
import cn.dev33.satoken.exception.DisableServiceException;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.youhaveme.entity.result.Result;
import cn.youhaveme.entity.result.ResultEnum;
import cn.youhaveme.entity.result.ResultJson;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
import java.util.stream.Collectors;
@RestControllerAdvice
@Slf4j
public class GlobalException {
// 拦截:未登录异常
@ExceptionHandler(NotLoginException.class)
public Result<String> handlerException(NotLoginException e) {
log.error("登录未认证异常", e);
String msg;
switch (e.getType()) {
case NotLoginException.NOT_TOKEN:
msg = "账号已注销,请重新登录";
break;
case NotLoginException.INVALID_TOKEN:
msg = NotLoginException.INVALID_TOKEN_MESSAGE;
break;
case NotLoginException.TOKEN_TIMEOUT:
msg = "Token已过期,请重新登录";
break;
case NotLoginException.BE_REPLACED:
msg = "账号被顶强制下线";
break;
case NotLoginException.KICK_OUT:
msg = "账号被强制踢出,请重新登录";
break;
default:
msg = "当前会话未登录";
}
return ResultJson.goErrorResponse(ResultEnum.UNAUTHORIZED.getCode(), msg);
}
// 拦截:缺少权限异常
@ExceptionHandler(NotPermissionException.class)
public Result<String> handlerException(NotPermissionException e) {
log.error("缺少权限异常", e);
return ResultJson.goErrorResponse(ResultEnum.FORBIDDEN.getCode(), "缺少权限:" + e.getPermission());
}
// 拦截:缺少角色异常
@ExceptionHandler(NotRoleException.class)
public Result<String> handlerException(NotRoleException e) {
log.error("缺少角色异常", e);
return ResultJson.goErrorResponse(ResultEnum.FORBIDDEN.getCode(), "缺少角色:" + e.getRole());
}
// 拦截:服务封禁异常
@ExceptionHandler(DisableServiceException.class)
public Result<String> handlerException(DisableServiceException e) {
log.error("服务封禁异常", e);
return ResultJson.goErrorResponse(ResultEnum.SERVICE_EXCEPTION.getCode(), "当前账号 " + e.getService() + " 服务已被封禁 (level=" + e.getLevel() + "):" + e.getDisableTime() + "秒后解封");
}
// 拦截:参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<String> handlerException(MethodArgumentNotValidException e) {
log.error("参数校验异常", e);
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
String message = allErrors.stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
return ResultJson.goErrorResponse(ResultEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION.getCode(), message);
}
// 拦截:业务逻辑异常
@ExceptionHandler(BusinessException.class)
public Result<String> handlerException(BusinessException e) {
log.error("业务逻辑异常", e);
return ResultJson.goErrorResponse(ResultEnum.BUSINESS_EXCEPTION.getCode(), e.getMsg());
}
// 拦截:系统异常(兜底异常)
@ExceptionHandler(Exception.class)
public Result<String> handlerException(Exception e) {
log.error("系统异常", e);
return ResultJson.goErrorResponse(ResultEnum.SYSTEM_EXCEPTION.getCode(), ResultEnum.SYSTEM_EXCEPTION.getMessage());
}
}
3.统一的接口返回
这个统一的接口返回也是非常有必要的,毕竟一个统一的接口,不仅仅可以处理成功信息,这部分的异常信息也同样可以处理,那么我之前写过一篇有关SpringBoot统一返回的文章,有兴趣的可以点击去翻阅一下,这里就不重复介绍了。
4.自定义异常抛出
在业务代码中,如果发生了自定义异常,或者有需要我们自己处理的一些东西,我们就需要选择性的将异常抛出,这样才会被全局异常处理器所拦截,并作出相应的处理逻辑。
@DeleteMapping("/delete")
public Result<String> delete(@Validated({ValidGroup.Delete.class}) @RequestBody User user) {
log.info("删除用户:{}", user);
User dbUser = userService.getUserByUserName(user.getUserName());
if (BeanUtil.isEmpty(dbUser)) {
throw new BusinessException("该用户不存在,无法删除");
}
boolean removeResult = userService.removeById(dbUser.getId());
if (!removeResult) {
throw new BusinessException("用户删除失败");
}
return ResultJson.goSuccessResponse("删除成功");
}
至此,关于统一异常的处理就已经完成了,这时候系统再出现异常,就不会说是直接抛出异常堆栈信息,而是一个非常优雅的异常提示信息。当然这只是一个小小的例子,具体的异常处理方式需要根据实际情况进行调整。