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}")
@Validate
public String add(@PathVariable @NotNull(message = "不能为空") @Min(value = 0, message = "不能小于0") Integer id)
{
//
}
当对单个参数进行校验时,直接在参数前添加对应的约束注解即可,value
为约束值,message
为错误信息。之后介绍参数校验的异常处理。
当使用单个参数校验时,需要将@Validate
注解放置在类上。
实体参数校验
参数传输对象
public class Person
{
@NotNull
@NotBlank
private 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)
@Documented
public @interface Validated {
Class<?>[] value() default {};
}
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Valid {
}
嵌套校验
如果一个参数模型类中包含包含另一个需要校验的模型类:
@Data
public class Person
{
@NotNull
@NotBlank
private String name;
@NotNull
@Min(value=0, message="年龄不能小于0岁")
private Integer age;
@Valid
@NotNull // 当pets不为空后,将对Pet类进行属性的校验
private List<Pet> pets;
// private List<@Valid Pet> pets;
}
@Data
public class Pet
{
@NotBlak(message="Pet名称不能为空")
private String name;
}
校验方式与以前一样
@PostMapping("/add")
public String add(@RequestBody @Validated Person person)
{
//
}
分组校验
只有在校验参数时使用@Validate
时才能进行分组校验。
参数实体类如下:
@Data
public class Person
{
@NotNull(groups = Update.class, message="手机号错误")
private Long id;
@NotNull
@NotBlank
private 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})
//@ReportAsSingleViolation
public @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 = "手机号不能为空";
@Override
public void initialize(Phone constraintAnnotation)
{
}
@Override
public 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
@RestControllerAdvice
public 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;
/// BindException
if (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;
}
}