JSR303数据校验

JSR303数据校验

JSR 303 是Java为Bean数据合法校验提供的标准框架,已经包含在Java EE 6.0中。JSR是一个规范,它的核心接口时Validator,该接口根据目标对象类中所标注的校验注解进行数据校验,并得到校验结果。JSR303通过在Bean属性中标注类似@NotNull、@Max等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。

JSR 303 包含注解

注解名称 描述
@Null 被注解的元素必须为null
@NotNull 被注解的元素必须不为null
@AssertTrue 被注解的元素必须为true
@AssertFalse 被注解的元素必须为false
@Min(value) 被注解的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注解的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注解的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注解的元素必须是一个数字,其值必须小于等于指定的最大值
@Size 被注解的元素的大小必须在指定的范围内
@Digits(integer,fraction) 被注解的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注解的元素必须是一个过去的时间
@Future 被注解的元素必须是一个将来的时间
@Pattern(regex=,flag=) 被注解的元素必须符合指定的正则表达式

Hibernate Vakudator

Hibernate Vakudator是JSR303的一个参考实现,出来支持所有的标准注解外,还支持一些扩展注解

注解名称 描述
@NotBlank(message=) 验证字符串非null,并且长度必须大于0
@Email 被注解的元素必须是电子邮箱地址
@Length(min=,max=) 被注解的字符串的大小必须在指定的范围内
@NotEmpty 被注解的字符串必须非空
@Range(min=,max=,message=) 被注解的元素必须在合适的范围内
@URL 被注解的元素必须是合法的URL

ValidationMessages.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
javax.validation.constraints.AssertFalse.message     = 必须为 false
javax.validation.constraints.AssertTrue.message = 必须为 true
javax.validation.constraints.DecimalMax.message = 必须小于 ${inclusive == true ? 'or equal to ' : ''}{value}
javax.validation.constraints.DecimalMin.message = 必须大于 ${inclusive == true ? 'or equal to ' : ''}{value}
javax.validation.constraints.Digits.message = 数字值超出了边界(期望 <{integer} digits>.<{fraction} digits>)
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 = 必须小于 0
javax.validation.constraints.NegativeOrZero.message = 必须小于或等于 0
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 = 必须大于 0
javax.validation.constraints.PositiveOrZero.message = 必须大于或等于 0
javax.validation.constraints.Size.message = 大小必须在 {min} 和 {max} 之间

org.hibernate.validator.constraints.CreditCardNumber.message = 无效信用卡卡号
org.hibernate.validator.constraints.Currency.message = 无效货币(必须为 {value} 之一)
org.hibernate.validator.constraints.EAN.message = 无效 {type} 条形码
org.hibernate.validator.constraints.Email.message = 电子邮件地址格式不规范
org.hibernate.validator.constraints.ISBN.message = 无效 ISBN
org.hibernate.validator.constraints.Length.message = 长度必须介于 {min} 与 {max} 之间
org.hibernate.validator.constraints.CodePointLength.message = 长度必须介于 {min} 与 {max} 之间
org.hibernate.validator.constraints.LuhnCheck.message = ${validatedValue} 的校验位无效,Luhn Modulo 10 校验和失败
org.hibernate.validator.constraints.Mod10Check.message = ${validatedValue} 的校验位无效,Modulo 10 校验和失败
org.hibernate.validator.constraints.Mod11Check.message = ${validatedValue} 的校验位无效,Modulo 11 校验和失败
org.hibernate.validator.constraints.ModCheck.message = ${validatedValue} 的校验位无效,{modType} 校验和失败
org.hibernate.validator.constraints.NotBlank.message = 可能不为空
org.hibernate.validator.constraints.NotEmpty.message = 可能不为空
org.hibernate.validator.constraints.ParametersScriptAssert.message = 脚本表达式 "{script}" 未求值为 true
org.hibernate.validator.constraints.Range.message = 必须介于 {min} 与 {max} 之间
org.hibernate.validator.constraints.SafeHtml.message = 可能具有不安全的 HTML 内容
org.hibernate.validator.constraints.ScriptAssert.message = 脚本表达式 "{script}" 未求值为 true
org.hibernate.validator.constraints.UniqueElements.message = 必须仅包含唯一元素
org.hibernate.validator.constraints.URL.message = 必须为有效 URL

org.hibernate.validator.constraints.br.CNPJ.message = 无效巴西企业纳税人登记号 (CNPJ)
org.hibernate.validator.constraints.br.CPF.message = 无效巴西个人纳税人登记号 (CPF)
org.hibernate.validator.constraints.br.TituloEleitoral.message = 无效巴西投票人身份证号

org.hibernate.validator.constraints.pl.REGON.message = 无效波兰纳税人识别号 (REGON)
org.hibernate.validator.constraints.pl.NIP.message = 无效 VAT 识别号 (NIP)
org.hibernate.validator.constraints.pl.PESEL.message = 无效波兰身份证号 (PESEL)

org.hibernate.validator.constraints.time.DurationMax.message = 必须短于 ${inclusive == true ? ' or equal to' : ''}${days == 0 ? '' : days == 1 ? ' 1 day' : ' ' += days += ' days'}${hours == 0 ? '' : hours == 1 ? ' 1 hour' : ' ' += hours += ' hours'}${minutes == 0 ? '' : minutes == 1 ? ' 1 minute' : ' ' += minutes += ' minutes'}${seconds == 0 ? '' : seconds == 1 ? ' 1 second' : ' ' += seconds += ' seconds'}${millis == 0 ? '' : millis == 1 ? ' 1 milli' : ' ' += millis += ' millis'}${nanos == 0 ? '' : nanos == 1 ? ' 1 nano' : ' ' += nanos += ' nanos'}
org.hibernate.validator.constraints.time.DurationMin.message = 必须长于 ${inclusive == true ? ' or equal to' : ''}${days == 0 ? '' : days == 1 ? ' 1 day' : ' ' += days += ' days'}${hours == 0 ? '' : hours == 1 ? ' 1 hour' : ' ' += hours += ' hours'}${minutes == 0 ? '' : minutes == 1 ? ' 1 minute' : ' ' += minutes += ' minutes'}${seconds == 0 ? '' : seconds == 1 ? ' 1 second' : ' ' += seconds += ' seconds'}${millis == 0 ? '' : millis == 1 ? ' 1 milli' : ' ' += millis += ' millis'}${nanos == 0 ? '' : nanos == 1 ? ' 1 nano' : ' ' += nanos += ' nanos'}

使用

1.导包

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.6.7</version>
</dependency>

2.添加注解

在需要校验的字段上添加相应的注解进行校验,其中message是当校验不合法是提示的错误信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 品牌
*
* @author liziyuan
* @email liziyuan@gmail.com
* @date 2022-07-08 08:58:02
*/
@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;

}

3.Cotroller开启校验

使用@Valid注解开启校验,其中参数BindingResult可以获取校验的结果。

  • BindingResult:BindingResult扩展了Errors接口,同时可以获取数据绑定结果对象的信息。@Valid和BindingResult参数是成对出现的,并且在形参中出现的顺序是固定的,一前一后。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 @RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
//BindingResult 参数能够获取校验结果
// 获取校验的错误结果
if (result.hasErrors()) {
Map<String,String> map=new HashMap<>();
result.getFieldErrors().forEach((item)->{
// 获取到错误提示
String message=item.getDefaultMessage();
String field = item.getField();//获取错误的属性的名字
map.put(field,message);
});
return R.error(400,"提交数据不合法").put("data",map);
}else{
brandService.save(brand);
}

return R.ok();
}

自定义校验

  1. 编写一个自定义校验注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * @Author: liZiYuan
    * <p>
    * Date: 2022/7/16 20:01
    */
    @Documented
    @Constraint(
    validatedBy = {ListValueConstraintValidator.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 "{com.zhang.common.valid.ListValue.message}";

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

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

    int[] vals() default {};
    }

  2. 编写一个自定义的校验器 ConstrainValidator

    实现ConstraintValidator<Annotation, T>接口,其中Annotation表示自定义的校验注解,T表示校验对象的类型;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer>{

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

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

    //判断是否校验成功

    /**
    *
    * @param value 需要校验的值
    * @param constraintValidatorContext
    * @return
    */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
    return set.contains(value);
    }
    }
  3. 关联自定义的校验器和自定义的校验注解

重点

@Validated是@Valid 的一次封装,是Spring提供的校验机制使用。@Valid不提供分组功能

@Valid和@Validated的区别

Spring Validation验证框架对参数的验证机制提供了@Validated(Spring的JSR-303规范,是标准JSR-303的一个变种),Javax提供了@Valid(标准JSR-303规范),配合BindingResult可以直接提供参数验证结果。

@Valid属于Javax.validation包下,是jdk给提供的,是使用Hibernate validation的时候使用(java的JSR303声明了@Valid这类接口,而Hibernate-validator对其进行了实现)

@Validated是org.springframework.validation.annotation包下的,是spring提供的,是只用Spring validator校验机制使用

在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:

1.分组

1
2
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制
@Valid:作为标准的JSR-303规范,还没有吸收分组的功能

@Validated会有分组的概念,后面默认是有一个Default.class,当你的@Validated后面没有加任何校验分组信息的时候默认会加Default分组,而对于被校验的对象的属性字段,如果你在属性的校验标签里面没有指定分组会添加到默认分组Default里面。所以如果你的对象里面的每个属性都指定了分组信息,而接口上面并没有添加分组信息会出现@Validated“失效的情况”,实际是因为它校验的是Default分组,而没有字段属于Default分组。上述校验不管加不加@ReqeustBody都能完成校验,但是都不能完成嵌套的校验。

特殊用法:

  • 分组: 当一个实体类需要多种验证方式时,例:对于一个实体类的id来说,新增的时候是不需要的,对于更新时是必须的, 可以通过groups对验证进行分组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public interface AllFiled {
}

public interface First {
}

public interface Secend {
}

public class UserInfo {
private Integer uid;

@NotBlank(message = "姓名不能为空", groups = {First.class, AllFiled.class})
private String username;

@NotEmpty(message = "密码不能为空", groups = {AllFiled.class})
@Length(min = 6, max = 10, message = "密码长度只能在6-10之间", groups = {AllFiled.class})
private String password;

//临时加的字段
@Min(value = 18, message = "年龄最小为18", groups = {AllFiled.class})
@Max(value = 60, message = "年龄最大为60", groups = {AllFiled.class})
int age;

//临时加的字段
@AssertTrue(message = "必须同意条款", groups = {Secend.class, AllFiled.class})
boolean agree;
}

@ResponseBody
@GetMapping("validate")
public Map<String, String> validate(@Validated({AllFiled.class}) UserInfo userInfo, BindingResult result){

Map<String, String> map = new HashMap<String, String>();
if (result.hasErrors()) {
List<ObjectError> list = result.getAllErrors();
for (ObjectError error :
list) {
FieldError fieldError = (FieldError)error;
String defaultMessage = fieldError.getDefaultMessage();
String field = fieldError.getField();
map.put(field, defaultMessage);
}
}
return map;
}
}

当@Validated分组为空时, 只会验证没有分组的属性, 如上面的uid;

当@Validated{First.class}时, 只会验证分组为First.class的字段, 对于其他分组字段和未分组字段都为空

注意

添加全局异常处理时Controller层参数中不能加BindingResult参数,否则无法捕获异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
@RestControllerAdvice(basePackages = "com.zhang.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {

// @ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handlerValidException(MethodArgumentNotValidException e){
System.out.println("校验异常处理");
log.error("数据校验出现问题{},异常类型{}",e.getMessage(),e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String,String> errorMap=new HashMap<>();
bindingResult.getFieldErrors().forEach((filedError)->{
errorMap.put(filedError.getField(),filedError.getDefaultMessage());
});
return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(),BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data",errorMap);
}

@ExceptionHandler(value = RuntimeException.class)
public R hadleException(){
System.out.println("其他异常");
return R.error(BizCodeEnum.UNKNOW_EXCEPTION.getCode(),BizCodeEnum.UNKNOW_EXCEPTION.getMsg());
}
}

JSR303数据校验
http://example.com/2022/09/04/JSR303数据校验/
作者
liziyuan
发布于
2022年9月4日
许可协议