hibernate Validator 使用介绍
1.hibernate Validator 简介
平时项目中,难免需要对参数 进行一些参数正确性的校验,这些校验出现在业务代码中,让我们的业务代码显得臃肿,而且,频繁的编写这类参数校验代码很无聊。鉴于此,觉得 Hibernate Validator 框架刚好解决了这些问题,可以很优雅的方式实现参数的校验,让业务代码 和 校验逻辑 分开,不再编写重复的校验逻辑。
Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
Bean Validation 为 JavaBean 验证定义了相应的元数据模型和API。缺省的元数据是 Java Annotations,通过使用 XML 可以对原有的元数据信息进行覆盖和扩展。Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。
2.Hibernate Validator 的作用
- 验证逻辑与业务逻辑之间进行了分离,降低了程序耦合度;
- 统一且规范的验证方式,无需你再次编写重复的验证代码;
- 你将更专注于你的业务,将这些繁琐的事情统统丢在一边。
3.Hibernate Validator 校验Demo
hibernate validator(官方文档)提供了一套比较完善、便捷的验证实现方式。
spring-boot-starter-web
包里面有hibernate-validator
包,不需要引用hibernate validator依赖。
package top.zjzcn.model;
import lombok.Data;
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
/**
* @author zhang.junze
**/
@Data
public class DemoModel1 {
@NotBlank(message = "用户名不能为空")
private String userName;
@NotBlank(message = "年龄不能为空")
@Pattern(regexp = "^[0-9]{1,2}$", message = "年龄不正确")
private String age;
@AssertFalse(message = "必须为false")
private Boolean isFalse;
/**
* 如果是空,则不校验,如果不为空,则校验
*/
@Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", message = "出生日期格式不正确")
private String birthday;
}
package top.zjzcn.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import top.zjzcn.model.DemoModel1;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
/**
* @author zhang.junze
**/
@RestController
@RequestMapping("validate")
@Slf4j
public class ValidateController {
//如果成功返回success,不成功返回错误信息
@GetMapping("/demo1")
public void testDemo1(@RequestBody @Valid DemoModel1 model1, BindingResult result){
if(result.hasErrors()){
for (ObjectError error : result.getAllErrors()) {
System.out.println(error.getDefaultMessage());
}
}
}
}
POST请求传入的参数{}
[用户名不能为空, 年龄不能为空]
POST请求传入的参数{"userName":"zhangjz","age":120,"isFalse":true,"birthday":"21010-21-12"}
[出生日期格式不正确, 年龄不正确, 必须为false]
4.Hibernate Validator 的校验模式
Hibernate Validator有以下两种验证模式:
- 普通模式(默认是这个模式)
普通模式(会校验完所有的属性,然后返回所有的验证失败信息)
- 快速失败返回模式
快速失败返回模式(只要有一个验证失败,则返回)
两种验证模式配置方式:(参考官方文档)
failFast:true 快速失败返回模式 false 普通模式
ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
.configure()
.addProperty("hibernate.validator.fail_fast", "false")
.buildValidatorFactory();
return factory.getValidator();
和 (hibernate.validator.fail_fast:true 快速失败返回模式 false 普通模式)
ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
.configure()
.addProperty("hibernate.validator.fail_fast", "true")
.buildValidatorFactory();
return factory.getValidator();
配置hibernate Validator为快速失败返回模式:
package top.zjzcn.config;
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
/**
* @author zhang.junze
**/
public class ValidatorConfig {
@Bean
public Validator validator() {
ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
.configure()
// 将fail_fast设置为true即可,如果想验证全部,则设置为false或者取消配置即可
.addProperty("hibernate.validator.fail_fast", "true")
.buildValidatorFactory();
return factory.getValidator();
}
}
5.Hibernate Validator简单实践
以下为最简使用方式
- 全局异常类
package top.zjzcn.config;
@Slf4j
@RestControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {
/**
* 统一处理请求参数校验(实体对象传参)
*
* @param e BindException
* @return FebsResponse
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String validExceptionHandler(BindException e) {
List<String> errorInformation = e.getBindingResult().getAllErrors()
.stream()
.map(ObjectError::getDefaultMessage).collect(Collectors.toList());
return errorInformation.toString();
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String validExceptionHandler(MethodArgumentNotValidException e) {
List<String> errorInformation = e.getBindingResult().getAllErrors()
.stream()
.map(ObjectError::getDefaultMessage).collect(Collectors.toList());
return errorInformation.toString();
}
/**
* 统一处理请求参数校验(普通传参)
*
* @param e ConstraintViolationException
* @return FebsResponse
*/
@ExceptionHandler(value = ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handleConstraintViolationException(ConstraintViolationException e) {
List<String> errorInformation = e.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList());
return errorInformation.toString();
}
}
- 校验模式配置类
@Configuration
public class ValidatorConfig {
@Bean
public Validator validator() {
ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
.configure()
// 将fail_fast设置为true即可,如果想验证全部,则设置为false或者取消配置即可
.addProperty("hibernate.validator.fail_fast", "true")
.buildValidatorFactory();
return factory.getValidator();
}
}
- Model类
@Data
public class DemoModel1 {
@NotBlank(message = "用户名不能为空")
private String userName;
@NotBlank(message = "年龄不能为空")
@Pattern(regexp = "^[0-9]{1,2}$", message = "年龄不正确")
private String age;
@AssertFalse(message = "必须为false")
private Boolean isFalse;
/**
* 如果是空,则不校验,如果不为空,则校验
*/
@Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", message = "出生日期格式不正确")
private String birthday;
}
- Controller类
@RestController
@RequestMapping("validate")
@Validated
@Slf4j
public class ValidateController {
//如果成功返回success,不成功返回错误信息
@PostMapping("/demo1")
public String testDemo1(@RequestBody @Valid DemoModel1 model1, BindingResult result){
if(result.hasErrors()){
for (ObjectError error : result.getAllErrors()) {
System.out.println(error.getDefaultMessage());
}
}
return "success";
}
@GetMapping()
public String getName(@NotBlank(message = "不能为空") String name, @Size(min=5,max = 10,message = "有效长度{min}到{max}个字符")String password){
log.info("进来了==="+name+"=="+password);
return name+"=="+password;
}
}
6.自定义Constraint的实现
有时候框架提供的校验注解(constraint)并不能满足我们的需求,所以需要自定义自己的校验规则。满足自己的校验需求。
比如我希望的时间格式是:yyyy-MM-dd HH:mm:ss
,就需要字符串格式能匹配这个格式。于是自己就实现了一个自定义注解来实现校验。
6.1 编写constraint
- 实现类必须实现接口ConstraintValidator
- 注解必须有@Constraint(validatedBy = {******.class}) 注解标注,validateBy 的值就是校验逻辑的实现类
- 自定义注解必须包含message ,groups,payload 属性
import org.apache.commons.lang3.time.DateUtils;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import java.lang.annotation.*;
import java.text.ParseException;
import java.util.Date;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {DateValidator.DateValidatorInner.class})
public @interface DateValidator {
/**
* 必须的属性
* 显示 校验信息
* 利用 {} 获取 属性值,参考了官方的message编写方式
*
* @see org.hibernate.validator 静态资源包里面 message 编写方式
*/
String message() default "日期格式不匹配{dateFormat}";
/**
* 必须的属性
* 用于分组校验
*/
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* 非必须
*/
String dateFormat() default "yyyy-MM-dd HH:mm:ss";
/**
* 必须要实现的接口
*/
class DateValidatorInner implements ConstraintValidator<DateValidator, String> {
private String dateFormat;
/**
* 初始化内容
* @param dateValidator
*/
@Override
public void initialize(DateValidator dateValidator) {
this.dateFormat = dateValidator.dateFormat();
}
/**
* 校验逻辑的实现
*
* @param value 需要校验的 值
* @return 布尔值结果
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (value == null) {
return true;
}
if ("".equals(value)) {
return true;
}
try {
Date date = DateUtils.parseDate(value, dateFormat);
return date != null;
} catch (ParseException e) {
return false;
}
}
}
}
4.2 测试自定义注解
- 在类DemoModel1中新增验证属性
import lombok.Data;
import top.zjzcn.common.constraint.DateValidator;
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
/**
* @author zhang.junze
**/
@Data
public class DemoModel1 {
@NotBlank(message = "{required}")
private String userName;
@NotBlank(message = "{required}")
@Pattern(regexp = "^[0-9]{1,2}$", message = "{age}")
private String age;
@AssertFalse(message = "必须为false")
private Boolean isFalse;
/**
* 如果是空,则不校验,如果不为空,则校验
*/
@DateValidator //此为新增
@Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", message = "{date}")
private String birthday;
}
- 测试代码
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import top.zjzcn.model.DemoModel1;
import top.zjzcn.utils.ValidationUtil;
@SpringBootTest
class HibernateValidatorDemoApplicationTests {
@Test
void contextLoads() {
DemoModel1 demoModel1 = new DemoModel1();
demoModel1.setUserName("zhangjz");
demoModel1.setAge("120");
demoModel1.setIsFalse(true);
demoModel1.setBirthday("201010-21-12");
ValidationUtil.ValidResult validResult = ValidationUtil.validateBean(demoModel1);
if(validResult.hasErrors()){
String errors = validResult.getErrors();
System.out.println(errors);
}
}
}
-
测试结果
age:年龄不正确 isFalse:必须为false birthday:日期格式不正确 birthday:日期格式不匹配yyyy-MM-dd HH:mm:ss
7.分组
添加校验注解的方式固然很方便,但是如果同一验证对象(eg:DemoModel) 在不同的业务中校验规则不同的话,难道我们需要编写两个验证对象么?答案肯定不是,我们可以使用分组校验,对不同的校验规则进行隔离校验,互相不受影响。
7.1 DemoModel添加多套校验规则
可以自己定义空的class对象当作group。如果不指定groups 那么就是默认的即为Default.class 分组。
import lombok.Data;
import top.zjzcn.common.constraint.DateValidator;
import top.zjzcn.common.groups.AddGroup;
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
/**
* @author zhang.junze
**/
@Data
public class DemoModel1 {
@NotBlank(message = "{required}")
private String userName;
@NotBlank(message = "{required}")
@Pattern(regexp = "^[0-9]{1,2}$", message = "{age}",groups = {AddGroup.class})
private String age;
@AssertFalse(message = "必须为false",groups = {AddGroup.class})
private Boolean isFalse;
/**
* 如果是空,则不校验,如果不为空,则校验
*/
@DateValidator
@Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", message = "{date}")
private String birthday;
}
7.2 测试分组校验
- 测试代码
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import top.zjzcn.common.groups.AddGroup;
import top.zjzcn.model.DemoModel1;
import top.zjzcn.utils.ValidationUtil;
@SpringBootTest
class HibernateValidatorDemoApplicationTests {
@Test
void contextLoads() {
DemoModel1 demoModel1 = new DemoModel1();
demoModel1.setUserName("zhangjz");
demoModel1.setAge("120");
demoModel1.setIsFalse(true);
demoModel1.setBirthday("201010-21-12");
ValidationUtil.ValidResult validResult = ValidationUtil.validateBean(demoModel1, AddGroup.class);
if(validResult.hasErrors()){
String errors = validResult.getErrors();
System.out.println(errors);
}
}
}
- 测试结果
由于age与isFalse指定分组,所以只会校验该属性
isFalse:必须为false age:年龄不正确
8. ValidationMessages.properties属性文件使用
Spring boot 表单验证 @Validated
的 message 国际化资源文件默认必须放在resources/ValidationMessages.properties
中
如果需要更改资源文件地址,需要进行如下配置
@Configuration
public class ValidationConfig {
@Bean
public Validator getValidator() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("资源文件所在位置");
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(messageSource);
return validator;
}
}
9.使用封装工具类代码中校验
- validationUtil 工具类
package top.zjzcn.utils;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import lombok.Data;
import org.hibernate.validator.HibernateValidator;
/**
* @author zhang.junze
* @description 校验工具类
* @date 2020/3/10
**/
public class ValidationUtil {
/**
* 开启快速结束模式 failFast (true)
*/
private static Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(false).buildValidatorFactory().getValidator();
/**
* 校验对象
*
* @param t bean
* @param groups 校验组
* @return ValidResult
*/
public static <T> ValidResult validateBean(T t, Class<?>... groups) {
ValidResult result = new ValidationUtil().new ValidResult();
Set<ConstraintViolation<T>> violationSet = validator.validate(t, groups);
boolean hasError = violationSet != null && violationSet.size() > 0;
result.setHasErrors(hasError);
if (hasError) {
for (ConstraintViolation<T> violation : violationSet) {
result.addError(violation.getPropertyPath().toString(), violation.getMessage());
}
}
return result;
}
/**
* 校验bean的某一个属性
*
* @param obj bean
* @param propertyName 属性名称
* @return ValidResult
*/
public static <T> ValidResult validateProperty(T obj, String propertyName) {
ValidResult result = new ValidationUtil().new ValidResult();
Set<ConstraintViolation<T>> violationSet = validator.validateProperty(obj, propertyName);
boolean hasError = violationSet != null && violationSet.size() > 0;
result.setHasErrors(hasError);
if (hasError) {
for (ConstraintViolation<T> violation : violationSet) {
result.addError(propertyName, violation.getMessage());
}
}
return result;
}
/**
* 校验结果类
*/
@Data
public class ValidResult {
/**
* 是否有错误
*/
private boolean hasErrors;
/**
* 错误信息
*/
private List<ErrorMessage> errors;
public ValidResult() {
this.errors = new ArrayList<>();
}
public boolean hasErrors() {
return hasErrors;
}
public void setHasErrors(boolean hasErrors) {
this.hasErrors = hasErrors;
}
/**
* 获取所有验证信息
*
* @return 集合形式
*/
public List<ErrorMessage> getAllErrors() {
return errors;
}
/**
* 获取所有验证信息
*
* @return 字符串形式
*/
public String getErrors() {
StringBuilder sb = new StringBuilder();
for (ErrorMessage error : errors) {
sb.append(error.getPropertyPath()).append(":").append(error.getMessage()).append(" ");
}
return sb.toString();
}
public void addError(String propertyName, String message) {
this.errors.add(new ErrorMessage(propertyName, message));
}
}
@Data
public class ErrorMessage {
private String propertyPath;
private String message;
public ErrorMessage() {
}
public ErrorMessage(String propertyPath, String message) {
this.propertyPath = propertyPath;
this.message = message;
}
}
}
- 测试代码
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import top.zjzcn.model.DemoModel1;
import top.zjzcn.utils.ValidationUtil;
@SpringBootTest
class HibernateValidatorDemoApplicationTests {
@Test
void contextLoads() {
DemoModel1 demoModel1 = new DemoModel1();
demoModel1.setUserName("zhangjz");
demoModel1.setAge("120");
demoModel1.setIsFalse(true);
demoModel1.setBirthday("201010-21-12");
ValidationUtil.ValidResult validResult = ValidationUtil.validateBean(demoModel1);
if(validResult.hasErrors()){
String errors = validResult.getErrors();
System.out.println(errors);
}
}
}
10.常用的注解
Bean Validation 中内置的 constraint
@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 附加的 constraint
@NotBlank(message =) 验证字符串非null,且长度必须大于0
@Email 被注释的元素必须是电子邮箱地址
@Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range(min=,max=,message=) 被注释的元素必须在合适的范围内//大于0.01,不包含0.01
@NotNull
@DecimalMin(value = "0.01", inclusive = false)
private Integer greaterThan;//大于等于0.01
@NotNull
@DecimalMin(value = "0.01", inclusive = true)
private BigDecimal greatOrEqualThan;@Length(min = 1, max = 20, message = "message不能为空")
//不能将Length错用成Range
//@Range(min = 1, max = 20, message = "message不能为空")
private String message;
参考:
https://www.cnblogs.com/mr-yang-localhost/p/7812038.html#_label4