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 束缚注解的根本应用
咱们对须要校验的办法入参进行注解束缚标记,例子如下:
@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-50
。
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