1. 简介
1.1 了解数据校验
在 SpringBoot 日常开发中,后端会接收到前端传递过来的大量数据,这些数据有时我们还需要进行校验,比如姓名不能为空、年龄范围等等,如果在校验时写上大量的 if…else..,无疑是不美观的,所以 Spring 框架已经给我们封装了一套校验组件:validation。
Java API 规范 (JSR303) 定义了 Bean 校验的标准 validation-api,但没有提供实现。hibernate validation 是对这个规范的实现,并增加了校验注解如@Email、@Length 等。Spring Validation是 对 hibernate validation 的二次封装,用于支持 spring mvc 参数自动校验。
从springboot-2.3
开始,校验包被独立成了一个starter
组件,所以需要引入 validation 和 web,而springboot-2.3
之前的版本只需要引入 web 依赖就可以了。
1.2 引入依赖
<!--校验组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 可以不用,用上面的校验组件即可 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
1.3 校验注解
@AssertFalse 可以为 null, 如果不为 null 的话必须为 false
@AssertTrue 可以为 null, 如果不为 null 的话必须为 true
@DecimalMax 设置不能超过最大值
@DecimalMin 设置不能超过最小值
@Digits 设置必须是数字且数字整数的位数和小数的位数必须在指定范围内
@Future 日期必须在当前日期的未来
@Past 日期必须在当前日期的过去
@Max 最大不得超过此最大值
@Min 最大不得小于此最小值
@NotNull 不能为 null,可以是空
@Null 必须为 null
@Pattern 必须满足指定的正则表达式
@Size 集合、数组、map 等的 size() 值必须在指定范围内
@Email 必须是 email 格式
@Length 长度必须在指定范围内
@NotBlank 字符串不能为 null, 字符串 trim() 后也不能等于 “”
@NotEmpty 不能为 null,集合、数组、map 等 size() 不能为 0;字符串 trim() 后可以等于 “”
@Range 值必须在指定范围内
@URL 必须是一个 URL
2. 实践入门
2.1 定义实体类
@Data
public class ValidVO {
@Length(min = 6,max = 12,message = "appId 长度必须位于 6 到 12 之间")
private String appId;
@NotBlank(message = "名字为必填项")
private String name;
@Email(message = "请填写正确的邮箱地址")
private String email;
private String sex;
@NotEmpty(message = "级别不能为空")
private String level;
}
2.2 定义Controller
这里我们先定义三个方法 test1,test2,test3,test1 使用了@RequestBody
注解,用于接受前端发送的 json 数据,test2 模拟表单提交,test3 模拟单参数提交。注意,当使用单参数校验时需要在 Controller 上加上 @Validated 注解,否则不生效。
@Validated
注解:用在类、方法和参数上,表示对后面的参数进行校验。
@Slf4j
@Validated
@RestController
public class ValidVoController {
@PostMapping("/valid/test1")
public String test1(@Validated @RequestBody ValidVO validVO) {
log.info("validEntity is {}", validVO);
return "test1 valid success";
}
@PostMapping(value = "/valid/test2")
public String test2(@Validated ValidVO validVO){
log.info("validEntity is {}", validVO);
return "test2 valid success";
}
@PostMapping(value = "/valid/test3")
public String test3(@Email String email){
log.info("email is {}", email);
return "email valid success";
}
}
2.3 发送请求测试
2.3.1 调用test1
通过 apifox 测试工具,调用 test1 方法,抛出 org.springframework.web.bind.MethodArgumentNotValidException
异常。
{
"appId": "ab1c",
"name": "yxw",
"email": "47693899",
"sex": "man",
"level": "12",
}
IDEA 执行结果如下:
2022-02-16 10:54:41.288 WARN 33576 --- [nio-9090-exec-8] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.xuwei.springbootvalidation.controller.ValidVoController.test1(com.xuwei.springbootvalidation.entity.ValidVO) with 2 errors: [Field error in object 'validVO' on field 'email': rejected value [47693899]; codes [Email.validVO.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@79091710,.*]; default message [请填写正确的邮箱地址]] [Field error in object 'validVO' on field 'appId': rejected value [ab1c]; codes [Length.validVO.appId,Length.appId,Length.java.lang.String,Length]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.appId,appId]; arguments []; default message [appId],12,6]; default message [appId 长度必须位于 6 到 12 之间]] ]
2.3.2 调用test2
调用 test2 方法,提示的是org.springframework.validation.BindException
异常。
localhost:9090/valid/test2?name=yxw&level=12&email=476938977&appId=ab1c
2022-02-16 10:56:58.056 WARN 33576 --- [nio-9090-exec-9] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errors<EOL>Field error in object 'validVO' on field 'email': rejected value [476938977]; codes [Email.validVO.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@79091710,.*]; default message [请填写正确的邮箱地址]<EOL>Field error in object 'validVO' on field 'appId': rejected value [ab1c]; codes [Length.validVO.appId,Length.appId,Length.java.lang.String,Length]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.appId,appId]; arguments []; default message [appId],12,6]; default message [appId 长度必须位于 6 到 12 之间]]
2.3.3 调用test3
调用 test3 方法,提示的是 javax.validation.ConstraintViolationException
异常。
localhost:9090/valid/test3?email=476938977
javax.validation.ConstraintViolationException: test3.email: 不是一个合法的电子邮件地址
2.4 全局自定义异常
这里需要说明下,如果我们使用的 @Validated 注解,但没有添加 BindingResult 这个参数,Spring 会抛出 BindException 级别的异常。所以可以写一个全局异常处理类来统一处理这种校验异常,从而免去重复组织异常信息的代码。
这里总结了三种参数校验时可能发生的异常:
- 使用 form data 方式调用接口,校验异常抛出 BindException。
- 使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException。
- 单个参数校验异常抛出 ConstraintViolationException。
@Slf4j
@RestControllerAdvice
public class MyRestExceptionHandler {
// 1.处理 form data方式调用接口校验失败抛出的异常
@ExceptionHandler(value = BindException.class)
public ResultData<List<String>> bindExceptionHandler(BindException e) {
log.error("form data方式调用接口校验失信息 ex={}", e.getMessage(), e);
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
List<String> collect = fieldErrors.stream().map(o -> o.getDefaultMessage()).collect(Collectors.toList());
return ResultData.formError(ReturnCode.RC500.getCode(), e.getMessage(), collect);
}
// 2.处理 json 请求体调用接口校验失败抛出的异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultData methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
List<String> collect = fieldErrors.stream().map(o -> o.getDefaultMessage()).collect(Collectors.toList());
return ResultData.formError(ReturnCode.RC500.getCode(), e.getMessage(), collect);
}
// 3.处理单个参数校验失败抛出的异常
@ExceptionHandler(ConstraintViolationException.class)
public ResultData constraintViolationExceptionHandler(ConstraintViolationException e) {
Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
List<String> collect = constraintViolations.stream()
.map(o -> o.getMessage())
.collect(Collectors.toList());
return ResultData.formError(ReturnCode.RC500.getCode(), e.getMessage(), collect);
}
}
3. 分组校验
有时我们会遇到一种情况:比如新增用户时用户名称不能为空的,更新用户时用户允许为空,所以这里的校验规则也是不同的,可以使用分组校验。
分组校验有三个步骤:
1、定义两个个分组类 Update 和 Save,或者直接定义一个分组接口。
public interface ValidGroup extends Default {
interface Crud extends ValidGroup {
interface Create extends Crud {
}
interface Update extends Crud{
}
interface Query extends Crud{
}
interface Delete extends Crud{
}
}
}
2、在注解上添加 groups 指定分组。
@Data
public class ValidVO {
@Null(groups = ValidGroup.Crud.Create.class)
@NotNull(groups = ValidGroup.Crud.Update.class, message = "应用 ID 不能为空")
private String appId;
@NotBlank(groups = ValidGroup.Crud.Create.class, message = "名字为必填项")
private String name;
@Email(message = "请填写正确的邮箱地址")
private String email;
private String sex;
@NotEmpty(message = "级别不能为空")
private String level;
}
3、Controller 方法的 @Validated 注解添加分组类。
@RestController
@Slf4j
@Validated
public class ValidVoController {
@PostMapping(value = "/valid/add")
public String add(@Validated(value = ValidGroup.Crud.Create.class) ValidVO validVO) {
log.info("validEntity is {}", validVO);
return "add valid success";
}
@PostMapping(value = "/valid/update")
public String update(@Validated(value = ValidGroup.Crud.Update.class) ValidVO validVO){
log.info("validEntity is {}", validVO);
return "update valid success";
}
}
注意:如果你的 ValidGroup 没有继承 Default 分组,那代码属性上就需要加上
@Validated(value = {ValidGroup.Crud.Create.class, Default.class}
才能让
4. 递归校验
如果 RequestForm 类增加一个 OrderVO 类的属性,而 OrderVO 中的属性也需要校验,就用到递归校验了,只要在相应属性上增加 @Valid 注解即可实现。
OrderVO类如下
public class OrderVO {
@NotNull
private Long id;
@NotBlank(message = "itemName 不能为空")
private String itemName;
}
在 RequestForm 类中增加一个 OrderVO 类型的属性。
@Data
public class RequestForm {
...
@Valid
private OrderVO orderVO;
}
5. 自定义校验
Spring Validation 允许用户自定义校验,实现很简单,分两步:
1、自定义校验注解
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {HaveNoBlankValidator.class})// 标明由哪个类执行校验逻辑
public @interface HaveNoBlank {
// 校验出错时默认返回的消息
String message() default "字符串中不能含有空格";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
/**
* 同一个元素上指定多个该注解时使用
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface List {
NotBlank[] value();
}
}
2、实现 ConstraintValidator 接口编写约束校验器
public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlank, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// null 不做检验
if (value == null) {
return true;
}
if (value.contains(" ")) {
// 校验失败
return false;
}
// 校验成功
return true;
}
}