后端验证 - JSR-303
编写信息
笔者:LiiiYiAn
编写日期:2023.05.20
参考文献
[9] 稀土掘金 - 一文学会JSR-303 参数校验,真香 参考日期:2023.05.20
[8] CSDN - 使用Hibernate-Validator实现参数校验 参考日期:2023.05.20
[7] 百度百科 - JCP 参考日期:2023.05.20
[6] 简书 - Hibernate Validator简介 参考日期:2023.05.20
[5] CSDN - 3. 什么是JSR参考实现? - JavaEE基础系列 参考日期:2023.05.20
[4] 博客园 - JSR-303简单使用 参考日期:2023.05.20
[3] 书籍 - Spring 实战第 5 版-【美】克雷格·沃斯(Craig Walls) 著 张卫滨 译 参考日期:2023.05.20
[2] 脚本之家 - SpringBoot后端进行数据校验JSR303的使用详解 参考日期:2023.05.20
[1] CSDN - JSR303的基本使用 参考日期:2023.05.20
序论
为什么需要后端验证?
前后端进行数据交互时,在前端把数据传送到后端前,一般会先验证一次,验证成功之后,才把数据发送到后端。
但是我们在后端还得在对数据进行一次校验。因为请求数据发送的链接很容易获取,可以不经过前端界面,使用 Postman 等工具直接向后台发送数据,这就可能会造成传送数据不合法的情况
早期的参数校验形式
在早期的时候,Java 的参数校验停留在获取参数之后在代码层面做校验,类似如下操作:
@PostMapping("/test")
public String test(@RequestBody TestRequest request) {
if (StringUtils.isEmpty(request.getName())) {
return "姓名不能为空";
}
if (request.getName().length() > 6) {
return "姓名长度不能超过6";
}
if (StringUtils.isEmpty(request.getPhone())) {
return "电话不能为空";
}
if (!isPhone(request.getPhone())) {
return "电话号码格式不正确";
}
return "SUCCESS";
}
/**
* 校验电话格式
*/
private boolean isPhone(String phone) {
return true;
}
如上,每一种校验规则都需要在代码中实现,那么光是参数校验就会耗费开发人员大量的精力,进而导致开发效率大幅降低
JSR-303 相关概念
JSR-303 中的 JSR 是何
进入正文,首先了解下 JSR-303 中的 JSR 是什么意思
JSR 是 Java Specification Requests 的缩写,意思是 Java 规范提案。是指向 JCP 提出新增一个标准化技术规范的正式请求。
JCP:Java Community Process,一个开放的国际组织,主要由 Java 开发者以及被授权者组成,职能是发展和更新
任何人都可以向 JCP 提交 JSR,以向 Java 平台增添新的 API 和服务。JSR 已成为 Java 界的一个重要标准。
JSR 就是一个提交到 JCP 的抽象请求,包含对 Java 技术平台的补充。因为是抽象的,所以不能直接被调用
JCP 官网中关于 JSR:https://jcp.org/en/jsr/overview
主角 JSR-303 登场
然后,再来介绍我们的主角 JSR-303
为什么叫 JSR-303 呢?
JSR 是 Java 规范提案。JSR-303 也就是第 303 号提案
JSR-303 是 Java EE 6 中的一项子规范,叫做 Bean Validation,即 Bean 验证,顾名思义,它提供了对 Java EE 和 Java SE 中的 Java Bean 中字段的值进行验证的方式。
Validation /ˌvæləˈdeɪʃən/ n.验证,确认
Spring MVC 中也支持 JSR-303,我们可以在控制器中对表单提交的数据方便地验证。
该规范主要使用注解的方式来实现对 Java Bean 的验证功能 。
JSR-303 与 JS 区别:
- JSR-303 属于后端验证
- JS 属于前端的脚本验证(只作用于客户端的浏览器中,可以被屏蔽)
通过使用 JSR-303,我们能够更容易地声明验证规则,而不必在应用代码中显式编写声明逻辑
JCP 官网中关于 JSR-303:https://jcp.org/en/jsr/detail?id=303
JSR-303 的一个参考实现 是 Hibernate Validator,Hibernate Validator 提供了 JSR-303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint
关于这个参考实现,想必很多人都是一脸懵,下文会详解
Hibernate Validator 与 Hibernate ORM 框架没有任何关系,Hibernate 项目不仅有 ORM 一个框架
拓展 - 关于参考实现
参考实现:Reference Implementation
JSR 是一个提交到 JCP 的抽象请求,包含对 Java 技术平台的补充。
因为是抽象的,所以不能直接被调用。JSR 需要有某种形式的实现,或者说开发人员能直接使用的某种具现。这就是参考实现的概念。
JCP 规定 每个 JSR 都必须有参考实现,和 JSR 文档打包在一起。 其具体实现公众可以自由使用,一般和应用服务器打包在一起发布。
参考实现不止一个,这也是上文为什么说 Hibernate Validator 是 JSR-303 的一个参考实现
每个 JSR 也都包含 TCK,这是一套测试工具, 用于检测 JSR 实现是否符合规范的要求。
TCK:Technology Compatibility Kit,技术兼容性工具包
TCK 用来测试 JSR 实现是否符合规范。这也从侧面说明,为什么 Java EE 是一套标准化规范
这种严格的审核过程确保了 JSR 文档中 API 的质量。下面是一些流行的消息格式的 JSR:
- JSR 380 (Bean Validation 2.0) – Hibernate Validator 6
- JSR 367 (JSON-B Binding) – Eclipse Yasson 1.0
- JSR 370 (JAX-RS 2.1) – Jersey
- JSR 365 (CDI 2.0) – WELD 3.0
这些都是新的 JSR 和各自的参考实现。大多数参考实现都和应用服务器捆绑在一起
在项目中使用 JSR-303 的前提
在非 Spring Boot 项目中使用 JSR-303 前提:
在非 Spring Boot 项目中使用 JSR-303,使用的是其参考实现 Hibernate Validator
-
普通项目,引入相关 jar 包,版本自定:
validation-api-1.0.0.GA.jar // JDK的接口 hibernate-validator-4.2.0.Final.jar // 对上述接口的实现
-
Maven 项目,在
pom.xml
中引入对应依赖,版本自定:<!-- JSR-303 --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.7.Final</version> </dependency>
在 Spring Boot 项目中使用 JSR-303,引入依赖:
<!-- JSR-303 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
需要注意的一点是,关于在 Spring Boot 项目中使用 JSR-303,《Spring 实战第 5 版》这样说:
JSR-303 和 JSR-303 的 Hibernate 实现会作为 Spring Boot web starter 的传递性依赖会自动添加到项目中
Spring Boot web starter:Spring Boot web 启动程序
传递性依赖:临时依赖项
这里需要纠正的是:老版本的确是默认引入,但在新版本的 Spring Boot 中,需要单独引入
应该是 Spring 2.3.x 开始取消了默认引入 JSR-303。可以先试试在 IDEA 中的实体类属性上打一个注解 @Size,如果弹出引入包提示就说明默认引入了,否则就需要单独引用
JSR-303 内置验证注解
注解签名 | 注解功能 |
---|---|
@Null | 被注解的元素必须为 null |
@NotNull | 被注解的元素必须不为 null |
@NotBlank | 字符串必须不为 null,且去掉前后空格长度必须大于 0 |
@AssertTrue | 被注解的元素必须为 true |
@AssertFalse | 被注解的元素必须为 false |
@Min(value) | value 为 int,被注解的元素必须是一个数字,其值必须大于等于 value |
@Max(value) | value 为 int,被注解的元素必须是一个数字,其值必须小于等于 value |
@DecimalMin(value) | value 为 int,被注解的元素必须是一个数字,其值必须大于等于 value |
@DecimalMax(value) | value 为 int,被注解的元素必须是一个数字,其值必须小于等于 value |
@Size(max, min) | 对象 (Array、Collection、Map、String) 长度必须在给定范围 |
@Digits (integer, fraction) | 被注解的元素必须是一个数字,integer 指定整数精度,fraction 指定小数精度 。例如 @Digits(integer=3, fraction=0) 表示值包含 3 位数字 |
@Past | 被注解的元素必须是一个过去的日期 |
@Future | 被注解的元素必须是一个将来的日期 |
@Pattern(regex, flag) | 被注解的元素必须符合指定的正则表达式 |
使用 @NotNull 时注意 import,因为有两种 @NotNull:
注意要选择 JSR-303 的,也就是 javax.validation.constraints.NotNull
注解一般是在
javax.validation.constraints
这个包下,也还有一些是hibernate
提供的
同时,根据 JSR-303 规范,每个注解中都默认有 3 个属性:
-
message 属性,用于定义验证错误时的提示信息,如:
@NotBlank(message = "名称不能为空") private String name;
当 name 属性验证错误,即为空时,就会提示该注解对应 message 属性设置的提示信息
-
groups 属性,用于分组验证
groups 属性在后文
@Vaild 和 @Vaildated
中会详解 -
payload 属性,定义负载信息
Hibernate Validator 附加验证注解
Hibernate Validator 是 JSR-303 的一个参考实现,除了支持所有标准的校验注解外,另外 Hibernate Validator 还有 JSR-380 的实现
以下注解都是 Hibernate Validator 自定义的,假如使用其他的 JSR-303 参考实现,可能没有这些注解
注解名称 | 注解功能 |
---|---|
被注解的元素必须是电子邮箱地址 | |
@Length(min, max) | 被注解的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注解的字符串的必须非空 |
@Range(min, max) | 被注解的元素必须在合适的范围内 |
@URL(protocol, host, port) | 检查是否是一个有效的 URL,如果提供了 protocol,host 等,则该 URL 还需满足提供的条件 |
@CreditCardNumber | 声明该属性值必须是通过 Luhn 算法检查的有效信用卡号,这可以防止用户出错的数据和故意错误的数据,但不能保证信用卡号码实际上被分配到一个帐户,或该帐户可以用于交易 |
Luhn 算法检查:https://en.wikipedia.org/wiki/Luhn_algorithm
使用 JSR-303 进行参数验证的基本流程
还记得 序论
中的早期的参数校验形式代码吗?
@PostMapping("/test")
public String test(@RequestBody TestRequest request) {
if (StringUtils.isEmpty(request.getName())) {
return "姓名不能为空";
}
if (request.getName().length() > 6) {
return "姓名长度不能超过6";
}
if (StringUtils.isEmpty(request.getPhone())) {
return "电话不能为空";
}
if (!isPhone(request.getPhone())) {
return "电话号码格式不正确";
}
return "SUCCESS";
}
/**
* 校验电话格式
*/
private boolean isPhone(String phone) {
return true;
}
在这个验证业务上,使用 JSR-303 将其改良
使用 JSR-303 很简单,我们只需要在想要验证的属性上加注解即可
首先在实体类中对属性添加对应验证规则的注解:
@Data
public class TestRequest {
@NotEmpty(message = "姓名不能为空")
@Length(min = 1, max = 6, message = "姓名长度必须在1-6之间")
private String name;
@NotBlank(message = "电话号码不能为空")
@Pattern(regexp = "^134[0-8]\\d{7}$|^13[^4]\\d{8}$|^14[5-9]\\d{8}$|^15[^4]\\d{8}$|^16[6]\\d{8}$|^17[0-8]\\d{8}$|^18[\\d]{9}$|^19[8,9]\\d{8}$", message = "电话号码格式不正确")
private String phone;
@Email(message = "邮件格式不正确")
private String email;
@NotNull(message = "年龄不能为空")
@Max(value = 18, message = "年龄不能超过18")
private Integer age;
}
但这不够,我们还需要在控制器类中,通过 @Valid 开启校验功能
在控制器类中,用 @Valid 对前端传送进来的数据进行验证:
@PostMapping("/test")
public String test(@RequestBody @Valid TestRequest request) {
return "SUCCESS";
}
@Valid 注解来自包 javax.validation.Valid
,它会对被注解的属性进行验证,验证时机是在绑定完前端传送数据之后,调用控制器方法之前
@Validated 与 @Valid 类似,在基本使用中没有区别,更多下文会讲解
前端传送数据:一般是通过表单传送
在这个示例中,被注解的属性是 TestRequest 对象属性 request,控制器方法是
test()
效果:
总结,使用 JSR-303 进行验证一般需要两步:
- 给 Bean 添加校验注解
- 在 Controller 中通过 @Valid 开启校验功能
@Valid 和 @Validated
基本理解
首先看一下他们所属的包:
- @Validated :
org.springframework.validation.annotation.Validated
- @Valid:
javax.validation.Valid
可以看到 @Validated 属于 spring,而 @Valid 属于 javax。
但是在实际的基本使用中,这二者是没有区别的,都会对被注解的属性进行验证,验证时机是在绑定完前端传送数据之后,调用控制器方法之前
注意是基本使用,也就是说,还是有区别的
比如说分组验证只能用 @Validated,嵌套验证只能用 @Vaild,这些下文都会详讲
比如,以上文的示例为例,将 @Vaild 替换为 @Vaildated:
@PostMapping("/test")
public String test(@RequestBody @Validated TestRequest request) {
return "SUCCESS";
}
将注解替换后,也能得到相同的结果:
两个注解源码
- @Valid
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface Valid {
}
- @Validated
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
Class<?>[] value() default {};
}
通过源码可以看到 @Validated 可以在类上面使用,并且多了一个 value 属性。
这个 value 属性是干嘛的呢?
@Validated 提供了一个分组功能,在校验参数时,可以根据不同的分组采用不同的校验机制。value 属性就是用于指定这个分组
如果 @Validated 没有添加分组属性 value,则默认验证没有分组功能
@Validated 分组验证
在不同业务场景下,校验规则是不一样的,比如 user 对象中 id 这个属性,在新增时,这个属性是不用填的,要为 null,但是在修改时,id 属性是不能为 null 的
可以用注解中的 groups 属性来指明:在指定场景下使用该注解
验证分组标识类,下面四个接口代表增删改查操作:
public class ValidatedGroup {
public interface Add extends Default {}
public interface Delete extends Default {}
public interface Update extends Default {}
public interface Query extends Default {}
}
测试请求类:
@Data
public class TestSaveRequest {
@NotNull(groups = {ValidatedGroup.Update.class, ValidatedGroup.Delete.class}, message = "更新或者删除时id不能为空")
private Long id;
@NotBlank(groups = {ValidatedGroup.Update.class}, message = "更新时姓名不能为空")
private String name;
@NotNull(groups = {ValidatedGroup.Add.class}, message = "新增时余额不能为空")
@Digits(integer = 10, fraction = 2)
private BigDecimal balance;
@NotBlank(groups = {ValidatedGroup.Add.class}, message = "新增时电话不能为空")
@Pattern(regexp = "^134[0-8]\\d{7}$|^13[^4]\\d{8}$|^14[5-9]\\d{8}$|^15[^4]\\d{8}$|^16[6]\\d{8}$|^17[0-8]\\d{8}$|^18[\\d]{9}$|^19[8,9]\\d{8}$", message = "电话号码格式不正确")
private String phone;
@NotBlank(groups = {ValidatedGroup.Add.class}, message = "新增时邮件不能为空")
@Email
private String email;
}
根据编写的测试请求类中属性上面注解的属性 groups,可预期:
- 验证分组为 Update 时,会验证 id、name 属性
- 验证分组为 Add 时,会验证 balance、phone、email 三个属性
测试如下:
@PostMapping("/testAdd")
public String testAdd(@RequestBody @Validated(value = ValidatedGroup.Add.class) TestSaveRequest request) {
return "SUCCESS";
}
@PostMapping("/testUpdate")
public String testUpdate(@RequestBody @Validated(value = ValidatedGroup.Update.class) TestSaveRequest request) {
return "SUCCESS";
}
“/testAdd” 请求结果:
“/testUpdate” 请求结果:
结果与预期完全符合。@Validated 在分组校验时,可以节省很多额外的开发,特别是当新增和更新时
一个需要传递主键一个不需要传递主键的情形,以前需要一个 AddRequest、一个 UpdateRequest,现在只需要一个注解标明就够了。
@Valid 嵌套验证
先来看看什么是嵌套验证
上代码:
实体类:
@Data
public class TestNestRequest {
@NotNull(message = "id不能为空")
private Long id;
@NotNull(message = "嵌套对象请求数据不能为空")
private ItemRequest itemRequest;
}
实体类中嵌套的实体类:
@Data
public class ItemRequest {
@NotEmpty(message = "name不能为空")
private String name;
}
控制器类:
@PostMapping("/testNest")
public String testNest(@RequestBody @Valid TestNestRequest request) {
return "SUCCESS";
}
测试结果:
发现有什么错误吗?
只验证了 id,没有验证嵌套对象 itemRequest 中的属性
要进行嵌套验证,需要明白一点:嵌套验证必须用 @Valid
首先,改造一下 TestNestRequest
类,在 itemRequest 属性上加上 @Valid,才能验证嵌套对象中的属性:
@Data
public class TestNestRequest {
@NotNull(message = "id不能为空")
private Long id;
@Valid
@NotNull(message = "嵌套对象请求数据不能为空")
private ItemRequest itemRequest;
}
测试结果:
可以看到嵌套对象验证成功
验证失败提示信息
默认提示信息
当验证失败的时候,会抛出两种异常:
使用 @Valid 校验时抛出:
org.springframework.validation.BindException
使用 @Validated 校验时抛出:
org.springframework.web.bind.MethodArgumentNotValidException
假设定义了这样一个 Bean:
@Data
public class User {
@Length(min = 4,max = 8)
private String password;
}
控制器类省略
在 Bean 中,我们并没有定义验证失败提示信息
当验证失败后,会出现如下错误。并且会给出默认的提示信息:
这个错误信息是怎么来的呢
顺便打开一个注解的源码,比如 @NotNULL :
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotBlank {
String message() default "{javax.validation.constraints.NotNull.message}";
//...
}
看到有一个 message 属性。显然就是指定错误的提示内容。
而这些错误提示是在一个叫 validationMessages.properties
的文件中,用 IDEA 的搜索工具可以找到,双击 shift
打开搜索
发现有这么多 validationMessages.properties
的文件,而且支持国际化:
打开validationMessages_zh.properties
,可以看到里面定义了很多提示。而错误提示就是从这文件中获取的:
自定义提示信息
如果我们不想用默认的校验提示信息的话,可以自己指定,通过注解属性 message 即可:
@NotBlank(message = "用户名不能为空")
private String username;
提示信息的获取与响应
当验证失败时,会默认返回一个错误界面,或者返回错误提示的 JSON 数据
但默认提供的显然不是我们想要的,如果可以拿到错误信息,那我们就能自定义相应数据了。
拿到错误信息的方式也很简单,只要在方法中加上 BindingResult result
这个参数,错误信息就会被封装在这个参数中:
@PostMapping("/user2")
@ResponseBody
public R user2(@Valid User user, BindingResult result) throws Exception {
System.out.println(user);
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);
}
// 若没有错误,则进行接下去的业务操作。
return null;
}
不过不推荐上面这种方式,当校验的地方多了,每个方法里面都加上这么个异常处理,会让代码很臃肿。
臃肿就代表着不优雅,对于一个程序员来说,这是难以忍受的
Spring MVC 中有个全局的异常处理,我们可以自定义一个异常处理,在这里面统一处理异常。
统一处理 BinException
。这样就可以不用在 Controller 中去处理错误信息了:
package cn.jxj4869.demo.execption;
import cn.jxj4869.demo.entity.R;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice(basePackages = "cn.jxj4869.demo.controller")
public class MyExceptionControllerAdvice {
@ExceptionHandler(value = BindException.class)
public R handleVaildException(BindException e) {
Map<String,String> map = new HashMap<>();
// 获取校验的错误结果
e.getFieldErrors().forEach((item)->{
// 获取到错误提示
String message = item.getDefaultMessage();
// 获取错误的属性的名字
String field = item.getField();
map.put(field,message);
});
return R.error(400, "提交的数据不合法").put("data", map);
}
}
自定义验证注解
当提供的注解不能满足我们需求的时候,可以自定义注解。
例如我们现在给 user 新加一个属性 status,并要求这个属性的值只能是 0 或者 1。而在 JSR-303 中并没有这样的注解,因此,我们需要自定义注解
根据 JSR-303 规范,验证注解得有三个属性:
- message:定义验证错误信息
- groups:指定校验分组
- payload:定义负载信息
新建一个 @StatusValue 注解,使用 @Constraint 指定该注解的验证器:
package cn.jxj4869.demo.valid;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
@Constraint(validatedBy = { StatusValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@interface StatusValue {
String message() default "{cn.jxj4869.valid.StatusValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] value() default { };
}
@Constraint(validatedBy = { StatusValueConstraintValidator.class }) 指定该注解的验证器
接下来,我们需要自定义刚刚指定注解的验证器:
需要实现 ConstraintValidator
这个接口,第一个泛型是表示要校验哪个注解,第二个泛型是要校验的数据的类型
initialize()
是初始化方法isValid()
是校验方法,判断是否校验成功
StatusValueConstraintValidator.java
:
package cn.jxj4869.demo.valid;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
public class StatusValueConstraintValidator implements ConstraintValidator<StatusValue,Integer> {
private Set<Integer> set = new HashSet<>();
//初始化方法
@Override
public void initialize(StatusValue constraintAnnotation) {
int[] value = constraintAnnotation.value();
for (int val : value) {
set.add(val);
}
}
/**
* 判断是否校验成功
* @param value
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
最后在resources
目录下添加一个 ValidationMessages.properties
文件,用来指定错误信息
文件内容如下:
cn.jxj4869.valid.StatusValue.message=必须提交指定的值
定义一个 UserBean 作为测试:
@Data
public class User {
@Null(groups = {AddGroup.class})
@NotNull(groups = {UpdateGroup.class})
private Integer id;
@NotBlank(message="用户名不能为空", groups={AddGroup.class,UpdateGroup.class})
private String username;
@NotEmpty
private String password;
private String email;
@StatusValue(value = {0,1},groups = {AddGroup.class,UpdateGroup.class})
private Integer status;
传送不合法数据:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mDOcrBa4-1684596842171)(D:\Typora本地图像存放\image-20230520210657205.png)]
传送合法数据:
param value
- @param context
- @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
最后在`resources`目录下添加一个 `ValidationMessages.properties` 文件,用来指定错误信息
文件内容如下:
cn.jxj4869.valid.StatusValue.message=必须提交指定的值
定义一个 UserBean 作为测试:
```java
@Data
public class User {
@Null(groups = {AddGroup.class})
@NotNull(groups = {UpdateGroup.class})
private Integer id;
@NotBlank(message="用户名不能为空", groups={AddGroup.class,UpdateGroup.class})
private String username;
@NotEmpty
private String password;
private String email;
@StatusValue(value = {0,1},groups = {AddGroup.class,UpdateGroup.class})
private Integer status;
传送不合法数据:
传送合法数据: