数据校验是在平时的编码过程中常做的工作,在系统的各个层可能都要去实现一些校验逻辑,再去做业务处理。这些繁琐的校验与我们的业务代码在一块就会显得臃肿。而且这些校验通常是业务无关的。也是在工作中使用到Hibernate Validator,但却发现有人没有使用好它(竟然还能看到一些if else的校验代码...),所以在这里决定整理下关于Hibernate Validator的使用
Bean Validation 2.0(JSR 380)定义了用于实体和方法验证的元数据模型和API,Hibernate Validator是目前最好的实现,这篇主要是说Hibernate Validator的使用
如果是Spring Boot项目,那么 spring-boot-starter-web
中就已经依赖 hibernate-validator
了
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> 复制代码
如果是Spring Mvc,那可以直接添加 hibernate-validator
依赖
<dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.17.Final</version> </dependency> 复制代码
先给我们的Java对象添加约束注解
@Data @AllArgsConstructor public class User { private String id; @NotBlank @Size(max = 20) private String name; @NotNull @Pattern(regexp = "[A-Z][a-z][0-9]") private String password; @NotNull private Integer age; @Max(10) @Min(1) private Integer level; } 复制代码
验证实体实例需要先获取 Validator
实例
ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); validator = factory.getValidator(); 复制代码
Validator
接口有三个方法,可用于验证整个实体或仅验证实体的单个属性
Validator#validate() Validator#validateProperty() Validator#validateValue()
public class UserTest { private static Validator validator; @BeforeAll public static void setUpValidator() { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); validator = factory.getValidator(); } @Test public void validatorTest() { User user = new User(null, "", "!@#$", null, 11); // 验证所有bean的所有约束 Set<ConstraintViolation<User>> constraintViolations = validator.validate(user); // 验证单个属性 Set<ConstraintViolation<User>> constraintViolations2 = validator.validateProperty(user, "name"); // 检查给定类的单个属性是否可以成功验证 Set<ConstraintViolation<User>> constraintViolations3 = validator.validateValue(User.class, "password", "sa!"); constraintViolations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); constraintViolations2.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); constraintViolations3.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); } } 复制代码
测试结果
不能为空 最大不能超过10 需要匹配正则表达式"[A-Z][a-z][0-9]" 不能为null 不能为空 需要匹配正则表达式"[A-Z][a-z][0-9]" 复制代码
从Bean Validation 1.1开始,约束不仅可以应用于JavaBean及其属性,而且可以应用于任何Java类型的方法和构造函数的参数和返回值,这里简单看一个例子
public class RentalStation { public RentalStation(@NotNull String name) { //... } public void rentCar(@NotNull @Future LocalDate startDate, @Min(1) int durationInDays) { //... } @NotNull @Size(min = 1) public List<@NotNull String> getCustomers() { //... return null; } } 复制代码
ExecutableValidator
接口可以完成方法约束的验证
ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); executableValidator = factory.getValidator().forExecutables(); 复制代码
该 ExecutableValidator
界面共有四种方法:
validateParameters()
和 validateReturnValue()
用于方法验证 validateConstructorParameters()
和 validateConstructorReturnValue()
用于构造函数验证 public class RentalStationTest { private static ExecutableValidator executableValidator; @BeforeAll public static void setUpValidator() { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); executableValidator = factory.getValidator().forExecutables(); } @Test public void validatorTest() throws NoSuchMethodException { RentalStation rentalStation = new RentalStation("z"); Method method = RentalStation.class.getMethod("rentCar", LocalDate.class, int.class); Object[] parameterValues = {LocalDate.now().minusDays(1), 0}; Set<ConstraintViolation<RentalStation>> violations = executableValidator.validateParameters( rentalStation, method, parameterValues); violations.forEach(violation -> System.out.println(violation.getMessage())); } } 复制代码
测试结果
需要是一个将来的时间 最小不能小于1 复制代码
validator-api-2.0的约束注解有22个,具体我们看下面表格
注解 | 支持Java类型 | 说明 |
---|---|---|
@Null | Object | 为null |
@NotNull | Object | 不为null |
@NotBlank | CharSequence | 不为null,且必须有一个非空格字符 |
@NotEmpty | CharSequence、Collection、Map、Array | 不为null,且不为空(length/size>0) |
注解 | 支持Java类型 | 说明 | 备注 |
---|---|---|---|
@AssertTrue | boolean、Boolean | 为true | 为null有效 |
@AssertFalse | boolean、Boolean | 为false | 为null有效 |
注解 | 支持Java类型 | 说明 | 备注 |
---|---|---|---|
@Future | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 验证日期为当前时间之后 | 为null有效 |
@FutureOrPresent | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 验证日期为当前时间或之后 | 为null有效 |
@Past | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 验证日期为当前时间之前 | 为null有效 |
@PastOrPresent | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 验证日期为当前时间或之前 | 为null有效 |
注解 | 支持Java类型 | 说明 | 备注 |
---|---|---|---|
@Max | BigDecimal、BigInteger,byte、short、int、long以及包装类 | 小于或等于 | 为null有效 |
@Min | BigDecimal、BigInteger,byte、short、int、long以及包装类 | 大于或等于 | 为null有效 |
@DecimalMax | BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类 | 小于或等于 | 为null有效 |
@DecimalMin | BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类 | 大于或等于 | 为null有效 |
@Negative | BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 | 负数 | 为null有效,0无效 |
@NegativeOrZero | BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 | 负数或零 | 为null有效 |
@Positive | BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 | 正数 | 为null有效,0无效 |
@PositiveOrZero | BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 | 正数或零 | 为null有效 |
@Digits(integer = 3, fraction = 2) | BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类 | 整数位数和小数位数上限 | 为null有效 |
注解 | 支持Java类型 | 说明 | 备注 |
---|---|---|---|
@Pattern | CharSequence | 匹配指定的正则表达式 | 为null有效 |
CharSequence | 邮箱地址 |
为null有效,默认正则 '.*'
|
|
@Size | CharSequence、Collection、Map、Array | 大小范围(length/size>0) | 为null有效 |
注解 | 支持Java类型 | 说明 |
---|---|---|
@Length | String | 字符串长度范围 |
@Range | 数值类型和String | 指定范围 |
@URL | URL地址验证 |
除了以上提供的约束注解(大部分情况都是能够满足的),我们还可以根据自己的需求自定义自己的约束注解
定义自定义约束,有三个步骤
那么下面就直接来定义一个简单的验证手机号码的注解
@Documented @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Constraint(validatedBy = {MobileValidator.class}) @Retention(RUNTIME) @Repeatable(Mobile.List.class) public @interface Mobile { /** * 错误提示信息,可以写死,也可以填写国际化的key */ String message() default "手机号码不正确"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String regexp() default "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])//d{8}$"; @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) @Documented @interface List { Mobile[] value(); } } 复制代码
关于注解的配置这里不说了,自定义约束需要下面3个属性
message groups payload
@Repeatable
注解和 List
定义可以让该注解在同一个位置重复多次,通常是不同的配置(比如不同的分组和消息)
@Constraint(validatedBy = {MobileValidator.class})
该注解是指明我们的自定义约束的验证器,那下面就看一下验证器的写法,需要实现 javax.validation.ConstraintValidator
接口
public class MobileValidator implements ConstraintValidator<Mobile, String> { /** * 手机验证规则 */ private Pattern pattern; @Override public void initialize(Mobile mobile) { pattern = Pattern.compile(mobile.regexp()); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (value == null) { return true; } return pattern.matcher(value).matches(); } } 复制代码
ConstraintValidator
接口定义了在实现中设置的两个类型参数。第一个指定要验证的注解类(如 Mobile
),第二个指定验证器可以处理的元素类型(如 String
); initialize()
方法可以访问约束注解的属性值; isValid()
方法用于验证,返回true表示验证通过
Bean验证规范建议将空值视为有效。如果 null
不是元素的有效值,则应使用 @NotNull
显式注释
到这里我们自定义的约束就写好了,可以用个例子来测试一下
public class MobileTest { public void setMobile(@Mobile String mobile){ // to do } private static ExecutableValidator executableValidator; @BeforeAll public static void setUpValidator() { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); executableValidator = factory.getValidator().forExecutables(); } @Test public void manufacturerIsNull() throws NoSuchMethodException { MobileTest mobileTest = new MobileTest(); Method method = MobileTest.class.getMethod("setMobile", String.class); Object[] parameterValues = {"1111111"}; Set<ConstraintViolation<MobileTest>> violations = executableValidator.validateParameters( mobileTest, method, parameterValues); violations.forEach(violation -> System.out.println(violation.getMessage())); } } 复制代码
手机号码不正确 复制代码
在上面的自定义约束中,有个 groups
属性是用来指定验证约束的分组,我们在为属性加上注解的时候,如果没有配置分组信息,那么默认会采用默认分组 javax.validation.groups.Default
分组是用接口定义的,用做标识,这里创建两个标识 AddGroup
和 UpdateGroup
,分别标识新增和修改
public interface AddGroup { } public interface UpdateGroup { } 复制代码
然后对我们的 User
对象的id属性做分组标识
@Data @NoArgsConstructor @AllArgsConstructor public class User { @Null(groups = AddGroup.class) @NotBlank(groups = UpdateGroup.class) private String id; // ... 省略了其他属性 } 复制代码
我们看下如何使用
@Test public void validatorGroupTest() { User user = new User(); // 检查给定类的单个属性是否可以成功验证 Set<ConstraintViolation<User>> constraintViolations = validator.validateValue(User.class, "id", "", UpdateGroup.class); Set<ConstraintViolation<User>> constraintViolations2 = validator.validateValue(User.class, "id", ""); Set<ConstraintViolation<User>> constraintViolations3 = validator.validateValue(User.class, "id", "", AddGroup.class); constraintViolations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); constraintViolations2.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); constraintViolations3.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); } 复制代码
上面的测试只有加了 UpdateGroug
分组才会验证,返回错误信息,而下面的constraintViolations2并不会去验证,因为默认会采用 Default
分组。如果想要不标记分组的时候,也会去验证 Default
分组,可以去继承默认分组
public interface AddGroup extends Default { } 复制代码
上面介绍了Validator的一些使用,还有注解的介绍,那么在Spring中我们怎么去使用Hibernate Validator做验证呢?或者说再Web项目中怎么使用Hibernate Validator?
spring-boot-starter-web
中是添加了 hibernate-validator
依赖的,说明Spring Boot本身也是使用到了Hibernate Validator验证框架的
@Configuration public class ValidatorConfig { /** * 配置验证器 * * @return validator */ @Bean public Validator validator() { ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() // 快速失败模式 .failFast(true) // .addProperty( "hibernate.validator.fail_fast", "true" ) .buildValidatorFactory(); return validatorFactory.getValidator(); } } 复制代码
可以通过方法 failFast(true)
或 addProperty("hibernate.validator.fail_fast", "true")
设置为 快速失败模式
,快速失败模式在校验过程中,当遇到第一个不满足条件的参数时就立即返回,不再继续后面参数的校验。否则会一次性校验所有参数,并返回所有不符合要求的错误信息
如果是Spring MVC的话,需要xml配置可参考下面的配置
<mvc:annotation-driven validator="validator"/> <!-- validator基本配置 --> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"> <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/> <!-- 映射资源文件 --> <property name="validationMessageSource" ref="messageSource"/> </bean> <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource" name="messageSource"> <!--<property name="basenames"> <list> <value>classpath:messages/messages</value> <value>classpath:messages/ValidationMessages</value> </list> </property>--> <property name="useCodeAsDefaultMessage" value="false" /> <property name="defaultEncoding" value="UTF-8" /> <property name="cacheSeconds" value="60" /> </bean> 复制代码
接口上的Bean验证,需要在参数前加上 @Valid
或Spring的 @Validated
注解,这两种注释都会导致应用标准Bean验证。如果验证不通过会抛出 BindException
异常,并变成400(BAD_REQUEST)响应;或者可以通过 Errors
或 BindingResult
参数在控制器内本地处理验证错误。另外,如果参数前有 @RequestBody
注解,验证错误会抛出 MethodArgumentNotValidException
异常。
@RestController public class UserController { @PostMapping("/user") public R handle(@Valid @RequestBody User user, BindingResult result) { // 在控制器内本地处理验证错误 if (result.hasErrors()) { result.getAllErrors().forEach(s -> System.out.println(s.getDefaultMessage())); return R.fail(result.getAllErrors().get(0).getDefaultMessage()); } // ... return R.success(); } @PostMapping("/user2") public R handle2(@Valid User user, BindingResult result) { // 在控制器内本地处理验证错误 if (result.hasErrors()) { result.getAllErrors().forEach(s -> System.out.println(s.getDefaultMessage())); return R.fail(result.getAllErrors().get(0).getDefaultMessage()); } // ... return R.success(); } /** * 验证不通过抛出 `MethodArgumentNotValidException` */ @PostMapping("/user3") public R handle3(@Valid @RequestBody User user) { // ... return R.success(); } /** * 验证不通过抛出 `BindException` */ @PostMapping("/user4") public R handle4(@Valid User user) { // ... return R.success(); } } 复制代码
配合Spring的 BindingResult
参数,我们是可以在控制器中去处理验证错误,不过通常也是把验证错误的消息转成我们自己的返回格式,那么在每个方法中都去做这样的验证错误处理,显然是没有必要的。我们可以利用验证不通过的异常来做统一的错误处理
@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { /** * hibernate validator 数据绑定验证异常拦截 * * @param e 绑定验证异常 * @return 错误返回消息 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(BindException.class) public R validateErrorHandler(BindException e) { ObjectError error = e.getAllErrors().get(0); log.info("数据验证异常:{}", error.getDefaultMessage()); return R.fail(error.getDefaultMessage()); } /** * hibernate validator 数据绑定验证异常拦截 * * @param e 绑定验证异常 * @return 错误返回消息 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentNotValidException.class) public R validateErrorHandler(MethodArgumentNotValidException e) { ObjectError error = e.getBindingResult().getAllErrors().get(0); log.info("数据验证异常:{}", error.getDefaultMessage()); return R.fail(error.getDefaultMessage()); } } 复制代码
Hibernate Validator是可以在方法级验证参数的,Spring中当然也是有实现的。
我们在Validator的配置中,添加 MethodValidationPostProcessor
Bean,在上面的ValidatorConfig.java中添加一下配置
/** * 设置方法参数验证器 */ @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); // 设置validator模式为快速失败返回 postProcessor.setValidator(validator()); return postProcessor; } 复制代码
如果是Spring Mvc,那么要在spring-mvc.xml中声明bean信息,不然在Controller里面是无效的
<!-- 设置方法参数验证器 --> <bean id="methodValidationPostProcessor" class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"> <property name="validator" ref="validator"/> </bean> 复制代码
配置了上面的 MethodValidationPostProcessor
,我们就可以在方法参数或返回值使用约束注解了,要注意的是,在要使用参数验证的类上一定要加上 @Validated
注解,否则无效
/** * 一定要加上 `@Validated` 注解 */ @Validated @RestController public class UserController { @GetMapping("/user") public R handle(@Mobile String mobile) { // ... return R.success(); } } 复制代码
如果验证不通过,会抛出 ConstraintViolationException
异常,同样的,我们可以在全局的异常处理器里面处理验证错误,在GlobalExceptionHandler中添加一下代码
/** * spring validator 方法参数验证异常拦截 * * @param e 绑定验证异常 * @return 错误返回消息 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(ConstraintViolationException.class) public R defaultErrorHandler(ConstraintViolationException e) { Set<ConstraintViolation<?>> violations = e.getConstraintViolations(); ConstraintViolation<?> violation = violations.iterator().next(); log.info("数据验证异常:{}", violation.getMessage()); return R.fail(violation.getMessage()); } 复制代码
Spring的 @Validate
注解是可以支持分组验证的
@PostMapping("/user") public R handle(@Validated(AddGroup.class) @RequestBody User user) { // ... return R.success(); } 复制代码