Hibernate Validator校验参数全攻略

1. 前言

数据字段通常都要遵循业务要求和数据库设计,因此后端的参数校验是必须的,应用程序必须经过某种手段来确保输入进来的数据从语义上来说是正确的。java

2. 数据校验的痛点

为了保证数据语义的正确,咱们须要进行大量的判断来处理验证逻辑。并且项目的分层也会形成一些重复的校验,产生大量与业务无关的代码。不利于代码的维护,增长了开发人员的工做量。git

3. JSR 303校验规范及其实现

为了解决上面的痛点,将验证逻辑与相应的领域模型进行绑定是十分有必要的。为此产生了JSR 303 – Bean Validation 规范。[Hibernate Validator]() 是JSR-303的参考实现,它提供了JSR 303规范中全部的约束(constraint)的实现,同时也增长了一些扩展。正则表达式

Hibernate Validator 提供的经常使用的约束注解

约束注解 详细信息
@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(value) 被注释的元素必须符合指定的正则表达式
@Email 被注释的元素必须是电子邮箱地址
@Length 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range 被注释的元素必须在合适的范围内

4. 验证注解的使用

Spring Boot开发中使用Hibernate Validator是很是容易的,引入下面的starter就能够了:spring

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

一种能够实现接口来定制Validator,一种是使用约束注解。胖哥以为注解能够知足绝大部分的需求,因此建议使用注解来进行数据校验。并且注解更加灵活,控制的粒度也更加细。接下来咱们来学习如何使用注解进行数据校验。数据库

4.1 约束注解的基本使用

咱们对须要校验的方法入参进行注解约束标记,例子以下:json

@Data
public class Student {

    @NotBlank(message = "姓名必须填")
    private String name;
    @NotNull(message = "年龄必须填写")
    @Range(min = 1,max =50, message = "年龄取值范围1-50")
    private Integer age;
    @NotEmpty(message = "成绩必填")
    private List<Double> scores;
}

POST请求

而后定义一个POST请求的Spring MVC接口:后端

@RestController
@RequestMapping("/student")
public class StudentController {

    
    @PostMapping("/add")
    public Rest<?> addStudent(@Valid @RequestBody Student student) {
        return RestBody.okData(student);
    }
}

经过对addStudent方法入参添加@Valid来启用参数校验。当使用下面数据进行请求将会抛出MethodArgumentNotValidException异常,提示age范围超出1-50app

POST /student/add HTTP/1.1
Host: localhost:8888
Content-Type: application/json

{
    "name": "felord.cn",
    "age": 77,
    "scores": [
        55
    ]
}

GET请求

如法炮制,咱们定义一个GET请求的接口:框架

@GetMapping("/get")
public Rest<?> getStudent(@Valid Student student) {
    return RestBody.okData(student);
}

使用下面的请求能够正确对学生分数scores进行了校验,可是抛出的并非MethodArgumentNotValidException异常,而是BindException异常。这和使用@RequestBody注解有关系,这对咱们后面的统一处理很是十分重要。数据库设计

GET /student/get?name=felord.cn&age=12 HTTP/1.1
Host: localhost:8888

自定义注解

可能有些同窗注意到上面的年龄我进行了这样的标记:

@NotNull(message = "年龄必须填写")
@Range(min = 1,max =50, message = "年龄取值范围1-50")
private Integer age;

这是由于@Range不会去校验为空的状况,它只处理非空的时候是否符合范围约束。因此要用多个注解来约束。若是咱们某些场景须要重复的捆绑多个注解来使用时,可使用自定义注解将它们封装起来组合使用,下面这个注解就是将@NotNull@Range进行了组合,你能够仿一个出来用用看。

import org.hibernate.validator.constraints.Range;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.NotNull;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import java.lang.annotation.*;

/**
 * @author a
 * @since 17:31
 **/
@Constraint(
        validatedBy = {}
)
@SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT})
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, 
        ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, 
        ElementType.PARAMETER, ElementType.TYPE_USE})
@NotNull
@Range(min = 1, max = 50)
@Documented
@ReportAsSingleViolation
public @interface Age {
    // message 必须有
    String message() default "年龄必须填写,且范围为 1-50 ";

    // 可选
    Class<?>[] groups() default {};

    // 可选
    Class<? extends Payload>[] payload() default {};
}

还有一种状况,咱们在后台定义了枚举值来进行状态的流转,也是须要校验的,好比咱们定义了颜色枚举:

public enum Colors {

    RED, YELLOW, BLUE

}

咱们但愿入参不能超出Colors的范围["RED", "YELLOW", "BLUE"],这就须要实现ConstraintValidator<A extends Annotation, T>接口来定义一个颜色约束了,其中泛型A为自定义的约束注解,泛型T为入参的类型,这里使用字符串,而后咱们的实现以下:

/**
 * @author felord.cn
 * @since 17:57
 **/
public class ColorConstraintValidator implements ConstraintValidator<Color, String> {
    private static final Set<String> COLOR_CONSTRAINTS = new HashSet<>();

    @Override
    public void initialize(Color constraintAnnotation) {
        Colors[] value = constraintAnnotation.value();
        List<String> list = Arrays.stream(value)
                .map(Enum::name)
                .collect(Collectors.toList());
        COLOR_CONSTRAINTS.addAll(list);

    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return COLOR_CONSTRAINTS.contains(value);
    }
}

而后声明对应的约束注解Color,须要在元注解@Constraint中指明使用上面定义好的处理类ColorConstraintValidator进行校验。

/**
 * @author felord.cn
 * @since 17:55
 **/
@Constraint(validatedBy = ColorConstraintValidator.class)
@Documented
@Target({ElementType.METHOD, ElementType.FIELD,
        ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,
        ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Color {
    // 错误提示信息
    String message() default "颜色不符合规格";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    // 约束的类型
    Colors[] value();
}

而后咱们来试一下,先对参数进行约束:

@Data
public class Param {
    @Color({Colors.BLUE,Colors.YELLOW})
   private String color;
}

接口跟上面几个同样,调用下面的接口将抛出BindException异常:

GET /student/color?color=CAY HTTP/1.1
Host: localhost:8888

当咱们把参数color赋值为BLUE或者YELLOW后,可以成功获得响应。

4.2 常见问题

在实际使用起来咱们会遇到一些问题,这里总结了一些常见的问题和处理方式。

检验基础类型不生效的问题

上面为了校验颜色咱们声明了一个Param对象来包装惟一的字符串参数color,为何直接使用下面的方式定义呢?

@GetMapping("/color")
public Rest<?> color(@Valid @Color({Colors.BLUE,Colors.YELLOW}) String color) {
    return RestBody.okData(color);
}

或者使用路径变量:

@GetMapping("/rest/{color}")
public Rest<?> rest(@Valid @Color({Colors.BLUE, Colors.YELLOW}) @PathVariable String color) {
    return RestBody.okData(color);
}

上面两种方式是不会生效的。不信你能够试一试,起码在Spring Boot 2.3.1.RELEASE是不会直接生效的。

使以上两种生效的方法是在类上添加@Validated注解。注意必定要添加到方法所在的类上才行。这时候会抛出ConstraintViolationException异常。

集合类型参数中的元素不生效的问题

就像下面的写法,方法的参数为集合时,如何检验元素的约束呢?

/**
 * 集合类型参数元素.
 *
 * @param student the student
 * @return the rest
 */
@PostMapping("/batchadd")
public Rest<?> batchAddStudent(@Valid @RequestBody List<Student> student) {
    return RestBody.okData(student);
}

一样是在类上添加@Validated注解。注意必定要添加到方法所在的类上才行。这时候会抛出ConstraintViolationException异常。

嵌套校验不生效

嵌套的结构如何校验呢?打个比方,若是咱们在学生类Student中添加了其所属的学校信息School并但愿对School的属性进行校验。

@Data
public class Student {

    @NotBlank(message = "姓名必须填")
    private String name;
    @Age
    private Integer age;
    @NotEmpty(message = "成绩必填")
    private List<Double> scores;
    @NotNull(message = "学校不能为空")
    private School school;
}


@Data
public class School {
    @NotBlank(message = "学校名称不能为空")
    private String name;
    @Min(value = 0,message ="校龄大于0" )
    private Integer age;
}

GET请求时正常校验了School的属性,可是POST请求却没法对School的属性进行校验。这时咱们只须要在该属性上加上@Valid注解便可。

@Data
public class Student {

    @NotBlank(message = "姓名必须填")
    private String name;
    @Age
    private Integer age;
    @NotEmpty(message = "成绩必填")
    private List<Double> scores;
    @Valid
    @NotNull(message = "学校不能为空")
    private School school;
}
每加一层嵌套都须要加一层 @Valid注解。一般在校验对象属性时, @NotNull@NotEmpty@Valid配合才能起到校验效果。

若是你有其它问题能够经过felord.cn联系到我探讨。

5. 总结

经过校验框架咱们能够专心于业务开发,本文对Hibernate Validator的使用和一些常见问题进行了梳理。咱们能够经过Spring Boot统一异常处理来解决参数校验的异常信息的提示问题。具体能够经过关注:码农小胖哥 回复 valid获取相关DEMO

关注公众号:Felordcn 获取更多资讯

我的博客:https://felord.cn

相关文章
相关标签/搜索