之前也学过对异常的统一管理,既然现在又看到了,就再学学springboot——全局异常处理器及封装结果集
如果一致对异常进行try…catch…,代码也会很冗余,我们直接throw就行,然后可以对异常进行统一处理
不论是那一层,都会有异常的出现,我们遇到异常都是向上抛出,最后让框架对异常统一处理
假如dao出异常就会抛给service,service有异常就会抛给controller,controller会抛给spring框架
@ExceptionHandler
提供的标识在方法上或类上的注解,用来表明方法的处理异常类型
@ControllerAdvice
控制器增强,在项目中来增强SpringMVC中的Controller。通常和@ExceptionHandler结合使用,来处理SpringMVC的异常信息。
@ResponseStatus:
提供的标识在方法上或类上的注解,用状态代码和应返回的原因标记方法或异常类。
调用处理程序方法时,状态代码将应用于HTTP响应
/**
* 统一异常处理类
*/
@Slf4j
@ControllerAdvice
@ResponseBody//返回JSON数据
//@RestControllerAdvice = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
/**
* 针对自定义异常进行处理的
*/
@ResponseBody
@ExceptionHandler(XueChengPlusException.class)//捕捉这个异常
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)//状态码500
public RestErrorResponse customException(XueChengPlusException e) {
// 捕捉到异常的信息后解析异常的信息
log.error("【系统异常】{}", e.getErrMessage(), e);
return new RestErrorResponse(e.getErrMessage());
}
/**
* 捕捉除了自定义异常以外的其他异常
*/
@ResponseBody
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)//状态码500
public RestErrorResponse exception(Exception e) {
log.error("【系统异常】{}", e.getMessage(), e);
return new RestErrorResponse(CommonError.UNKOWN_ERROR.getErrMessage());//执行过程异常,请重试
}
}
@EqualsAndHashCode(callSuper = true)
@Data
public class XueChengPlusException extends RuntimeException {
private static final long serialVersionUID = -378480393877627738L;
private String errMessage;
public XueChengPlusException() {
super();
}
public XueChengPlusException(String errMessage) {
super(errMessage);
this.errMessage = errMessage;
}
public static void cast(CommonError commonError) {
throw new XueChengPlusException(commonError.getErrMessage());
}
public static void cast(String errMessage) {
throw new XueChengPlusException(errMessage);
}
}
/**
* 和前端约定返回的异常信息模型
* 错误响应参数包装
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RestErrorResponse implements Serializable {
private static final long serialVersionUID = 9026504397012666687L;
private String errMessage;
}
/**
* 通用异常信息
*/
public enum CommonError {
UNKOWN_ERROR("执行过程异常,请重试。"),
PARAMS_ERROR("非法参数"),
OBJECT_NULL("对象为空"),
QUERY_NULL("查询结果为空"),
REQUEST_NULL("请求参数为空");
private String errMessage;
public String getErrMessage() {
return errMessage;
}
private CommonError( String errMessage) {
this.errMessage = errMessage;
}
}
SpringBoot提供了JSR-303的支持,它就是spring-boot-starter-validation,它的底层使用Hibernate Validator,Hibernate Validator是Bean Validation 的参考实现。
所以,我们准备在Controller层使用spring-boot-starter-validation完成对请求参数的基本合法性进行校验。
前端请求后端接口传输参数,是在controller中校验还是在Service中校验?
答案是都需要校验,只是分工不同。因为某些Service不仅仅被一个Controller调用,如果Service中不写校验再被其他Controller或其他Service调用时也会发生问题
Controller里一般校验什么?
Contoller中校验请求参数的合法性,包括:必填项校验,数据格式校验,比如:是否是符合一定的日期格式,等。
Service里一般校验什么?
Service中要校验的是业务规则相关的内容,比如:课程已经审核通过所以提交失败。
Service中根据业务规则去校验不方便写成通用代码,Controller中则可以将校验的代码写成通用代码
首先在Base工程添加spring-boot-starter-validation的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
在javax.validation.constraints包下有很多这样的校验注解,直接使用注解定义校验规则即可
现在准备对内容管理模块添加课程接口进行参数校验,如下接口
定义好校验规则还需要开启校验,在controller方法中添加@Validated注解,否则校验不会生效
@ApiOperation("新增课程")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@Validated @RequestBody AddCourseDto addCourseDto) {
// 将来会集成SpringSecurity框架,用户登录之后就可以获取到用户所属机构的ID
// 先把机构ID写死
return courseBaseInfoService.createCourseBase(10086L,addCourseDto);
}
进入AddCourseDto类,在属性上添加校验规则
@NotEmpty表示属性不能为空
@Size表示限制属性内容的长短
/**
* @description 添加课程dto
*/
@Data
@ApiModel(value = "AddCourseDto", description = "新增课程基本信息")
public class AddCourseDto {
@NotEmpty(message = "课程名称不能为空")
@ApiModelProperty(value = "课程名称", required = true)
private String name;
@NotEmpty(message = "适用人群不能为空")
@Size(message = "适用人群内容过少", min = 10)
@ApiModelProperty(value = "适用人群", required = true)
private String users;
@ApiModelProperty(value = "课程标签")
private String tags;
@NotEmpty(message = "课程分类不能为空")
@ApiModelProperty(value = "大分类", required = true)
private String mt;
@NotEmpty(message = "课程分类不能为空")
@ApiModelProperty(value = "小分类", required = true)
private String st;
@NotEmpty(message = "课程等级不能为空")
@ApiModelProperty(value = "课程等级", required = true)
private String grade;
@ApiModelProperty(value = "教学模式(普通,录播,直播等)", required = true)
private String teachmode;
@ApiModelProperty(value = "课程介绍")
private String description;
@ApiModelProperty(value = "课程图片", required = true)
private String pic;
@NotEmpty(message = "收费规则不能为空")
@ApiModelProperty(value = "收费规则,对应数据字典", required = true)
private String charge;
@ApiModelProperty(value = "价格")
private Float price;
@ApiModelProperty(value = "原价")
private Float originalPrice;
@ApiModelProperty(value = "qq")
private String qq;
@ApiModelProperty(value = "微信")
private String wechat;
@ApiModelProperty(value = "电话")
private String phone;
@ApiModelProperty(value = "有效期")
private Integer validDays;
}
该异常表示在方法参数验证失败时抛出,其中验证失败的详细信息可以通过 BindingResult
对象获取
getBindingResult()
方法是 MethodArgumentNotValidException
类的一个方法,用于获取包含验证错误详细信息的 BindingResult
对象。BindingResult
包含了验证结果,包括错误对象、错误代码、默认错误消息等。
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {
// 这个异常对象有一个方法getBindingResult
BindingResult bindingResult = e.getBindingResult();
List<String> msgList = new ArrayList<>();
//将错误信息放在msgList
//因为校验的时候可能多个字段都会出现问题,所以使用了一个list集合存放有问题的消息,我们后面会将他们一块拼接起来的
bindingResult.getFieldErrors().stream().forEach(item -> msgList.add(item.getDefaultMessage()));
//拼接错误信息
String msg = StringUtils.join(msgList, ",");
log.error("【系统异常】{}", msg);
return new RestErrorResponse(msg);
}
比如下面的这一次请求中就是两个字段不合格,都会放到List集合中,最后由StringUtils.join将其拼接起来
假如修改课程的Dto和增加课程的Dto是一个样子的,但是校验规则不一样,怎么办呢?
方法1:定义两个Dto来分别校验
方法2:分组校验
分组校验:将Dto中的校验规则分组,新增课程使用一个检验规则组,修改课程使用一个校验规则组,但是都还是同一个Dto
定义分组
/**
* 用于分组校验,定义一些常用的组
*/
public class ValidationGroups {
public interface Insert{};
public interface Update{};
public interface Delete{};
}
划分组别
// @NotEmpty(message = "课程名称不能为空")
// 这个地方就说明了下面@NotEmpty校验属于ValidationGroups.Insert.class组别
@NotEmpty(message = "新增课程名称不能为空",groups = {ValidationGroups.Insert.class})
// 这个地方就说明了下面@NotEmpty校验属于ValidationGroups.Update.class组别
@NotEmpty(message = "修改课程名称不能为空",groups = {ValidationGroups.Update.class})
@ApiModelProperty(value = "课程名称", required = true)
private String name;
此时Controller中也需要修改
@ApiOperation("新增课程")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@Validated(ValidationGroups.Insert.class) @RequestBody AddCourseDto addCourseDto) {
// 将来会集成SpringSecurity框架,用户登录之后就可以获取到用户所属机构的ID
// 先把机构ID写死
return courseBaseInfoService.createCourseBase(10086L,addCourseDto);
}
假如说是修改的话,如下所示
public CourseBaseInfoDto updateCourseBase(@Validated(ValidationGroups.Update.class) @RequestBody AddCourseDto addCourseDto) {
...
}
如果javax.validation.constraints包下的校验规则满足不了需求怎么办?
1、手写校验代码 。
2、自定义校验规则注解。
上面的异常处理是在看学成在线的时候做的笔记,但是怎么看怎么别扭,特别是封装的响应结果集,很别扭,然后就再整理一份
系统如何处理异常?
我们自定义一个统一的异常处理器去捕获并处理异常
使用控制器增加注解@ControllerAdvice和异常处理注解@ExceptionHandler来实现
处理自定义异常
程序在编写代码时根据校验结果主动抛出自定义异常类对象,抛出异常时指定详细的异常信息,异常处理器捕获异常信息记录异常日志并响应给用户
处理自定义异常
接口执行过程中的一些运行时异常也会由异常处理器统一捕获,记录异常日志,统一响应给用户500错误(状态码由前后端协定)
在异常处理中还可以针对某个异常类型进行单独处理
/**
* 全局异常处理
* 底层是通过代理,代理controller,通过AOP把我们的一些方法拦截到,如果有异常,就在这个类统一进行处理
* 下面就是只要带有RestController.class, Controller.class,Service.class的注解的类或方法出现异常,我们都会进行统一处理
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class, Service.class}) // 通知
@ResponseBody //我们需要返回JSON数据
@Slf4j
public class GlobalExceptionHandler {
// 表示处理SQL异常
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
// 打印日志信息
log.error(ex.getMessage());//Duplicate entry 'zhangjingqi' for key 'employee.idx_username'
// 在这里也可以判断异常的具体信息,比如:
if(ex.getMessage().contains("Duplicate entry")){
String[] s = ex.getMessage().split(" ");
String username = s[2];
String msg =s[2]+"已经存在";
return R.error(msg);
}
// 其他情况下可以直接输出
return R.error("未知错误:"+ex.getMessage());
}
}
不论是成功响应还是异常响应,都使用下面这个结果集
/**
* 通用返回结果类,服务端响应的数据都会封装成此对象
* @param <T> 这个类会接受多种类型,可能是普通对象,可能是数组、集合等等等等,所以我们要将这个加个泛型<T>,表示可以接收任何参数
*/
// 为什么不用Object,而用<T>? 如果用object需要强转类型 而T不用
@Data
public class R<T> {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据
// 方法的返回值及参数中的T属于泛型
public static <T> R<T> success(T object) {
R<T> r = new R<T>();
r.data = object;
r.code = 1; //成功
return r;
}
public static <T> R<T> error(String msg) {
R r = new R();
r.msg = msg;
r.code = 0; //失败
return r;
}
public R<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
SpringMVC 中 @ControllerAdvice 注解的三种使用场景! - 江南一点雨 - 博客园 (cnblogs.com)
basePackages/basePackageClasses属性
指定扫描哪个包
@ControllerAdvice(basePackages = "com.zhangjingqi.controller")
public class GlobalExceptionHandler {
// ...
}
annotations属性
指定注解类型,限定只有被特定注解标记的控制器才会受到 @ControllerAdvice
类的影响
@ControllerAdvice(annotations = {RestController.class, Controller.class, Service.class}) // 通知
@ResponseBody //我们需要返回JSON数据
@Slf4j
public class GlobalExceptionHandler {
.......
}
assignableTypes属性
通过指定类类型,限定只有继承自特定类的控制器才会受到 @ControllerAdvice
类的影响
@ControllerAdvice(assignableTypes = MyController.class)
public class GlobalExceptionHandler {
// ...
}
value
与 basePackages
属性类似,用于指定扫描的包。在很多情况下,value
属性可以替代 basePackages
@ControllerAdvice(value = "com.zhangjingqi.controllers")
public class GlobalExceptionHandler {
// ...
}
useDefaultResponseAdvice
默认为 true
。当设置为 false
时,禁用默认的 ResponseEntityExceptionHandler
,这样你就可以完全掌控异常处理的行为
@ControllerAdvice(useDefaultResponseAdvice = false)
public class CustomExceptionHandler {
// ...
}
可以捕获到controller中抛出的一些自定义异常,统一进行处理,一般用于进行一些特定的异常处理
可以根据需要定义多个 @ExceptionHandler
方法,每个方法处理一种特定类型的异常
// 表示处理SQL异常
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
......
}
value
指定要处理的异常类型。可以是单个异常类或一组异常类
@ExceptionHandler(value = {NullPointerException.class, IllegalArgumentException.class})
public String handleSpecificExceptions(Exception e) {
// 处理特定类型的异常
return "处理特定类型的异常";
}
exceptions
与 value
属性类似,用于指定要处理的异常类型
@ExceptionHandler(exceptions = NullPointerException.class)
public String handleCustomException(Exception e) {
// 处理自定义异常
return "处理自定义异常";
}
basePackages/basePackageClasses
限定 @ExceptionHandler
方法的扫描范围,类似于 @ControllerAdvice
的属性
@ExceptionHandler(basePackages = "com.zhangjingqi.controllers")
public String handleExceptionsInControllerPackage(Exception e) {
// 处理指定包中的异常
return "packageError";
}