如何优雅的做数据校验-Hibernate Validator详细使用说明

原文地址

数据校验是在平时的编码过程中常做的工作,在系统的各个层可能都要去实现一些校验逻辑,再去做业务处理。这些繁琐的校验与我们的业务代码在一块就会显得臃肿。而且这些校验通常是业务无关的。也是在工作中使用到Hibernate Validator,但却发现有人没有使用好它(竟然还能看到一些if else的校验代码...),所以在这里决定整理下关于Hibernate Validator的使用

Bean Validation 2.0(JSR 380)定义了用于实体和方法验证的元数据模型和API,Hibernate Validator是目前最好的实现,这篇主要是说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>
bean约束声明和验证,Validator
先给我们的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() 验证所有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()));
}

}
测试结果

不能为空
最大不能超过10
需要匹配正则表达式"[A-Z][a-z][0-9]"
不能为null
不能为空
需要匹配正则表达式"[A-Z][a-z][0-9]"
方法约束声明和验证,ExecutableValidator
从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)
Boolean值检查
注解 支持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有效
@Email CharSequence 邮箱地址 为null有效,默认正则 '.*'
@Size CharSequence、Collection、Map、Array 大小范围(length/size>0) 为null有效
hibernate-validator扩展约束(部分)
注解 支持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 错误提示信息,可以写死,也可以填写国际化的key
groups 分组信息,允许指定此约束所属的验证组(下面会说到分组约束)
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 {
}
在Spring中使用Hibernate Validator
上面介绍了Validator的一些使用,还有注解的介绍,那么在Spring中我们怎么去使用Hibernate Validator做验证呢?或者说再Web项目中怎么使用Hibernate Validator?

spring-boot-starter-web中是添加了hibernate-validator依赖的,说明Spring Boot本身也是使用到了Hibernate Validator验证框架的

配置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"/>


<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="useCodeAsDefaultMessage" value="false" />
<property name="defaultEncoding" value="UTF-8" />
<property name="cacheSeconds" value="60" />
</bean>
请求参数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的配置中,添加MethodValidationPostProcessorBean,在上面的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();
}

作者:TurboSnail
链接:https://www.jianshu.com/p/3267689ebf1b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容