在写业务代码时,对参数的校验必不可少,基于Hibernate的Validator,可以非常便捷的实现参数校验。本文以SpringBoot为例,介绍一下如何使用Validator
基本操作
1、maven依赖
首先需要引入validator的starter依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
在该starter中,实际上最终会依赖Hibernate,
2、实体添加校验参数
对需要校验的实体对象,添加校验规则
public class Student {
@NotNull(message = "student id should not be null", groups = UpdateStudentBasicInfo.class)
private Long id;
@NotEmpty(message = "student name should not be empty")
private String name;
@NotNull(message = "student address should not be null")
private String address;
@NotEmpty(message = "student sex should not be empty")
@Pattern(regexp = "male|female", message = "student sex should match male or female")
private String sex;
@NotEmpty(message = "student telephone should not be empty", groups = StudentAdvanceInfo.class)
private String telephone;
}
上面使用了一些常用的规则。
3、请求参数使用注解,开启校验
实现一个controller,对实体进行操作,同时开启校验。备注: 这里仅仅是为了验证校验,不做实际业务处理。
@PostMapping("/add")
public ApiCommonResponse<String> addStudent(@Valid Student student) {
return ApiCommonResponse.success("OK");
}
4、对校验结果进行处理
方案1:直接在请求方法中,添加BindResult进行处理
对请求方法进行修改,通过BindResult获取到校验结果,基于结果进行响应处理。
@PostMapping("/add")
public ApiCommonResponse<String> addStudent(@Valid Student student, BindingResult bindResult) {
if (bindResult.hasErrors()) {
log.error(bindResult.toString());
return ApiCommonResponse.fail(HttpStatus.BAD_REQUEST.value(), bindResult.toString());
}
return ApiCommonResponse.success("OK");
}
方案2:统一使用ExceptionHandler拦截器,对校验结果封装(推荐)
此处主要是基于@RestControllerAdvice和@ExceptionHandler注解完成的,不过这里有一个需要注意的地方。
那就是Validator对于json的请求和表单的请求,当违反校验时,抛出的异常不一致。
@RestControllerAdvice
public class ValidationHandlers {
/**
* 表单请求校验结果处理
* @param bindException
* @return
*/
@ExceptionHandler(value = BindException.class)
public ApiCommonResponse<String> errorHandler(BindException bindException) {
BindingResult bindingResult = bindException.getBindingResult();
return extractException(bindingResult.getAllErrors());
}
/**
* JSON请求校验结果,也就是请求中对实体标记了@RequestBody
* @param methodArgumentNotValidException
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ApiCommonResponse<String> errorHandler(MethodArgumentNotValidException methodArgumentNotValidException) {
BindingResult bindingResult = methodArgumentNotValidException.getBindingResult();
return extractException(bindingResult.getAllErrors());
}
private ApiCommonResponse<String> extractException(List<ObjectError> errorList) {
StringBuilder errorMsg = new StringBuilder();
for (ObjectError objectError : errorList) {
errorMsg.append(objectError.getDefaultMessage()).append(";");
}
// 移出最后的分隔符
errorMsg.delete(errorMsg.length() - 1, errorMsg.length());
return ApiCommonResponse.fail(HttpStatus.BAD_REQUEST.value(), errorMsg.toString());
}
}
5、测试结果
场景1:基于表单+ExceptionHandler的add
场景2:请求参数中,直接使用BindResult
@PostMapping("/addWithBindResult")
@ResponseBody
public ApiCommonResponse<String> addStudent(@Valid Student student, BindingResult bindResult) {
if (bindResult.hasErrors()) {
log.error(bindResult.toString());
return ApiCommonResponse.fail(HttpStatus.BAD_REQUEST.value(), bindResult.toString());
}
return ApiCommonResponse.success("OK");
}
高级用法
1、对请求中的pathVariable和requestParam进行校验
第一步: 对方法参数添加校验
@GetMapping("/getStudentById/{id}")
public ApiCommonResponse<String> getStudentById(@PathVariable("id") @Min(value = 10, message = "input id must great than 10") Long id) {
return ApiCommonResponse.success("OK");
}
@GetMapping("/getStudentById")
public ApiCommonResponse<String> getStudentByIdRequestParam(@RequestParam @Min(value = 10, message = "input id must great than 10") Long id) {
return ApiCommonResponse.success("OK");
}
第二步:在类级别开启校验
@Controller
@Slf4j
@Validated
public class ValidatorDemoController
测试情况:
可以看到此时,捕获的异常是ConstraintViolationException,所以可以通过新增ExceptionHandler,返回统一的错误响应。
/**
* pathVariable 和RequestParam的校验
* @param constraintViolationException
* @return
*/
@ExceptionHandler(value = ConstraintViolationException.class)
public ApiCommonResponse<String> errorHandler(ConstraintViolationException constraintViolationException) {
Set<ConstraintViolation<?>> constraintViolations = constraintViolationException.getConstraintViolations();
String errorMsg = constraintViolations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));
return ApiCommonResponse.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
}
2、group分组
主要应用场景,是针对同一个实体,在不同的场景下,会有不同的校验规则。比如新增的时候,唯一标识id可以为空。但是在修改的时候,该值必须不为空。
第一步:定义不同场景下的标记接口。
// 新增场景
public interface AddStudentBasicInfo {
}
// 修改场景
public interface UpdateStudentBasicInfo {
}
第二步:对实体的校验规则,指名触发的group条件
@NotNull(message = "student id should not be null", groups = UpdateStudentBasicInfo.class)
private Long id;
第三步:在需要校验的地方,指定触发的条件
@PostMapping("/update")
@ResponseBody
public ApiCommonResponse<String> updateStudent(@Validated(UpdateStudentBasicInfo.class) Student student) {
return ApiCommonResponse.success("OK");
}
这里需要注意一下,如果指定了groups,那么校验就会只针对该groups中的规则进行。所以,如果对于没有指定groups的规则,默认属于Default.class,此时如果需要包含,可以使用下面的方式。
@PostMapping("/update")
@ResponseBody
public ApiCommonResponse<String> updateStudent(@Validated({UpdateStudentBasicInfo.class, Default.class}) Student student) {
return ApiCommonResponse.success("OK");
}
package javax.validation.groups;
public interface Default {
}
3、group sequence
当有多个group时,校验规则的顺序是不固定的,可以通过以下两种方式指定校验的顺序。这里,有点类似组合校验。
比如这里,会有学生的基本信息,也会有学生的高级信息。校验的时候,希望先对基本信息校验,通过后,再对高级信息校验。
第一步:还是需要定义高级信息和基本信息标记接口:
// 高级信息
public interface StudentAdvanceInfo {
}
// 基本信息
public interface StudentBasicInfo {
}
第二步:按照需要加到实体的group上。
@NotEmpty(message = "student name should not be empty", groups = StudentBasicInfo.class)
private String name;
@NotEmpty(message = "student telephone should not be empty", groups = StudentAdvanceInfo.class)
private String telephone;
下面会有2种方式,指定校验顺序。
方案1:在被校验实体上指定
@GroupSequence({StudentBasicInfo.class, StudentAdvanceInfo.class, Student.class})
public class Student
注意,一定要将自身包含到GroupSequence中。否则会报错误:xxx must be part of the redefined default group sequence
之后对Student的校验,会默认按照StudentBasicInfo-> StudentAdvanceInfo->Default的顺序执行。
方案2:定义一个新的标记接口,指名sequence顺序。相比较而言,如果不希望全局影响Student的校验行为,推荐用该方式。
@GroupSequence({StudentBasicInfo.class, StudentAdvanceInfo.class})
public interface ValidateStudentGroupSequence {
}
@GetMapping("/testGroupSequence")
@ResponseBody
public ApiCommonResponse<String> testGroupSequence(@Validated(ValidateStudentGroupSequence.class) Student student) {
return ApiCommonResponse.success("OK");
}
可以看到当name属性有了之后,会自动走到StudentAdvanceInfo对应的telephone的校验。
4、 自定义校验
Validator自身提供了非常多常用的校验,如果不满足需要,可以自行实现自定义的校验。这里举得例子,实际上用默认的也可以。
第一步:定义校验注解。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = CustomerValidator.class)
public @interface CustomerValidatorAnnotation {
/**
* 违反校验时的错误信息
*/
String message() default "{CustomerValidatorAnnotation.message}";
/**
* 用于指定校验的条件
*/
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
第二步:实现校验器,也就是第一步中@Constraint中的类
@Slf4j
public class CustomerValidator implements ConstraintValidator<CustomerValidatorAnnotation, String> {
private static final String CUSTOMER_TEST = "china";
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
return s != null && s.startsWith(CUSTOMER_TEST);
}
}
第三步:按照需要将其放置到实体上即可。
@CustomerValidatorAnnotation(message = "student address must start with china")
private String address;
下图中的start with china,即是自定义规则。
[图片上传失败...(image-afa4d6-1618763454151)]
5、Validator的一些定制行为
自定义注入Validator
@Bean
public Validator validator() {
ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(false)
.buildValidatorFactory();
return factory.getValidator();
}
这个里面,配置了failFast属性,failFast为false时,会对所有的信息进行校验。如果设置failFast为true,则当碰到不满足的条件后,会立即终止,返回当前违反的错误。
备注:这里需要注意一下,如果实体类是继承的,即使failFast设置为true,子类中有违反的约束,父类也会触发校验。也就是failFast为true,可以理解是每个类都校验,也就是每个类最多会存在一个违反约束的校验结果。
在非Controller中使用Validator
除了在controller中使用validator验证,实际上在service中同样可以使用
@Service
@Validated
public class ValidatorService {
public void testValidateService(@Valid Student student) {
}
}
注意此时如果违反约束,会抛出ConstraintViolationException。
另外一种方式是基于注入的Validator实现,这种方式需要自己处理校验结果,不会主动抛出异常。
@Service
public class ValidatorBeanService {
@Resource
private Validator validator;
public ApiCommonResponse<String> validate(Student student) {
Set<ConstraintViolation<Student>> constraintViolations = validator.validate(student);
if (CollectionUtils.isEmpty(constraintViolations)) {
return ApiCommonResponse.success("OK");
}
String errorMsg = constraintViolations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));
return ApiCommonResponse.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
}
}
@GetMapping("/testValidatorBean")
@ResponseBody
public ApiCommonResponse<String> testValidatorBean(Student student) {
validatorBeanService.validate(student);
return ApiCommonResponse.success("OK");
}
参考文章 https://reflectoring.io/bean-validation-with-spring-boot/