前言
在日常的开发中,服务端对象的校验是非常重要的一个环节,比如用户注册的时候:校验用户名,密码,身份证,邮箱等信息是否为空,以及格式是否正确,但是这种在日常的开发中进行校验太繁琐了,代码繁琐而且很多。Validator框架应运而生,它的出现就是为了解决开发人员在开发的时候减少代码的,提升开发效率。它专门用来做接口的参数校验,比如:密码长度、是否为空等等。
概述
JSR303
定义了 Bean Validation
(校验)的标准 validation-api
,但并没有提供实现。Hibernate Validation
是对这个规范的实现 ,并且增加了 @Email
、@Length
、@Range
等注解。Spring Validation
底层依赖的就是Hibernate Validation
。
JSR303:JSR303是一项标准,只提供规范不提供实现。定义了校验规范即校验注解如:@Null、@NotNull、@Pattern。位于:javax.validation.constraints
包下。
hibernate validation:是对 JSR303 规范的实现并且进行了增强和扩展。并增加了注解:@Email、@Length、@Range等。
**spring Validation: ** 是对Hibernate Validation的二次封装。在SpringMvc模块中添加了自动校验。并将校验信息封装到特定的类中。
常用约束说明
JSR 提供的校验注解:
@Null
被注释的元素必须为 null
@NotNull
被注释的元素必须不为 null
@AssertTrue
被注释的元素必须为 true
@AssertFalse
被注释的元素必须为 false
@Min(value)
被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)
被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value)
被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)
被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max=, min=)
被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction)
被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past
被注释的元素必须是一个过去的日期
@Future
被注释的元素必须是一个将来的日期
@Pattern(regex=,flag=)
被注释的元素必须符合指定的正则表达式
Hibernate Validator提供的校验注解:
@NotBlank(message =)
验证字符串非 null,且长度必须大于 0
@Email
被注释的元素必须是电子邮箱地址
@Length(min=,max=)
被注释的字符串的大小必须在指定的范围内
@NotEmpty
被注释的字符串的必须非空
@Range(min=,max=,message=)
被注释的元素必须在合适的范围内
实战开发
@Validated注解用法
引入依赖
SpringBoot提供了validator启动器
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
|
非SpringBoot项目,需要自行引入依赖
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.1.5.Final</version> </dependency> <dependency> <groupId>org.glassfish</groupId> <artifactId>jakarta.el</artifactId> <version>3.0.3</version> </dependency>
|
实体约束示例
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
| package com.lzy.entity;
import lombok.Data; import javax.validation.Valid; import javax.validation.constraints.*;
@Data public class Student { @NotBlank(message = "用户名不能为空") private String name; @Min(value = 18, message = "年龄不能小于18岁") private Integer age; @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误") private String phone; @Email(message = "邮箱格式错误") private String email; @Valid @NotNull private School school;
@Data private static class School{ @NotBlank(message = "学校名不能为空") private String name; @NotBlank(message = "学校地址不能为空") private String address; } }
|
校验演示
准备Contoller进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.lzy.controller; import com.lzy.config.Update; import com.lzy.entity.Student; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; import javax.validation.constraints.NotBlank;
@RestController @Slf4j public class TestController { }
|
当Controller层方法中参数是对象时(如Student,且对象中字段已经加了约束),若不加@Validated注解,则约束不起效果
1 2 3 4 5 6 7 8 9
| @RestController @Slf4j public class TestController { @GetMapping("/t1") public String test3(Student student) { log.info("学生信息:{}", student); return "ok"; } }
|
1
| 2021-07-18 15:20:38.740 INFO 3212 --- [nio-8080-exec-8] com.lzy.controller.TestController : 学生信息:Student(id=, name=, age=null, phone=, email=, school=Student.School(name=, address=))
|
对象参数前加上@Validated注解后约束生效
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @RestController @Slf4j public class TestController { @GetMapping("/t2") public String test1(@Validated Student student) { log.info("学生信息:{}", student); return "ok"; } @GetMapping("/t3") public String test2(@Validated @RequestBody Student student) { log.info("学生信息:{}", student); return "ok"; } }
|
1 2 3 4 5 6 7
| 2021-07-18 15:30:53.440 WARN 3212 --- [io-8080-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 6 errors Field error in object 'student' on field 'name': rejected value []; codes [NotBlank.student.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.name,name]; arguments []; default message [name]]; default message [用户名不能为空] Field error in object 'student' on field 'school.name': rejected value []; codes [NotBlank.student.school.name,NotBlank.school.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.school.name,school.name]; arguments []; default message [school.name]]; default message [学校名不能为空] Field error in object 'student' on field 'school.address': rejected value []; codes [NotBlank.student.school.address,NotBlank.school.address,NotBlank.address,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.school.address,school.address]; arguments []; default message [school.address]]; default message [学校地址不能为空] Field error in object 'student' on field 'age': rejected value [11]; codes [Min.student.age,Min.age,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.age,age]; arguments []; default message [age],18]; default message [年龄不能小于18岁] Field error in object 'student' on field 'email': rejected value [1234qq.com]; codes [Email.student.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@2e90ef80,.*]; default message [邮箱格式错误] Field error in object 'student' on field 'phone': rejected value []; codes [Pattern.student.phone,Pattern.phone,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.phone,phone]; arguments []; default message [phone],[Ljavax.validation.constraints.Pattern$Flag;@5e6ea6fb,^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\d{8}$]; default message [手机号格式错误]]
|
Json格式传递参数同样
1 2 3 4 5 6 7 8 9
| @RestController @Slf4j public class TestController { @GetMapping("/t3") public String test2(@Validated @RequestBody Student student) { log.info("学生信息:{}", student); return "ok"; } }
|
1
| 2021-07-18 15:32:56.979 WARN 3212 --- [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.lzy.controller.TestController.test2(com.lzy.entity.Student) with 3 errors: [Field error in object 'student' on field 'name': rejected value []; codes [NotBlank.student.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.name,name]; arguments []; default message [name]]; default message [用户名不能为空]] [Field error in object 'student' on field 'phone': rejected value [18812345]; codes [Pattern.student.phone,Pattern.phone,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.phone,phone]; arguments []; default message [phone],[Ljavax.validation.constraints.Pattern$Flag;@5e6ea6fb,^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\d{8}$]; default message [手机号格式错误]] [Field error in object 'student' on field 'school.name': rejected value []; codes [NotBlank.student.school.name,NotBlank.school.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.school.name,school.name]; arguments []; default message [school.name]]; default message [学校名不能为空]] ]
|
如果想对Controller层方法中参数直接进行约束,此时必须在类上添加@Validated,否则约束不起效果
1 2 3 4 5 6 7 8 9 10 11 12
| @RestController @Slf4j public class TestController {
@GetMapping("/t4") public String test4(@NotBlank(message = "用户名不能为空") String name, @Min(value = 18, message = "年龄不能小于18岁") Integer age) { log.info("学生{}的年龄为{}", name,age); return "ok"; }
}
|
1
| 2021-07-18 15:49:53.751 INFO 21548 --- [nio-8080-exec-7] com.lzy.controller.TestController : 学生null的年龄为16
|
在类上加上@Validated后约束生效
1 2 3 4 5 6 7 8 9 10 11
| @RestController @Slf4j @Validated public class TestController { @GetMapping("/t4") public String test4(@NotBlank(message = "用户名不能为空") String name, @Min(value = 18, message = "年龄不能小于18岁") Integer age) { log.info("学生{}的年龄为{}", name,age); return "ok"; } }
|
1 2 3 4 5
| 2021-07-18 15:55:20.738 ERROR 16840 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: test4.name: 用户名不能为空, test4.age: 年龄不能小于18岁] with root cause
javax.validation.ConstraintViolationException: test4.name: 用户名不能为空, test4.age: 年龄不能小于18岁 at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.8.jar:5.3.8] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.8.jar:5.3.8]....
|
@Validated与@Valid
@Valid注解与@Validated注解功能大部分类似;
不同点:
@Valid属于javax包下,而@Validated属于Spring下
@Valid支持嵌套校验、而@Validated不支持
@Validated支持分组,而@Valid不支持
自定义约束注解
创建自定义注解@Phone
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.lzy.annotation;
import com.lzy.handler.PhoneValidator; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*;
@Documented @Constraint(validatedBy = PhoneValidator.class) @Target({ElementType.METHOD, ElementType.FIELD,ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface Phone { String message() default "手机格式不正确!"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
|
定义具体的验证器
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
| package com.lzy.handler; import com.lzy.annotation.Phone; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.regex.Matcher; import java.util.regex.Pattern;
public class PhoneValidator implements ConstraintValidator<Phone, String> { @Override public boolean isValid(String phoneNum, ConstraintValidatorContext constraintValidatorContext) { if (phoneNum == null && phoneNum.length() == 0) { return true; } Pattern p = Pattern.compile("^(13[0-9]|14[5|7|9]|15[0|1|2|3|5|6|7|8|9]|17[0|1|6|7|8]|18[0-9])\\d{8}$"); Matcher matcher = p.matcher(phoneNum); return matcher.matches(); } @Override public void initialize(Phone constraintAnnotation) { } }
|
使用及测试
1 2 3 4 5 6 7 8 9 10
| @RestController @Slf4j @Validated public class TestController { @GetMapping("/phone") public String phone(@Phone(message = "手机号别瞎jb填") String phoneNum) { log.info("学生手机号码:{}", phoneNum); return "ok"; } }
|
1 2 3 4
| 2021-07-18 17:48:47.890 ERROR 18844 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: phone.phoneNum: 手机号别瞎jb填] with root cause
javax.validation.ConstraintViolationException: phone.phoneNum: 手机号别瞎jb填 at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.8.jar:5.3.8]....
|
多级嵌套模型校验
前面已经出现过,Student对象中的school属性也是一个对象,如果要想School对象里的name,adress上的约束生效,则必须要在school上添加 @Valid 注解,
且上文已说过,@Valid支持嵌套校验、而@Validated不支持, 当然此处应该加上一个 @NotNull
,避免school对象为null
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
| package com.lzy.entity;
import lombok.Data; import javax.validation.Valid; import javax.validation.constraints.*;
@Data public class Student { @NotBlank(message = "用户名不能为空") private String name; @Min(value = 18, message = "年龄不能小于18岁") private Integer age; @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误") private String phone; @Email(message = "邮箱格式错误") private String email; @Valid @NotNull private School school;
@Data private static class School{ @NotBlank(message = "学校名不能为空") private String name; @NotBlank(message = "学校地址不能为空") private String address; } }
|
分组校验
场景说明:比如我们在业务开发中对用户的新增和修改操作,新增用户时肯定没有用户id,修改用户时肯定要传入用户id,此时便可以使用分组校验
自定义两个分组Create和Update
1 2 3 4 5 6 7 8 9 10
| package com.lzy.config;
import javax.validation.groups.Default;
public interface Create extends Default { }
|
1 2 3 4 5 6 7 8 9 10
| package com.lzy.config;
import javax.validation.groups.Default;
public interface Update extends Default { }
|
在之前的Student对象模型中id属性上指定group为Update
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
| @Data public class Student {
@NotBlank(message = "id不能为空",groups ={Update.class} ) private String id;
@NotBlank(message = "用户名不能为空") private String name;
@Min(value = 18, message = "年龄不能小于18岁") private Integer age;
@Phone private String phone;
@Email(message = "邮箱格式错误") private String email;
@Valid @NotNull private School school;
@Data private static class School{ @NotBlank(message = "学校名不能为空") private String name; @NotBlank(message = "学校地址不能为空") private String address; } }
|
在Controller启动校验时,指定校验分组
1 2 3 4 5 6 7 8 9 10 11
| @PostMapping("/createStudent") public String createStudent(@Validated @RequestBody Student student) { log.info("学生信息:{}", student); return "createStudent success!"; }
@PostMapping("/updateStudent") public String updateStudent(@Validated(value = Update.class) @RequestBody Student student) { log.info("学生信息:{}", student); return "updateStudent success!"; }
|
1
| 2021-07-18 18:15:00.239 INFO 23140 --- [nio-8080-exec-3] com.lzy.controller.TestController : 学生信息:Student(id=, name=xxxxxx@qq.com, age=18, phone=18158872278, email=xxxxxx@qq.com, school=Student.School(name=中科大, address=安徽省合肥市))
|
1
| 2021-07-18 18:15:48.322 WARN 23140 --- [nio-8080-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.lzy.controller.TestController.updateStudent(com.lzy.entity.Student): [Field error in object 'student' on field 'id': rejected value []; codes [NotBlank.student.id,NotBlank.id,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.id,id]; arguments []; default message [id]]; default message [id不能为空]] ]
|
关于自定义分组继承Default说明
继承Default并不是必须的。
如果继承了Default,那么@Validated(value = Create.class)的校验范畴为【Create】和【Default】;
如果没继承Default,那么@Validated(value = Create.class)的校验范畴为【Create】;
Student对象中的name、age、phone等属性默认分组为Default,如果自定义分组没有继承Default,那在Contoller的方法中指定校验分组时,必须加上Default分组,否则name、age、phone等属性约束会失效
处理校验抛出的异常
当注解校验不通过时,直接将异常信息返回给前端并不友好,我们可以将异常包装处理后返回给前端。
方式一:使用BindingResult类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @PostMapping("/t6") public String test6(@Validated @RequestBody Student student,BindingResult bindingResult) { if (bindingResult.hasErrors()){ List<ObjectError> allErrors = bindingResult.getAllErrors(); StringBuilder builder = new StringBuilder(); allErrors.forEach(e->{ log.error(e.getDefaultMessage()); builder.append("【"+e.getDefaultMessage()+"】"); }); return builder.toString(); } log.info("学生信息:{}", student); return "ok"; }
|
注意:
@Validated(或@Valid) 和 BindingResult 是成对出现的,如果有多个@Validated,那么每个@Validated后面都需要添加BindingResult用于接收对象中的校验信息
方式二(推荐):全局异常处理
详情可见系列文章《SpringBoot全局异常处理》
Reference
https://blog.csdn.net/justry_deng/article/details/86571671
https://blog.csdn.net/lihua5419/article/details/83418043