通用异常处理
1 场景预设
项目应用场景:当客户端执行某个业务,服务端进行处理业务-Controller-Service-Dao 过程中可能由于用户,代码问题导致业务执行失败,给用户一个提示,告知用户执行结果,如果失败告知原因。
我们预设这样一个场景,假如我们做新增用户,需要接收下面的参数:
name:名称
age:年龄
然后对数据做简单校验:
- 年龄不能为空
需求:新增时,自动生成ID,然后随用户对象一起返回
开发业务功能步骤:
- 实体类 PO DTO
- 持久层Mapper
- 业务层Service
- 控制层Controller
- 完成三层注入
- 按照接口文档开发业务功能
8.1.1 代码
在nc_sys_pojo中增加用户实体类:
package com.itheima.sys.entity;
import lombok.Data;
/**
* @author heima
*/
@Data
public class UserDomain {
private Long id;
private String name;
private Integer age;
}
在nc_sys_service中增加用户业务类:
package com.itheima.sys.service;
import com.itheima.sys.entity.UserDomain;
import org.springframework.stereotype.Service;
import java.util.Random;
@Service
public class UserDominService {
/**
* 模拟测试保存用户记录
* @param user
* @return
*/
public UserDomain saveUser(UserDomain user) {
Random random = new Random(1000);
user.setId(random.nextLong());
return user;
}
}
在nc_sys_service中增加用户控制器类:
package com.itheima.sys.controller;
import com.itheima.common.vo.Result;
import com.itheima.sys.entity.UserDomain;
import com.itheima.sys.service.UserDominService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试控制器
* @author itheima
*/
@RestController
public class TestController {
@Autowired
private UserDominService userDominService;
@GetMapping("/test")
public Result<String> hello(){
return new Result<>(true, 200, "请求成功!", "hello nineclock!");
}
/**
* 全局异常场景模拟方法
* @param user
* @return
*/
@PostMapping("/test/user")
public Result saveUser(@RequestBody UserDomain user){
//如果年龄为空,则抛出异常,返回400状态码,返回错误提示消息
if (user.getAge() == null) {
throw new RuntimeException("用户年龄为必填项!");
}
return new Result(true, 200, "保存成功", userDominService.saveUser(user));
}
}
8.1.2 测试异常
使用POSTMAN发送请求进行测试,注意提交参数格式为Json.
8.1.3 问题分析
刚才的处理看似完美,但是仔细想想,我们在异常处理中,如果抛出RuntimeException交给框架处理返回状态码为固定的500了,仅凭用户抛出的异常,我们根本无法判断到底该返回怎样的状态码,可能是参数有误、数据库异常、没有权限,等等各种情况。
解决问题的思路为,所有的异常全部抛出,然后统一的拦截到异常处理,给前端返回结果。
8.2 统一异常处理
接下来,我们使用SpringMVC提供的统一异常拦截器,因为是统一处理,我们放到nc_common
项目中:
新建一个全局异常处理类,名为:BasicExceptionAdvice 注意:必须保证被启动类能够扫描到。
具体代码如下:
package com.itheima.common.exception.advice;
import com.itheima.common.vo.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author: itheima
* @create: 2021-07-23 12:07
*/
@ResponseBody
@ControllerAdvice //对controller层进行AOP增强,异常通知
public class BasicExceptionAdvice {
/**
* 处理RuntimeException类型异常
* @param e
* @return
*/
@ExceptionHandler(RuntimeException.class)
public Result<String> handlerRuntimeErr(RuntimeException e){
return new Result<>(false, 500, "执行业务失败!"+e.getMessage());
}
}
解读:
-
@ControllerAdvice
:默认情况下,会拦截所有加了@Controller
的类 -
@ExceptionHandler(RuntimeException.class)
:作用在方法上,声明要处理的异常类型,可以有多个,这里指定的是RuntimeException
。被声明的方法可以看做是一个SpringMVC的Handler
:- 参数是要处理的异常,类型必须要匹配
- 返回结果可以是
ModelAndView
、ResponseEntity
等,基本与handler
类似
重启系统微服务项目进行测试:
成功返回了错误信息!
8.2.1 异常信息及状态
刚才的处理看似完美,但是仔细想想,我们在通用的异常处理中,把返回状态码写死为400了:
这样显然不太合理。
因此,用户抛出异常时,就必须传递两个内容:
- 异常信息
- 异常状态码
但是RuntimeException
是无法接受状态码的,只能接受异常的消息,所以我们需要做两件事情:
- 自定义异常,来接受状态码、异常消息
- 状态码与异常消息可能会重复使用,我们通过枚举来把这些信息变为常量
8.2.2 自定义异常枚举类型
枚举:把一件事情的所有可能性列举出来。在计算机中,枚举也可以叫多例,单例是多例的一种情况 。
单例:一个类只能有一个实例。
多例:一个类只能有有限个数的实例。
我们定义一个枚举,用于封装异常状态码和异常信息:
package com.itheima.common.enums;
import lombok.Getter;
@Getter
public enum ResponseEnum {
//一定要将选择项放在最上
AGE_NOT_NULL(400, "年龄不能为空!"),
SUCCESS(200, "操作成功!"),
ERROR(500, "操作失败!"),
;
private Integer code;
private String message;
//提供私有无参构造
ResponseEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
8.2.3 自定义异常
然后自定义异常,来获取枚举对象。
在nc_common中定义自定义异常类:
package com.itheima.common.exception;
import com.itheima.common.exception.enums.ResponseEnum;
import lombok.Getter;
/**
* 增加业务执行状态码属性
*/
@Getter
public class NcException extends RuntimeException {
//业务状态码
private Integer code;
/**
* 通过构造注入值
* @param code
* @param message
*/
public NcException(Integer code, String message) {
super(message);
this.code = code;
}
/**
* 自定义异常状态码提示消息,通过枚举类型
* @param enums
*/
public NcException(ResponseEnum enums) {
super(enums.getMessage());
this.code = enums.getCode();
}
}
修改Controller中代码:
package com.itheima.sys.controller;
import com.itheima.common.enums.ResponseEnum;
import com.itheima.common.exception.NcException;
import com.itheima.common.vo.Result;
import com.itheima.sys.entity.UserDomain;
import com.itheima.sys.service.UserDominService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
private UserDominService userDominService;
/**
* 全局异常场景模拟方法
* @param user
* @return
*/
@PostMapping("/test/user")
public Result saveUser(@RequestBody UserDomain user){
//如果年龄为空,则抛出异常,返回400状态码,返回错误提示消息
if (user.getAge() == null) {
//throw new RuntimeException("用户年龄为必填项!");
throw new NcException(ResponseEnum.AGE_NOT_NULL);
}
return new Result(true, 200, "保存成功", userDominService.saveUser(user));
}
}
修改全局异常处理逻辑:
package com.itheima.common.exception.advice;
import com.itheima.common.exception.NcException;
import com.itheima.common.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @Author: 黑马程序员
* @description:
**/
@Slf4j
@ResponseBody
@ControllerAdvice
public class BasicExceptionAdvice {
@ExceptionHandler(RuntimeException.class)
public Result handleException(RuntimeException e) {
// 我们暂定返回状态码为400, 然后从异常中获取友好提示信息
return new Result(false, 400, e.getMessage());
}
/**
* 处理自定义异常
* @return
*/
@ExceptionHandler(NcException.class)
public Result handlerNcException(NcException e){
return new Result(false, e.getCode(), e.getMessage());
}
/**
* 处理其他异常
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public Result handlerNcException(Exception e){
return new Result(false, 500, e.getMessage());
}
}
测试结果如下:
8.2.4 封装返回结果
将来返回结果给前端,不管成功还是失败都需要返回结果对象Result,目前每次返回都必修手动New一个对象,使用起来比较繁琐。改为将成功,以及失败的Result对象返回的静态方法封装在Result类中。
package com.itheima.common.vo;
import com.itheima.common.exception.enums.ResponseEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;
/**
* @program: 黑马程序员
* @description: 服务端提供前端返回的结果
**/
@Data
public class Result<T> implements Serializable {
/**
* 是否成功
*/
private Boolean success;
/**
* 返回状态码:执行业务状态
*/
private int code;
/**
* 提示消息
*/
private String message;
/**
* 返回数据
*/
private T data;
public Result() {
}
public Result(Boolean success, int code, String message) {
this.success = success;
this.code = code;
this.message = message;
}
public Result(Boolean success, int code) {
this.success = success;
this.code = code;
}
public Result(Boolean success, int code, T data) {
this.success = success;
this.code = code;
this.data = data;
}
public Result(Boolean success, int code, String message, T data) {
this.success = success;
this.code = code;
this.message = message;
this.data = data;
}
//TODO 问题:响应结果在controller方法中需要每次都new 解决:抽取静态方法代表成功 或者 失败结果
public static <T> Result<T> success() {
return new Result<T>(true, ResponseEnum.SUCCESS.getCode());
}
public static <T> Result<T> successMessage(String message) {
return new Result<T>(true, ResponseEnum.SUCCESS.getCode(), message);
}
public static <T> Result<T> success(T data) {
return new Result<T>(true, ResponseEnum.SUCCESS.getCode(), data);
}
public static <T> Result<T> success(String message, T data) {
return new Result<T>(true, ResponseEnum.SUCCESS.getCode(), message, data);
}
public static <T> Result<T> error() {
return new Result<T>(false, ResponseEnum.ERROR.getCode(), ResponseEnum.ERROR.getMessage());
}
public static <T> Result<T> errorMessage(String errorMessage) {
return new Result<T>(false, ResponseEnum.ERROR.getCode(), errorMessage);
}
public static <T> Result<T> errorCodeMessage(int errorCode, String errorMessage) {
return new Result<T>(false, errorCode, errorMessage);
}
}
修改项目中正常以及异常处理方法返回结果:
package com.itheima.common.exception.advice;
import com.itheima.common.exception.NcException;
import com.itheima.common.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @Author: 黑马程序员
* @description:
**/
@Slf4j
@ResponseBody
@ControllerAdvice
public class BasicExceptionAdvice {
@ExceptionHandler(RuntimeException.class)
public Result handleException(RuntimeException e) {
// 我们暂定返回状态码为400, 然后从异常中获取友好提示信息
//return new Result(false, 500, e.getMessage());
return Result.errorCodeMessage(500, e.getMessage());
}
/**
* 处理自定义异常
* @return
*/
@ExceptionHandler(NcException.class)
public Result handlerNcException(NcException e){
//return new Result(false, exception.getCode(), exception.getMessage());
return Result.errorCodeMessage(e.getCode(), e.getMessage());
}
/**
* 处理其他异常
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public Result handlerNcException(Exception e){
//return new Result(false, 500, e.getMessage());
return Result.errorCodeMessage(500, e.getMessage());
}
}
再次修改TestController中代码:
8.3 返回异常枚举类
如果还有其他异常提示信息,只需要增加即可!
package com.itheima.common.exception.enums;
import lombok.Getter;
@Getter
public enum ResponseEnum {
//一定要将选择项放在最上
AGE_NOT_NULL(400, "年龄不能为空!"),
SUCCESS(200, "操作成功!"),
ERROR(500, "操作失败!"),
INVALID_FILE_TYPE(400, "无效的文件类型!"),
INVALID_PARAM_ERROR(400, "无效的请求参数!"),
INVALID_PHONE_NUMBER(400, "无效的手机号码"),
INVALID_VERIFY_CODE(400, "验证码错误!"),
INVALID_USERNAME_PASSWORD(400, "无效的用户名和密码!"),
INVALID_SERVER_ID_SECRET(400, "无效的服务id和密钥!"),
INVALID_NOTIFY_PARAM(400, "回调参数有误!"),
INVALID_NOTIFY_SIGN(400, "回调签名有误!"),
DATA_TRANSFER_ERROR(500, "数据转换异常!"),
INSERT_OPERATION_FAIL(500, "新增操作失败!"),
UPDATE_OPERATION_FAIL(500, "更新操作失败!"),
DELETE_OPERATION_FAIL(500, "删除操作失败!"),
FILE_UPLOAD_ERROR(500, "文件上传失败!"),
DIRECTORY_WRITER_ERROR(500, "目录写入失败!"),
FILE_WRITER_ERROR(500, "文件写入失败!"),
SEND_MESSAGE_ERROR(500, "短信发送失败!"),
CODE_IMAGE_ERROR(500, "验证码错误!"),
USER_MOBILE_EXISTS(500, "该手机号已经被注册!"),
USER_NOT_REGISTER(500, "当前用户还未进行注册!"),
USER_NOT_JOIN_COMPANY(500, "请加入企业后再使用该功能!"),
USER_NOT_COMPANY_ADMIN(500, "您不是企业管理员!"),
USER_NOT_MATCH_ATTGROUP(500, "未查询到用户考勤组,请先配置!"),
USER_NOT_FOUND(500, "没有查询到用户!"),
COMPANY_ADMIN_NOT_EXISTS(500, "没有找到对应企业管理员!"),
COMPANY_NOT_FOUND(500, "企业不存在!"),
WROK_NUM_EXISTS(500, "当前工号已经存在!"),
COMPANY_USER_NOT_FOUND(404, "企业员工不存在!"),
SMS_CODE_TIMEOUT(404, "验证码超时,请重新发送!"),
PUNCH_INVALID_AREA(500, "打卡地点不在考勤点范围内!"),
PUNCH_INVALID_DAY(500, "非工作日无需打卡!"),
PUNCH_ALREADY(500, "已经打卡,无需重复打卡!"),
SIGN_DATA_NULL(404, "未检索到签到数据!"),
MESSAGE_PARAMS_LOST(500, "查询参数缺失!"),
UNAUTHORIZED(401, "登录失效或未登录!"),
FOBIDDEN(403, "禁止访问!"),
SIGNDATA_NOT_FOUND(500, "当前检索条件没有签到数据!"),
ROLE_NOT_FOUND(403, "角色列表不存在!"),
ROLE_SYS_NOT_FOUND(403, "系统管理员角色不存在!"),
SYS_PERMISSSION_NOT_FOUND(403, "当前企业无权限"),
DOC_NOT_FOUND(403, "文档不存在或者已经删除!"),
DOC_NOT_ALLOWED(403, "只有作者才能设置协作者!"),
FILE_NOT_ALLOWED_MODIFY(403, "您没有权限修改文档!"),
FILE_NOT_BELONG_YOU(403, "您不是该文档拥有者,无法设置权限!"),
;
private Integer code;
private String message;
//提供私有无参构造
ResponseEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}