统一异常处理
前面我们后端数据校验每个 controller 校验的 bean 的属性上添加上校验注解,然后在 controller 入参上加上@Valid 注解,然后通过紧跟着的 BindingResult 来进行获取校验的结果,如果错误则返回给前端,否则正常入库。这样有一个弊端就是每个 controller 都需要进行一套将错误信息封装到 Request 中的过程,这是十分重复的。我们可以利用 Advice 来解决。
- 那么我们如果获取数据校验错误的信息 呢?
如果我们不添加 BindResult 来绑定抛出的结果的话,controller 则会把异常抛出,我们可以通过抛出的特定异常**@ExceptionHandler**来捕捉
- 如何获取 Controller 层发出的异常
通过注解**@ControllerAdvice或@RestControllerAdvice**注解
一、Controller 层同一异常处理
1.数据对象添加校验注解
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须不为空")
private String name;
/**
* 品牌logo地址
*/
@NotEmpty
@URL(message="logo必须是一个合法的url地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
@Pattern(regexp="/^[a-zA-Z]$/", message = "检索首字符必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value=0, message="排序必须大于等于0")
private Integer sort;
}
2.Controller 层校验入参添加@Valid 注解
/**
* 保存,方法入参添加@Valid进行参数校验,校验发生失败则会抛出异常不会入库,就会被异常捕捉类捕捉到
*/
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(@Valid @RequestBody BrandEntity brand/** ,BindingResult bindingResult*/) {
// if(bindingResult.hasErrors()){
// Map<String, String> map = new HashMap<>();
// // 1、获取校验的错误结果
// bindingResult.getFieldErrors().forEach(item ->{
// // 获取到错误提示
// String errorMessage = item.getDefaultMessage();
// // 获取错误的属性名
// String field = item.getField();
// map.put(field, errorMessage);
// });
// return R.error(400, "请求的数据不合法").put("data", map);
// }else{
// brandService.save(brand);
// }
brandService.save(brand);
return R.ok();
}
3.定义 Controller 层的异常捕捉类
@Slf4j // lomback提供
// @ControllerAdvice(basePackages = "com.lookstarry.doermail.product.controller") //basePackages表明处理哪些controller的异常
// @ResponseBody
@RestControllerAdvice(basePackages = "com.lookstarry.doermail.product.controller")
public class DoermailExceptionControllerAdvice {
@ExceptionHandler(value = MethodArgumentNotValidException.class) //捕捉特定的异常
public R handleValidException(MethodArgumentNotValidException e) { // 方法参数就是自动捕捉到的异常
log.error("数据校验出现问题:{},异常类型:{}", e.getMessage(), e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String, String> map = new HashMap<>();
bindingResult.getFieldErrors().forEach(item -> {
map.put(item.getField(), item.getDefaultMessage());
});
return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(), BizCodeEnum.VALID_EXCEPTION.getMessage()).put("data", map);
}
// 如果异常没有被其他方法捕捉到,则进入该方法,Throwable是所有Exception和Error的基类
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable) {
return R.error(BizCodeEnum.UNKNOW_EXCEPTION.getCode(), BizCodeEnum.UNKNOW_EXCEPTION.getMessage());
}
}
可以通过将@ControllerAdvice 和@ResponseBody 合并为一个,使用@RestControllerAdvice 4.测试
我们特定调用 save 接口,让 name 为空字符串,则会在 name 上校验错误,被异常捕捉类捕捉到
二、分组校验
上述校验逻辑还是存在一点问题的,例如我们的 brandId 应该在调用 save 接口时校验不能指定主键,而在更新或在删除时应该要求主键不能为空,这样通过不同的策略来设置校验的规则,就可以使用分组校验
我们可以使用校验注解中的groups 属性,表明该校验注解适用于哪一套策略,这时候 Controller 层校验注解就要从@Valid 转为@Validated(groups={})指定采用哪种校验策略
1.定义校验策略接口
对应上 groups 中的 Class 属性,接口中方法体为空,仅做标识作用,无具体含义
// 新增策略分组
public interface AddGroup {
}
// 更新策略分组
public interface UpdateGroup {
}
2.Bean 对象校验注解添加 groups 属性
表明该校验注解适用于哪一套策略下
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@NotNull(message="修改必须指定ID", groups = {UpdateGroup.class})
@Null(message="新增不能指定ID", groups = {AddGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
// 要求更新时该字段不能更新为空值
@NotBlank(message = "品牌名必须不为空", groups={AddGroup.class, UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotEmpty(groups={AddGroup.class})
@URL(message="logo必须是一个合法的url地址", groups={AddGroup.class, UpdateGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(groups={AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals={0,1}, groups={AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(groups={AddGroup.class})
@Pattern(regexp="^[a-zA-Z]$", message = "检索首字符必须是一个字母", groups={AddGroup.class, UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(groups={AddGroup.class})
@Min(value=0, message="排序必须大于等于0", groups={AddGroup.class, UpdateGroup.class})
private Integer sort;
}
3.Controller 层方法入参@Validated 标明使用哪套校验策略
@Valid 是 java 提供的一套数据校验标准,其中是没有任何属性的,这里我们为使用分组的策略,采用 Spring 提供的@Validated 校验注解
@RequestMapping("/save")
// 新增时@Validated表示需要校验,同时value标识使用新增时校验策略,则此时BrandEntity所有有groups的校验注解且为AddGroup.class都会启用
// 其他没有groups属性以及有groups属性但不是AddGroup.class不会被启用
public R save(@Validated(value={AddGroup.class}) @RequestBody BrandEntity brand/** ,BindingResult bindingResult*/) {
brandService.save(brand);
return R.ok();
}
/**
* 修改
*/
@RequestMapping("/update")
//@RequiresPermissions("product:brand:update")
// 新增时@Validated表示需要校验,同时value标识使用新增时校验策略,则此时BrandEntity所有有groups的校验注解且为UpdateGroup.class都会启用
// 其他没有groups属性以及有groups属性但不是UpdateGroup.class不会被启用
public R update(@Validated(value={UpdateGroup.class}) @RequestBody BrandEntity brand) {
brandService.updateById(brand);
return R.ok();
}
4.测试
三、总结:
使用统一异常处理步骤
1)在校验的 Bean 属性上使用 javax.validation.constraints 或 Hibernate 或正则注解@Pattern 或自定义注解 2) Controller 相应方法入参的 Bean 上添加@Valid 注解
3)编写异常处理类:使用@RestControllerAdvice(@ControllerAdvice + @ResponseBody) ,basePackage 属性表明需要处理哪些 controller 的异常
4)使用@ExceptionHandler 标注方法专门处理的异常
5)分组校验
(1) 新建策略接口(仅做标识哪套策略作用)
(2) 给校验注解添加上 groups 属性表明什么情况才需要使用该校验注解
(3)Controller 参数校验从@Valid 改为@Validated 并用 groups 属性表明该方法使用哪种校验策略
(4) @Validated 在没有指定 groups 分组的情况下,那些校验注解没有 groups 属性的将会被校验,而那些有 groups 属性的校验注解则不会被校验
本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 (CC BY-NC-ND 4.0) 进行许可。