数据校验是在平时的编码过程当中常作的工做,在系统的各个层可能都要去实现一些校验逻辑,再去作业务处理。这些繁琐的校验与咱们的业务代码在一块就会显得臃肿。并且这些校验一般是业务无关的。也是在工做中使用到Hibernate Validator,但却发现有人没有使用好它(居然还能看到一些if else的校验代码...),因此在这里决定整理下关于Hibernate Validator的使用java
Bean Validation 2.0(JSR 380)定义了用于实体和方法验证的元数据模型和API,Hibernate Validator是目前最好的实现,这篇主要是说Hibernate Validator的使用git
若是是Spring Boot项目,那么spring-boot-starter-web
中就已经依赖hibernate-validator
了web
<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对象添加约束注解spring
@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
实例api
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
复制代码
Validator
接口有三个方法,可用于验证整个实体或仅验证明体的单个属性spring-mvc
Validator#validate()
验证全部bean的全部约束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()));
}
}
复制代码
测试结果bash
不能为空
最大不能超过10
须要匹配正则表达式"[A-Z][a-z][0-9]"
不能为null
不能为空
须要匹配正则表达式"[A-Z][a-z][0-9]"
复制代码
从Bean Validation 1.1开始,约束不只能够应用于JavaBean及其属性,并且能够应用于任何Java类型的方法和构造函数的参数和返回值,这里简单看一个例子mvc
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
接口能够完成方法约束的验证app
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
错误提示信息,能够写死,也能够填写国际化的keygroups
分组信息,容许指定此约束所属的验证组(下面会说到分组约束)payload
有效负载,能够经过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();
}
复制代码