SpringBoot使用Valid验证
Bean Validation是Java定义的一套基于注解的数据校验规范。Hibernate Validation是Bean Validation的一个实现。
Bean Validation官网:https://beanvalidation.org/
Hibernate Validation官网:http://hibernate.org/validator/
在SpringBoot项目中使用BeanValidate校验参数需要的依赖
<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId><version>2.0.1.Final</version></dependency><dependency><groupId>org.hibernate</groupId><artifactId>hibernate-validator</artifactId><version>6.0.1.Final</version></dependency>
内置注解
validator内置注解:@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(value)被注释的元素必须符合指定的正则表达式
Hibernate Validator 附加的注解:@Email 被注释的元素必须是电子邮箱地址@Length 被注释的字符串的大小必须在指定的范围内@NotEmpty 被注释的字符串的必须非空@Range 被注释的元素必须在合适的范围内@NotBlank 验证字符串非null,且长度必须大于0
参数校验简单使用
参数校验可以分为两种场景:
- 单个参数校验
- 实体类参数校验
单个参数校验
@PostMapping("/get/{id}")@Validatepublic String add(@PathVariable @NotNull(message = "不能为空") @Min(value = 0, message = "不能小于0") Integer id){//}
当对单个参数进行校验时,直接在参数前添加对应的约束注解即可,value为约束值,message为错误信息。之后介绍参数校验的异常处理。
当使用单个参数校验时,需要将@Validate注解放置在类上。
实体参数校验
参数传输对象
public class Person{@NotNull@NotBlankprivate String name;@NotNull@Min(value=0, message="年龄不能小于0岁")private Integer age;}
直接将参数放置在对象的属性上即可,多个注解可以叠加使用
需要进行参数校验时,只需在参数前面加上@Validated注解就可以完成校验
@PostMapping("/add")public String add(@RequestBody @Validated Person person){//}
@Validated与@Valid的简单对比说明
@Valid注解与@Validated注解功能大部分类似;两者的不同主要在于:
- @Valid属于javax下的,而@Validated属于spring下;
- @Valid支持嵌套校验、而@Validated不支持
- @Validated支持分组,而@Valid不支持。
从它们的源码也可以看出@Validate不能使用在属性域上,所有无法进行嵌套校验。
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Validated {Class<?>[] value() default {};}
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Valid {}
嵌套校验
如果一个参数模型类中包含包含另一个需要校验的模型类:
@Datapublic class Person{@NotNull@NotBlankprivate String name;@NotNull@Min(value=0, message="年龄不能小于0岁")private Integer age;@Valid@NotNull // 当pets不为空后,将对Pet类进行属性的校验private List<Pet> pets;// private List<@Valid Pet> pets;}@Datapublic class Pet{@NotBlak(message="Pet名称不能为空")private String name;}
校验方式与以前一样
@PostMapping("/add")public String add(@RequestBody @Validated Person person){//}
分组校验
只有在校验参数时使用@Validate时才能进行分组校验。
参数实体类如下:
@Datapublic class Person{@NotNull(groups = Update.class, message="手机号错误")private Long id;@NotNull@NotBlankprivate String name;@NotNull@Min(value=0, message="年龄不能小于0岁")private Integer age;}
在id属性上添加@NotNull注解,并且指定groups=Update.class,Update类是一个自定义的接口,用来充当标识作用。注意只能使用接口作为分组的标识。
public interface Update{}
分组校验使用:
@PostMapping("/add")public String add(@RequestBody @Validated Person person){System.out.println(person);return "ADD OK";}@PutMapping("/update")public String update(@RequestBody @Validated(Update.class) Person person){System.out.println(person);return "UPDATE OK";}
在更新方法中,给@Validate注解也添加了Update.class的值,从前面的源码可看到,@Validate注解只有一个属性值,而这个值就是用来进行分组校验的,并且它是一个数组,也就是说可以放入多个分组标识。
上面的update方法将对Person类中的id属性进行校验,而add方法则不会对id属性进行校验。
实际上,每一个参数校验默认都有一个分组为
Default,Default组和无参构造机制类似,当没有指定分组时,会默认当前校验属于Default组,但是一旦主动给当前校验指定了分组那么就不会再额外指定属于Default组了。我们也可以让自定义的标识接口继承Default接口,使得它同时也属于Default组。
在上面的校验中,因为add方法没有明确使用分组,所有用了Default分组,而id属性的校验属于Update分组,所以不会对id进行为空校验。而当Update接口继承Default后:
public interface Update extends Default{}
此时id属性的校验既是Update分组也是Default分组,add方法的Person参数也会对id属性进行校验。
自定义参数检验注解
虽然Bean Validation和Hibernate Validator已经提供了非常丰富的校验注解,但是在实际业务中,难免会碰到一些现有注解不足以校验的情况;这时,我们可以考虑自定义Validation注解。
1.创建自定义注解
@Target({METHOD, FIELD, TYPE, CONSTRUCTOR, PARAMETER})@Documented@Retention(RUNTIME)@Constraint(validatedBy = {PhoneValidator.class})//@ReportAsSingleViolationpublic @interface Phone{String message() default "手机号格式不正确"; // 默认错误提示信息Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})@Retention(RUNTIME)@Documented@interface List{Phone[] value();}}
@Constraint(validatedBy = {PhoneValidator.class})标注了处理该自定义校验注解的类。
public class PhoneValidator implements ConstraintValidator<Phone, String>{private static final String TEMPLATE = "手机号不能为空";@Overridepublic void initialize(Phone constraintAnnotation){}@Overridepublic boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext){if (StringUtils.isBlank(s)){// 禁用默认提示信息constraintValidatorContext.disableDefaultConstraintViolation();// 添加新的错误提示信息constraintValidatorContext.buildConstraintViolationWithTemplate(TEMPLATE).addConstraintViolation();return false;}String regex = "^1[3|4|5|7|8][0-9]\\d{4,8}$";Pattern pattern = Pattern.compile(regex);return pattern.matcher(s).matches();}}
参数校验的异常处理
方式一:使用BindingResult类来容纳异常信息
当校验不通过时,不影响正常程 序往下走。我们只需要处理BindingResult中的异常信息即可。
@PostMapping("/add")public String add(@RequestBody @Validated Person person, BindingResult result){if (result.hasErrors()){List<ObjectError> allErrors = result.getAllErrors();for (ObjectError error : allErrors){System.out.println(error.getDefaultMessage());}return "FAILURE";}System.out.println(person);return "ADD OK";}
方式二:通过SpringMVC全局异常处理器来处理异常。
如果不采用BindingResult来容纳异常信息时,那么异常会被向外抛出。注解校验不通过时,可能抛出的异常有BindException异常、ValidationException异常(或其子类异常)、MethodArgumentNotValidException异常。
- 单个参数校验出错会抛出
ConstraintViolationException异常。 - 实体类模型参数出错会排除抛出
MethodArgumentNotValidException异常
ConstraintViolationException异常是ViolationException异常的子异常
加入全局异常处理
@Slf4j@RestControllerAdvicepublic class ExceptionController{@ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class})public String handleParameterVerificationException(Exception e){log.error(" handleParameterVerificationException has been invoked", e);String msg = null;/// BindExceptionif (e instanceof BindException){// getFieldError获取的是第一个不合法的参数(P.S.如果有多个参数不合法的话)FieldError fieldError = ((BindException) e).getFieldError();if (fieldError != null){msg = fieldError.getDefaultMessage();}/// MethodArgumentNotValidException} else if (e instanceof MethodArgumentNotValidException){BindingResult bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();// getFieldError获取的是第一个不合法的参数(P.S.如果有多个参数不合法的话)FieldError fieldError = bindingResult.getFieldError();if (fieldError != null){msg = fieldError.getDefaultMessage();}/// ValidationException 的子类异常ConstraintViolationException} else if (e instanceof ConstraintViolationException){/** ConstraintViolationException的e.getMessage()形如* {方法名}.{参数名}: {message}* 这里只需要取后面的message即可*/msg = e.getMessage();if (msg != null){int lastIndex = msg.lastIndexOf(':');if (lastIndex >= 0){msg = msg.substring(lastIndex + 1).trim();}}/// ValidationException 的其它子类异常} else{msg = "处理参数时异常";}return msg;}}
