后台数据校验及统一异常处理及自定义校验注解

依赖:

        <!--jsr3参数校验器-->
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>2.3.2.RELEASE</version>
<!--            版本与springboot一致即可-->
        </dependency>

<!--        里面依赖了hibernate-validator,而hibernate-validator又是javax.validation的实现。javax.validation是标准-->

依赖的包有哪些注解可以参看:javax.validation.constraints

 

当然除了使用注解还可以使用正则表达式如;

	/**
	 * 检索首字母
	 */
	@Pattern(regexp="^[a-zA-Z]$",message="首字母必须为空")
	private String firstLetter;

备注:注解@NotBlank、@NotEmpty及@NotNull区别

1.@NotNull:
不能为null,但可以为empty(""," “,” ") ,一般用在基本数据类型的非空校验上,而且被其标注的字段可以使用 @size/@Max/@Min对字段数值进行大小的控制

2.@NotEmpty:
不能为null,而且长度必须大于0(" “,” "),一般用在集合类上面

3.@NotBlank:
这玩意只能作用在接收的String类型上,不能为null,而且调用trim()后,长度必须大于0

一、简单的数据校验

1.1实体类需要检验的字段加上注解@NotBlank(message="品牌名不能为空")

@Data
@TableName("xxx")
public class BrandEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 品牌名
	 */
	@NotBlank(message="品牌名不能为空")
	private String name;
    
	/**
	 * 其他字段省略....
	 */

}

1.2controller方法参数需要使用注解@Valid才会生效

    /**
     * 保存
     */
    @RequestMapping("/save")
    //@RequiresPermissions("product:brand:save")
    public R save(@Valid @RequestBody BrandEntity brand) {

         brandService.save(brand);
         return R.ok();
    
    }

 当发送请求:不携带name字段

POST http://localhost:xxxx/brand/save
Content-Type: application/json

{ "descript": "测试JSR303"}

请求结果如下:

{
    "timestamp": "2020-04-29T09:36:04.125+0000",
    "status": 400,
    "error": "Bad Request"
}

备注:虽然成功校验了数据但是@NotBlank(message="品牌名不能为空")里边的message没有生效且也没有使用默认的message,即:ValidationMessages.properties的默认message。原因有待解决。。

但是这种返回的错误结果并不符合我们的业务需要。接下来看第二

二、简单的数据校验以及使用 BindingResult 封装错误结果

2.1依赖,实体类同上

2.2改写controller方法

    /**
     * 保存
     */
    @RequestMapping("/save")
    //@RequiresPermissions("product:brand:save")
    public R save(@Valid @RequestBody BrandEntity brand, BindingResult result) {

        HashMap<String, String> stringStringHashMap = new HashMap<>();
        if (result.hasErrors()) {
            List<FieldError> fieldErrors = result.getFieldErrors();

            fieldErrors.forEach(item -> {
                stringStringHashMap.put(item.getField(), item.getDefaultMessage());
            });
            return R.error(400,"数据异常").put("data", stringStringHashMap);
        } else {
            brandService.save(brand);
            return R.ok().put("data", stringStringHashMap);
        }

    }

备注:R为系统统一返回数据的一个Map (自行封装)

给校验的Bean后,紧跟一个BindResult,就可以获取到校验的结果。拿到校验的结果,就可以自定义封装。

处理结果如下:

{
  "msg": "数据异常",
  "code": 400,
  "data": {
    "name": "品牌名不能为空"
  }
}

但是针对于每一个请求设置了一个内容校验以及错误封装,显然不是太合适,实际上可以统一的对于异常进行处理。

三、数据校验之统一异常处理

介绍:https://www.cnblogs.com/lenve/p/10748453.html

3.1 添加一个全局异常处理类  com.xxxx.exception.MyExceptionControllerAdvice ,并使用 注解 @RestControllerAdvice(basePackages = "com.xxxx.controller")指定扫描controller包

定义一个方法handleValidException,并使用注解@ExceptionHandler(value = Exception.class)处理器捕获异常。方法参数为MethodArgumentNotValidException,即校验异常

@Slf4j
@RestControllerAdvice(basePackages = "com.xxxx.controller")
public class MyExceptionControllerAdvice  {

    //@ResponseBody //上边用了RestControllerAdvice这里可以不写
    @ExceptionHandler(value = Exception.class)
    public R handleValidException(MethodArgumentNotValidException e){

        Map<String,String> map=new HashMap<>();
        BindingResult bindingResult = e.getBindingResult();
        bindingResult.getFieldErrors().forEach(fieldError ->{
            map.put(fieldError.getField(),fieldError.getDefaultMessage());
        });
        /**
         * 如果不确定异常类型为 MethodArgumentNotValidException
         * 可以先写Exception,然后使用
         * log.error("数据校验出现问题{},异常类型{}",e.getMessage(),e.getClass());
         * 看看具体的异常类型
         */

        return R.error(400,"数据校验异常").put("data",map);
    }

    //最后用于匹配上边未精确匹配到的其他异常
    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable){
        return R.error();
    }
}

3.2controller改回原来的样子

    /**
     * 保存
     */
    @RequestMapping("/save")
    //@RequiresPermissions("product:brand:save")
    public R save(@Valid @RequestBody BrandEntity brand/*, BindingResult result*/) {

/*        HashMap<String, String> stringStringHashMap = new HashMap<>();
        if (result.hasErrors()) {
            List<FieldError> fieldErrors = result.getFieldErrors();

            fieldErrors.forEach(item -> {
                stringStringHashMap.put(item.getField(), item.getDefaultMessage());
            });
            return R.error(400,"数据异常").put("data", stringStringHashMap);
        } else {
            brandService.save(brand);
            return R.ok().put("data", stringStringHashMap);
        }*/
        brandService.save(brand);
        return R.ok();

    }

发送异步请求:

POST http://xxxx/brand/save
Content-Type: application/json

{ "descript": "2测试JSR303"}

请求结果:

{
  "msg": "数据校验异常",
  "code": 400,
  "data": {
    "name": "品牌名不能为空"
  }
}

3.3 规范错误码

上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义

image-20200429183748249

为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码

com.xxxx.exception.BizCodeEnum

/***
 * 错误码和错误信息定义类
 * 1. 错误码定义规则为5为数字
 * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
 * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
 * 错误码列表:
 *  10: 通用
 *      001:参数格式校验
 *  11: 商品
 *  12: 订单
 *  13: 购物车
 *  14: 物流
 */
public enum BizCodeEnum {
    UNKNOW_EXEPTION(10000,"系统未知异常"),
    VALID_EXCEPTION(10001,"数据校验失败");

    private int code;
    private String msg;

    BizCodeEnum(int code,String msg){
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }

}

3.3.1 改造 MyExceptionControllerAdvice  

@Slf4j
@RestControllerAdvice(basePackages = "com.xxxx.controller")
public class MyExceptionControllerAdvice  {

    //@ResponseBody //上边用了RestControllerAdvice这里可以不写
    @ExceptionHandler(value = Exception.class)
    public R handleValidException(MethodArgumentNotValidException e){

        Map<String,String> map=new HashMap<>();
        BindingResult bindingResult = e.getBindingResult();
        bindingResult.getFieldErrors().forEach(fieldError ->{
            map.put(fieldError.getField(),fieldError.getDefaultMessage());
        });
        /**
         * 如果不确定异常类型为 MethodArgumentNotValidException
         * 可以先写Exception,然后使用
         * log.error("数据校验出现问题{},异常类型{}",e.getMessage(),e.getClass());
         * 看看具体的异常类型
         */

        return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(),BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data",map);
    }

    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable){
        return R.error();
    }
}

测试校验失败请求结果如下:

{
  "msg": "数据校验失败",
  "code": 10001,
  "data": {
    "name": "品牌名不能为空"
  }
}

 但是实际上业务并没有这么简单,在新增品牌数据时,name确实不能为空,但是如果我们是修改品牌的某一项数据呢,此时name是可以不携带的,此时我们需要用到 分组校验功能(完成多场景的复杂校验)

四、分组校验功能(完成多场景的复杂校验)

org.springframework.validation.annotation.Validated; @Validated注解提供了 Class<?>[] value() default {}; 对象数组,其实际需要的是一个空接口。

4.1添加分组接口(空接口即可,根据请情况添加,例如系统添加、更新操作都需要校验数据)

public interface AddGroup {
}

public interface UpdateGroup {
}

4.2 给entity标记分组。生效情况请看下边有描述

@Data
@TableName("xxx")
public class BrandEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 品牌id
	 */
	@TableId
	@Null(message = "不能携带品牌id",groups = {AddGroup.class})
	@NotNull(message = "品牌id不能为空",groups = {UpdateGroup.class})
	private Long brandId;
	/**
	 * 品牌名
	 */

	@NotBlank(message="品牌名不能为空",groups = {AddGroup.class,UpdateGroup.class})
	private String name;
	/**
	 * 品牌logo地址
	 */
	@NotBlank(message = "logo不能为空",groups = {AddGroup.class})
	@URL(message = "logo必须为一个URL",groups = {AddGroup.class,UpdateGroup.class})//不携带就不校验
	private String logo;
	/**
	 * 介绍
	 */
	private String descript;
	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	@NotNull(message = "显示状态不能为空",groups = {AddGroup.class})
	//@Pattern注解不能使用在Integer上报错:No validator could be found for constraint
	//Pattern只能用在String对象上
	//@Pattern(regexp = "^[0-1]$",message = "showStatus只能为0或者1",groups = {AddGroup.class,UpdateGroup.class})
	private Integer showStatus;
	/**
	 * 检索首字母
	 */
	@NotBlank(message = "检索字母不能为空",groups = {AddGroup.class})
	@Pattern(regexp="^[a-zA-Z]$",message="首字母必须为a-z或者A-z中的一个字母",groups = {AddGroup.class,UpdateGroup.class})
	private String firstLetter;
	/**
	 * 排序
	 */
	@NotNull(message = "排序字段不能为空",groups = {AddGroup.class})
	@Min(value = 0,message = "排序字段不能小于0",groups = {AddGroup.class,UpdateGroup.class})
	private Integer sort;

}

4.3 controller接口改写

    /**
     * 保存
     */
    @RequestMapping("/save")
    //@RequiresPermissions("product:brand:save")
    public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand) {

        brandService.save(brand);
        return R.ok();

    }

    /**
     * 修改
     */
    @RequestMapping("/update")
    //@RequiresPermissions("product:brand:update")
    public R update(@Validated(UpdateGroup.class)@RequestBody BrandEntity brand) {
        brandService.updateById(brand);

        return R.ok();
    }

4.4 测试

4.41 save接口测试

POST http://xxx/brand/save
Content-Type: application/json

{ "descript": "2测试JSR303",
  "name": "ss",
  "logo": "ddddhttp://baidu.com",
  "showStatus": "55",
  "sort": "66",
  "firstLetter": "ss"
}

结果:

{
  "msg": "数据校验失败",
  "code": 10001,
  "data": {
    "logo": "logo必须为一个URL",
    "firstLetter": "首字母必须为a-z或者A-z中的一个字母"
  }
}

修改后测试:

{ "descript": "2测试JSR303",
  "name": "ss",
  "logo": "http://baidu.com",
  "showStatus": "55",
  "sort": "66",
  "firstLetter": "s"
}

结果;


{
  "msg": "success",
  "code": 0
}

4.42 update接口测试:

{
  "descript": "2测试JSR303",
  "name": "ss",
  "logo": "sshttp://baidu.com",
  "showStatus": "55",
  "sort": "66",
  "firstLetter": "ss"
}

结果:

{
  "msg": "数据校验失败",
  "code": 10001,
  "data": {
    "brandId": "品牌id不能为空",
    "logo": "logo必须为一个URL",
    "firstLetter": "首字母必须为a-z或者A-z中的一个字母"
  }
}

 测试完毕,update成功就不测了。

@Validated的生效有两种情况:

1、单纯使用@Validated注解,不添加 Class<?>[] value() default {};属性。此时entity上没有使用group属性的字段校验才会生效

如:BrandEntity.calss

@Data
@TableName("xxx")
public class BrandEntity implements Serializable {
	private static final long serialVersionUID = 1L;


	/**
	 * 品牌名
	 */
	@NotBlank(message="品牌名不能为空",groups = {AddGroup.class})
	private String name;

	/**
	 * 检索首字母
	 */
	@Pattern(regexp="^[a-zA-Z]$",message="首字母必须为空")
	private String firstLetter;

	/**
	 * 其他字段省略...
	 */    

}

 controller:

    /**
     * 保存
     */
    @RequestMapping("/save")
    //@RequiresPermissions("product:brand:save")
    public R save(@Validated @RequestBody BrandEntity brand/*, BindingResult result*/) {

        brandService.save(brand);
        return R.ok();

    }

此时BrandEntity中,name字段的校验是不生效的,因为controller的方法使用的是@Validated注解没有携带AddGroup.class; 而firstLetter的校验是生效的,也就是说使用@Validated注解不携带group作用同@Valid。

2、使用@Validated(XxxxGroup.class)注解,实体类字段的校验必须带上group={XxxxGroup.cass}校验才会生效。

如,此时save操作,name,firstLetter两个字段校验都会生效。

   @Data
@TableName("xxx")
public class BrandEntity implements Serializable {
	private static final long serialVersionUID = 1L;


	/**
	 * 品牌名
	 */
	@NotBlank(message="品牌名不能为空",groups = {AddGroup.class})
	private String name;

	/**
	 * 检索首字母
	 */
	@Pattern(regexp="^[a-zA-Z]$",message="首字母必须为空",group= {AddGroup.class})
	private String firstLetter;

	/**
	 * 其他字段省略...
	 */    

}




 /**
     * 保存
     */
    @RequestMapping("/save")
    //@RequiresPermissions("product:brand:save")
    public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand/*, BindingResult result*/) {

        brandService.save(brand);
        return R.ok();

    }

五、自定义校验注解

自定义校验注解步骤:

  1. 自定义注解
  2. 自定义校验器
  3. 关联注解和校验器

5.1 自定义校验注解

直接参考任意一个注解,例如参考@NotBlank注解


@Documented
@Constraint(
    validatedBy = {}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(NotBlank.List.class)
public @interface NotBlank {
    String message() default "{javax.validation.constraints.NotBlank.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        NotBlank[] value();
    }
}

发现需要以下三个字段:

    String message() default "{javax.validation.constraints.NotBlank.message}"; //错误信息

    Class<?>[] groups() default {};//分组

    Class<? extends Payload>[] payload() default {};//自定义负载信息

且需要的元注解有:注解的注解,称为元注解。

@Documented
@Constraint(
    validatedBy = {}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)

其中

@Documented 这个注解只是用来标注生成javadoc的时候是否会被记录。可以不用。

@Constraint 这个注解是最重要的一个注解,用于关联我们将要自定义的校验器,可以关联多个校验器。

@Target 标注注解可以标注的地方如类、方法、字段等
@Retention注解有一个属性value,是RetentionPolicy类型的,Enum RetentionPolicy是一个枚举类型,
这个枚举决定了Retention注解应该如何去保持,也可理解为Rentention 搭配 RententionPolicy使用。RetentionPolicy有3个值:CLASS  RUNTIME   SOURCE
按生命周期来划分可分为3类:
1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;

这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。
那怎么来选择合适的注解生命周期呢?
首先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。
一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解,比如@Deprecated使用RUNTIME注解
如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;
如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,使用SOURCE 注解。

@Repeatable  https://blog.csdn.net/weixin_42245133/article/details/99678509

所以假设我们需要使用的自定义注解@ListValue(vals={0,1,x,xx,xxx})为这样的形式用于校验以下字段

	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	@NotNull(message = "显示状态不能为空",groups = {AddGroup.class})
	//@Pattern注解不能使用在Integer上报错:No validator could be found for constraint
	//Pattern只能用在String对象上
	//@Pattern(regexp = "^[0-1]$",message = "showStatus只能为0或者1",groups = {AddGroup.class,UpdateGroup.class}) //x
	private Integer showStatus;

 @ListValue注解如下:

@Documented
@Constraint(
        validatedBy = {}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {

    String message() default "{javax.validation.constraints.NotBlank.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    //vals
    int [] vals() default {};
}

5.2 自定义校验器

我们点击 @Constraint( validatedBy = {})中的validateBy发现,他需要的是一个 ConstraintValidator

@Documented
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraint {
    Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}

ConstraintValidator如下所示:是一个接口,那么我们可以自定义一个该接口的实现作为我们的校验器

A是我们自定义的注解,T是我们需要校验的字段的类型

public interface ConstraintValidator<A extends Annotation, T> {
    default void initialize(A constraintAnnotation) {
        //初始化数据 A就是我们自定义的注解
    }
    //
    boolean isValid(T var1, ConstraintValidatorContext var2);//
}
ListValueConstraintValidatorForInteger接口如下:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;

public class ListValueConstraintValidatorForInteger implements ConstraintValidator<ListValue, Integer> {

    HashSet set = new HashSet<Integer>();

    @Override
    public void initialize(ListValue constraintAnnotation) {
        // 初始化数据
        int[] vals = constraintAnnotation.vals();
        if (vals.length > 0 && vals != null) {
            for (int i = 0; i < vals.length; i++) {
                set.add(vals[i]);
            }
        }

    }

    @Override
    public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
        //校验数据 integer就是我们需要检验的数据
        
        return set.contains(integer);
    }
}

5.3 关联自定义注解以及自定义校验器

只需要在自定义注解上的@constraint元注解上加上自定义校验器即可,可以填多个

@Documented
@Constraint(
        validatedBy = {ListValueConstraintValidatorForInteger.class}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {

    String message() default "{javax.validation.constraints.NotBlank.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    //vals
    int [] vals() default {};
}

5.4 测试

	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	@ListValue(vals = {0,1},message = "显示状态必须为0或1",groups = {AddGroup.class,UpdateGroup.class})
	@NotNull(message = "显示状态不能为空",groups = {AddGroup.class})
	//@Pattern注解不能使用在Integer上报错:No validator could be found for constraint
	//Pattern只能用在String对象上
	//@Pattern(regexp = "^[0-1]$",message = "showStatus只能为0或者1",groups = {AddGroup.class,UpdateGroup.class}) //x
	private Integer showStatus;

 请求:

POST http://xxx/update
Content-Type: application/json

{
  "brandId": "23",
  "descript": "SSS试JSR303",
  "name": "ss",
  "logo": "http://baiSSSSdu.com",
  "showStatus": "55",
  "sort": "66",
  "firstLetter": "s"
}

 结果:

{
  "msg": "数据校验失败",
  "code": 10001,
  "data": {
    "showStatus": "显示状态必须为0或1"
  }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值