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 定义实体类
@Datapublic 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@RestControllerpublic 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=ab1c2022-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=476938977javax.validation.ConstraintViolationException: test3.email: 不是一个合法的电子邮件地址
2.4 全局自定义异常
这里需要说明下,如果我们使用的 @Validated 注解,但没有添加 BindingResult 这个参数,Spring 会抛出 BindException 级别的异常。所以可以写一个全局异常处理类来统一处理这种校验异常,从而免去重复组织异常信息的代码。
这里总结了三种参数校验时可能发生的异常:
- 使用 form data 方式调用接口,校验异常抛出 BindException。
- 使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException。
- 单个参数校验异常抛出 ConstraintViolationException。
@Slf4j@RestControllerAdvicepublic 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 指定分组。
@Datapublic 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@Validatedpublic 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 {@NotNullprivate Long id;@NotBlank(message = "itemName 不能为空")private String itemName;}
在 RequestForm 类中增加一个 OrderVO 类型的属性。
@Datapublic class RequestForm {...@Validprivate 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)@Documentedpublic @interface List {NotBlank[] value();}}
2、实现 ConstraintValidator 接口编写约束校验器
public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlank, String> {@Overridepublic boolean isValid(String value, ConstraintValidatorContext context) {// null 不做检验if (value == null) {return true;}if (value.contains(" ")) {// 校验失败return false;}// 校验成功return true;}}
