1. 前言

数据字段个别都要遵循业务要求和数据库设计,所以后端的参数校验是必须的,应用程序必须通过某种伎俩来确保输出进来的数据从语义上来讲是正确的。

2. 数据校验的痛点

为了保证数据语义的正确,咱们须要进行大量的判断来解决验证逻辑。而且我的项目的分层也会造成一些反复的校验,产生大量与业务无关的代码。不利于代码的保护,减少了开发人员的工作量。

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就能够了:

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

一种能够实现接口来定制Validator,一种是应用束缚注解。胖哥感觉注解能够满足绝大部分的需要,所以倡议应用注解来进行数据校验。而且注解更加灵便,管制的粒度也更加细。接下来咱们来学习如何应用注解进行数据校验。

4.1 束缚注解的根本应用

咱们对须要校验的办法入参进行注解束缚标记,例子如下:

@Datapublic 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-50

POST /student/add HTTP/1.1Host: localhost:8888Content-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.1Host: 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@ReportAsSingleViolationpublic @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();}

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

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

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

GET /student/color?color=CAY HTTP/1.1Host: 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的属性进行校验。

@Datapublic class Student {    @NotBlank(message = "姓名必须填")    private String name;    @Age    private Integer age;    @NotEmpty(message = "问题必填")    private List<Double> scores;    @NotNull(message = "学校不能为空")    private School school;}@Datapublic class School {    @NotBlank(message = "学校名称不能为空")    private String name;    @Min(value = 0,message ="校龄大于0" )    private Integer age;}

GET申请时失常校验了School的属性,然而POST申请却无奈对School的属性进行校验。这时咱们只须要在该属性上加上@Valid注解即可。

@Datapublic 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