一止长渊

统一异常处理

N 人看过
字数:1.7k字 | 预计阅读时长:7分钟

前面我们后端数据校验每个 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 上校验错误,被异常捕捉类捕捉到
image.png

二、分组校验

上述校验逻辑还是存在一点问题的,例如我们的 brandId 应该在调用 save 接口时校验不能指定主键,而在更新或在删除时应该要求主键不能为空,这样通过不同的策略来设置校验的规则,就可以使用分组校验
我们可以使用校验注解中的groups 属性,表明该校验注解适用于哪一套策略,这时候 Controller 层校验注解就要从@Valid 转为@Validated(groups={})指定采用哪种校验策略
截屏2021-04-03 23.13.21.png 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.测试

截屏2021-04-05 18.28.28.png

三、总结:

使用统一异常处理步骤
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) 进行许可。