服务器校验数据-JSR303校验

130 阅读5分钟

为什么要在服务器校验数据?

对于请求数据的校验通常会在前端数据进行校验,但是 “前端防君子、后端防小人”,为了避免恶意跳过前端使用类似postman等工具向后端发起请求导致数据不规范,所以可以使用JSR303便捷的校验后端数据的正确性。

1. JSR303简单使用

JSR303校验

JSR是Java Specification Requests的缩写,意思是Java规范提案,JSR-303是JAVA EE6中的一项子规范,叫做Bean Validation即,JSR 303,Bean Validation规范 ,为Bean验证定义了元数据模型和API。 默认的元数据模型是通过Annotations来描述的,但是也可以使用XML来重载或者扩展。

如何导入使用

maven导入坐标

<!-- JSR参数校验器 -->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

可以通过javax.validation.constraints包下查看相应注解

image.png

列举常用注解的使用

注解解释
@NotNull属性不能为null,无法查检长度为0的字符串
@NotBlank检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格
@NotEmpty该字段不能为null或""
@Email被注释的元素必须是电子邮箱地址
@Digits被注释的元素必须是数字
@Length被注释的字符串的大小必须在指定的范围内
@Range被注释的元素必须在合适的范围内
@Pattern(value)被注释的元素必须符合指定的正则表达式
@Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值

使用步骤

① 在需要校验的实体类添加注解

先随便进入其中一个注解看看源码

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(NotNull.List.class)
@Documented
@Constraint(
    validatedBy = {}
)
public @interface NotNull {
    String message() default "{javax.validation.constraints.NotNull.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 {
        NotNull[] value();
    }
}

可以看到 message 默认使用 javax.validation.constraints.NotNull.message

这个信息可以在 ValidationMessages_zh_CN.properties 下找到相应的 message

同理在添加注解的时候可以通过message进行自定义信息覆盖掉默认的信息

javax.validation.constraints.AssertFalse.message     = 只能为false
javax.validation.constraints.AssertTrue.message      = 只能为true
javax.validation.constraints.DecimalMax.message      = 必须小于或等于{value}
javax.validation.constraints.DecimalMin.message      = 必须大于或等于{value}
javax.validation.constraints.Digits.message          = 数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
javax.validation.constraints.Email.message           = 不是一个合法的电子邮件地址
javax.validation.constraints.Future.message          = 需要是一个将来的时间
javax.validation.constraints.FutureOrPresent.message = 需要是一个将来或现在的时间
javax.validation.constraints.Max.message             = 最大不能超过{value}
javax.validation.constraints.Min.message             = 最小不能小于{value}
javax.validation.constraints.Negative.message        = 必须是负数
javax.validation.constraints.NegativeOrZero.message  = 必须是负数或零
javax.validation.constraints.NotBlank.message        = 不能为空
javax.validation.constraints.NotEmpty.message        = 不能为空
javax.validation.constraints.NotNull.message         = 不能为null
javax.validation.constraints.Null.message            = 必须为null
javax.validation.constraints.Past.message            = 需要是一个过去的时间
javax.validation.constraints.PastOrPresent.message   = 需要是一个过去或现在的时间
javax.validation.constraints.Pattern.message         = 需要匹配正则表达式"{regexp}"
javax.validation.constraints.Positive.message        = 必须是正数
javax.validation.constraints.PositiveOrZero.message  = 必须是正数或零
javax.validation.constraints.Size.message            = 个数必须在{min}和{max}之间

接下来为实体类属性进行注解

/**
 * 品牌实体类
 *
 * @author 兴趣使然的L
 */
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {

   private static final long serialVersionUID = 1L;

   /**
    * 品牌id
    */
   @NotNull(message = "修改必须指定品牌id")
   @Null(message = "新增不能指定id")
   @TableId
   private Long brandId;
   
   /**
    * 品牌名
    */
   @NotBlank(message = "品牌名必须提交")
   private String name;
   
   /**
    * 品牌logo地址
    */
   @NotBlank
   @URL(message = "logo必须是一个合法的url地址")
   private String logo;
   
   /**
    * 介绍
    */
   private String descript;
   
   /**
    * 显示状态[0-不显示;1-显示]
    */
   @NotNull
   private Integer showStatus;
   
   /**
    * 检索首字母
    */
   @NotEmpty
   @Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母")
   private String firstLetter;
   
   /**
    * 排序
    */
   @NotNull
   @Min(value = 0,message = "排序必须大于等于0")
   private Integer sort;
}

② 在对应controller的接口方法上加上校验注解@Valid,开启校验

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

③ 对校验报错进行全局异常捕获处理

通过postman对接口进行测试 当不进行捕获处理时返回的结果如下

{
    "timestamp": "2020-04-29T09:36:04.125+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "NotBlank.brandEntity.name",
                "NotBlank.name",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "brandEntity.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "品牌名必须非空",
            "objectName": "brandEntity",
            "field": "name",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "NotBlank"
        }
    ],
    "message": "Validation failed for object='brandEntity'. Error count: 1",
    "path": "/product/brand/save"
}

可以看到返回的信息又臭又长(虽然很详细),并非真正想要获取的信息,可以通过异常处理返回自己指定格式的数据。

/**
 * 集中处理所有异常(全局异常处理)
 *
 * @author 兴趣使然的L
 */
@Slf4j
@RestControllerAdvice(basePackages = "com.xqsr.gulimall.product.controller")
public class ExceptionControllerAdvice {
    
    /**
     * 对数据校验失败的异常进行捕获并返回数据
     */
    @ExceptionHandler(value= MethodArgumentNotValidException.class)
    public R handleVaildException(MethodArgumentNotValidException e){
        log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass());
        
        // BindingResult 可以获取校验的结果
        BindingResult bindingResult = e.getBindingResult();
        
        // 自定义封装格式
        Map<String,String> errorMap = new HashMap<>();
        
        // 遍历属性错误信息通过自定义的map进行挨个封装
        bindingResult.getFieldErrors().forEach((fieldError)->{
            errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
        });
        
        // 返回数据
        return R.error(400, "参数格式校验失败").put("data",errorMap);
    }
}

再次通过postman进行测试

image.png


2. 分组校验使用(适合多场景校验)

对于实体类的操作可能有新增和修改等操作,对于不同的操作可能会存在不同的情况,如新增时要求某一个属性不能为空,但修改时允许该属性可以为空,此时就需要按不同场景校验,此时就可以使用分组校验属性group

使用步骤

① 创建分组接口:AddGroup.javaUpdateGroup.java

通过上面的源码分析可以看到注解中具有 Class<?>[] groups() default {}; 属性,可以看到这是一个类属性,所以需要创建类来标识分组,类中可以不带任何内容

/**
 * 添加操作分组(用于验证操作时指定分组操作)
 *
 * @author 兴趣使然的L
 */
public interface AddGroup {
}
/**
 * 修改操作分组(用于验证操作时指定分组操作)
 *
 * @author 兴趣使然的L
 */
public interface UpdateGroup {
}

② 在校验属性的注解上添加 group 属性分组

/**
 * 品牌实体类
 *
 * @author 兴趣使然的L
 */
@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地址
    */
   @NotBlank(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})
   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;
}

③ 在controller的接口方法上添加 @Validated(XXX.class) 注解修饰参数

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

通过如上三个步骤就可以完成分组校验操作,对不同的操作可以进行不同的校验处理


3. 自定义校验注解使用

可能存在对于某一些属性的校验,使用给定的注解无法满足校验要求,此时就需要自定义注解

需求:对于实体类的showStatus属性进行限定值只能在0和1中取值

使用步骤

① 创建自定义注解

/**
 * 自定义验证注解
 * ListValue 用于限制 Integer 类型的属性只能使用vals指定范围的值
 *
 * @author 兴趣使然的L
 */
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class }) // 这个注解用来指定校验器
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
    // 报错信息
    String message() default "{com.xqsr.common.valid.ListValue.message}";
    // 分组信息
    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
    
    // 自定义限制信息(只允许使用在vals数组内的值)
    int[] vals() default { };
}

补充:@Constraint(validatedBy = { XXX1.class, XXX2.class })可以匹配多个注解器

② 创建ValidationMessages.properties用来自定义默认信息

可以看到上述的报错信息使用的是com.xqsr.common.valid.ListValue.message(自定义信息)

com.xqsr.common.valid.ListValue.message=未使用指定范围的值

注意: 小bug会出现乱码问题,需要更改字符编码(可以在idea的设置中的文件编码处更改默认编码为UTF-8,更改后再创建properties文件即可)

image.png

③ 创建自定义的校验器(实现 ConstraintValidator<A extends Annotation, T> 接口,其中A指自定义的注解, T指该注解需要修饰的类型)

实现接口的两个方法:初始化方法 与 校验方法

因为需求是限定值的范围,所以这里直接使用HashSet集合进行校验

/**
 * 自定义验证器
 *
 * @author 兴趣使然的L
 */
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {

    private final Set<Integer> set = new HashSet<>();

    /**
     * 初始化方法
     */
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] vals = constraintAnnotation.vals();
        for (int val : vals) {
            set.add(val);
        }
    }

    /**
     * 校验方法
     * @param value 需要校验的值
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return set.contains(value);
    }
}

④ 使用注解修饰实体类的属性

/**
 * 显示状态[0-不显示;1-显示]
 */
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
// 通过自定义的vals属性限定只能是0或1
@ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class}) 
private Integer showStatus;

到这里自定义注解就已经可以实现了