数据校验JSR303快速入门(简单使用、分组效验、自定义注解效验)

  • Post author:
  • Post category:其他


前言:

在实际开发中,除了前端需要在表单中验证用户的输入。后台服务也需要对用户传入的参数进行效验,避免他人在得知请求格式后,直接通过类似Postman这样的测试工具进行非常数据请求。



JSR303是什么

JSR303是一套JavaBean参数校验的标准,定义了很多常用的校验注解

可以直接将这些注解加在我们JavaBean的属性上面就可以在需要校验的时候进行校验了

接下来以添加品牌为例



简单的数据效验

我们可以使用

javax.validation.constraints

包中提供的注解,给实体类字段添加规则。

在这里插入图片描述



1、给实体类添加验证规则

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

	@TableId
	private Long brandId;

	//约束name不能为null,且至少有一个非空字符
	@NotBlank(message = "品牌名必须提交")
	private String name;

	@NotBlank(message = "logo不能为空")
	//URL是hibernate提供的注解,实现了JSR303规范。约束如果logo不为null的话,必须符合url格式
	@URL(message = "logo格式不符")		
	private String logo;

	private String descript;

//	@Pattern(regexp = "[0-1]")		//pattern不支持Integer
	private Integer showStatus;

	//使用正则表达式约束字段
	@Pattern(regexp = "^[a-zA-Z]$" , message = "首字母必须是一个字母")
	private String firstLetter;

	@Min(value = 0 , message = "排序字段必须大于等于0")
	private Integer sort;
}



2、controller层使用

@Valid

开启验证功能

    @RequestMapping("/save")
    public R save(@Valid @RequestBody BrandEntity brand){
		brandService.save(brand);

        return R.ok();
    }



3、使用postman测试验证

在这里插入图片描述

至此,已经完成了最简单的后台数据效验。步骤很简单:

  1. 使用注解对实体类字段进行约束
  2. 使用@Valid开启数据验证功能



使用BindingResult处理异常

在上例中,虽然我们完成了数据验证,但是返回给前端的数据并不友好。在项目中应该返回一个统一的结果。

使用BindingResult接受参数效验的结果

  @RequestMapping("/save")
    //@RequiresPermissions("product:brand:save")
    public R save(@Valid @RequestBody BrandEntity brand, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            //1.出现参数非法情况
            Map<String, String> map = new HashMap<>();
            bindingResult.getFieldErrors().forEach(fieldError -> {
                map.put(fieldError.getField(), fieldError.getDefaultMessage());
            });
            return R.error(400, "提交的数据不合法").put("data", map);
        } else {
            //2.参数验证通过, 执行正常逻辑
            brandService.save(brand);
            return R.ok();
        }
    }

再次测试:

在这里插入图片描述

非常nice



统一异常处理

添加BindingResult参数后,虽然可以使后台在出现异常时,进行处理并返回统一的结果。但是我们会发现,我们写了许多与业务不相关的代码,为了解决这个问题,我们可以通过

@ControllerAdvice

进行异常的统一处理。

1、编写统一异常处理类

//@RestControllerAdvice其实就是@ControllerAdvice+@ResponseBody,表示返回的是json格式
@RestControllerAdvice
@Slf4j
public class GlobalExceptionControllerAdvice {

    //捕获MethodArgumentNotValidException类型的异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R handlerMethodArgumentNotValidException(MethodArgumentNotValidException e){
        BindingResult bindingResult = e.getBindingResult();

        Map<String, String> map = new HashMap<>();
        bindingResult.getFieldErrors().forEach(fieldError -> {
            map.put(fieldError.getField(), fieldError.getDefaultMessage());
        });
        return R.error(400,"提交的数据不合法").put("data",map);
    }

    //兜底
    @ExceptionHandler(Exception.class)
    public R handlerException(Exception e){
        return R.error(10000,"未知的系统异常").put("data",e.getMessage());
    }
}

2、把之前加的BindingResult去掉,还原成原先最干净的代码

    @RequestMapping("/save")
    public R save(@Valid @RequestBody BrandEntity brand) {
        brandService.save(brand);
        return R.ok();
    }

测试结果一样的。




code状态码

在我们后台的返回结果中,有个code状态码,前端可以根据这个状态码判断是出现了什么问题,如何解决。就好像是我们进行http请求时,我们知道200响应码代表请求成功、404代表找不到资源、500代表服务器出错等。

随着业务越来越复杂,异常的类型越来越多,为了统一规范,我们就不该将code状态码写死在代码中,而应该统一管理起来。

我们可以使用枚举类进行管理,如下。

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

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

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

2、修改我们全局异常处理类中的代码

@RestControllerAdvice
@Slf4j
public class GlobalExceptionControllerAdvice {

    //捕获MethodArgumentNotValidException类型的异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R handlerMethodArgumentNotValidException(MethodArgumentNotValidException e){
        BindingResult bindingResult = e.getBindingResult();

        Map<String, String> map = new HashMap<>();
        bindingResult.getFieldErrors().forEach(fieldError -> {
            map.put(fieldError.getField(), fieldError.getDefaultMessage());
        });
        return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",map);
    }

    //兜底
    @ExceptionHandler(Exception.class)
    public R handlerException(Exception e){
        return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg()).put("data",e.getMessage());
    }
}



分组效验

在简单的数据验证中,我们使用完成了数据验证。但是还存在一些问题,如在添加品牌的时候

brandId

为null,但在修改品牌的时候

brandId

不能为null,这样的话,就冲突了。

那怎么办呢?我们可以给他们分个组,添加操作使用一组验证规则,修改操作使用一组验证规则。这就是分组验证的功能。

以@NotNull注解为例

@Constraint(validatedBy = { })
public @interface NotNull {

	String message() default "{javax.validation.constraints.NotNull.message}";
	
	//分组验证时使用
	Class<?>[] groups() default { };
	...

我们通过

@NotNul

注解的groups指定属于哪个组


实现步骤:

1、创建

AddGroup



UpdateGroup

接口分别表示添加组和更新组

//这俩个接口只是用来标记的,不需要实现
public interface AddGroup {
}

public interface UpdateGroup {
}

2、实体类中使用注解时,标明该验证规则属于哪个组

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

	@TableId
	//只有在AddGroup组才会生效
	@Null(message = "添加操作  不要传brandId",groups = AddGroup.class)
	//只有在UpdateGroup组才会生效
	@NotNull(message = "修改操作  brandId不能为null",groups = UpdateGroup.class)
	private Long brandId;

	@NotBlank(message = "添加操作 品牌名必须提交",groups = AddGroup.class)
	private String name;

	@NotBlank(message = "添加操作 logo不能为空",groups = AddGroup.class)
	//在AddGroup组和UpdateGroup组中都会生效
	@URL(message = "logo格式不符",groups = {AddGroup.class,UpdateGroup.class})		//
	private String logo;

	private String descript;

	/**
	 * 显示状态[0-不显示;1-显示]
	 */
//	@Pattern(regexp = "[0-1]")		//pattern不支持Integer
	private Integer showStatus;

	@Pattern(regexp = "^[a-zA-Z]$" , message = "首字母必须是一个字母" , groups = {AddGroup.class,UpdateGroup.class})
	private String firstLetter;

	@Min(value = 0 , message = "排序字段必须大于等于0",groups = {AddGroup.class,UpdateGroup.class})
	private Integer sort;
}


3、使用@Validated替代@Valid

@Validated是@Valid的变体,它支持分组效验功能

    @RequestMapping("/save")
    //使用AddGroup组中的验证规则
    public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand) {
        brandService.save(brand);
        return R.ok();
    }
    
  @PutMapping("/update")
  //使用UpdateGroup组中的验证规则
    public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand) {
        brandService.updateById(brand);
        return R.ok();
    }

4、测试

测试添加操作:

在这里插入图片描述

测试修改操作:

在这里插入图片描述

至此分组验证功能完成



自定义效验注解

	/**
	 * 显示状态[0-不显示;1-显示]
	 */
//	@Pattern(regexp = "[0-1]")		//pattern不支持Integer
	private Integer showStatus;

在@Pattern的注释中,有下面这一段话,说明了该注解不支持Interger类型。那怎么办呢?

Accepts {@code CharSequence}. {@code null} elements are considered valid.

当提供的验证规则中没有我们需要的时,它支持我们自定义验证规则。(我知道有办法实现只能0和1,我只是想说可以自定义效验注解,别杠( o=^•ェ•)o ┏━┓)



自定义效验注解步骤:

  1. 添加依赖
  2. 编写一个自定义的效验注解
  3. 编写一个自定义的效验器
  4. 关联自定义的效验器和自定义的效验注解


1、添加依赖

        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>


2、编写一个自定义的效验注解

该注解的功能,验证输入的参数是否在value中。

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
//指定使用哪个效验器,如果不指定的话,就需要在初始化的时候指定
//可以指定多个不同的效验器,适配不同类型的效验
@Constraint(validatedBy = { ListValueConstraintValidator.class})
public @interface ListValue {
    //JSR303规范中,要求必须有message、groups、payload这三个方法
    //default: 当message为null时,默认会到ValidationMessages.properties配置文件中找com.fcp.common.valid.ListValue.message的值
    String message() default "{com.fcp.common.valid.ListValue.message}";

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

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

	//用来存放符合规则的数字
    int[] value();
}

3、在工程resource中创建ValidationMessages.properties配置文件

com.fcp.common.valid.ListValue.message=The committed number is not in the specified array


4、编写一个自定义的效验器

/**
* ListValue:使用的效验注解类型
* Integer: 被验证目标类型。我们验证的目标都是数字所以是Integer
*/
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
    private Set<Integer> contain = new HashSet<>();

    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] values = constraintAnnotation.value();
        if (values==null) return;
		
		//将符合规则的值放到容器中
        for (int value : values) {
            contain.add(value);
        }
    }

	//该方法判断参数合不合法
    //value是需要验证的值,即用户输入的参数
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
    	//返回用户输入的参数是否在容器中
        return contain.contains(value);
    }
}


5、关联自定义的效验器和自定义的效验注解


第二步已经做了,就是这个

@Constraint(validatedBy = { ListValueConstraintValidator.class})

至此,自定义效验器完成,可以开心的使用了:

	//表示输入的参数,必须要在value指定的数组中,也就是0和1
    @ListValue(value = {0, 1},groups = {AddGroup.class,UpdateGroup.class})
    private Integer showStatus;

测试:

在这里插入图片描述



版权声明:本文为weixin_44335140原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。