API开发中经常会遇到一些对请求数据进行验证的情况,这时候如果使用注解就有两个好处:
1、一是验证逻辑和业务逻辑分离,代码清晰
2、二是验证逻辑可以轻松复用,只需要在要验证的地方加上注解就可以
因此,我们在业务开发过程中经常遇到形形色色的注解(Java提供了一些基本的验证注解,比如 @NotNull
、@Size
),框架自有的注解并不是总能满足复杂的业务需求,我们可以自定义注解来满足我们的需求。方法很简单,仅需要两个东西:
- 一个自定义的注解,并且指定验证器
- 一个验证器的实现
字段注解一般是用于校验字段是否满足要求,比如JSR303校验依赖就提供了很多校验注解 ,如@NotNull、@Range等,但是这些注解并不是能够满足所有业务场景的。比如我们希望传入的参数在指定的String集合中,那么已有的注解就不能满足需求了,需要自己实现。
1、定义一个@Check注解,通过@interface声明一个注解
// 校验对象里的age域的值是奇数,这时候就可以创建以下注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = AgeValidator.class)
public @interface Odd {
String message() default "Age Must Be Odd";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
其中:@Constraint 通过使用validatedBy来指定与注解关联的验证器
@Target
指明这个注解要作用在什么地方,可以是对象、域、构造器等,因为要作用在age
域上,因此这里选择FIELD
@Retention
指明了注解的生命周期,可以有SOURCE
(仅保存在源码中,会被编译器丢弃),CLASS
(在class文件中可用,会被VM丢弃)以及RUNTIME
(在运行期也被保留),这里选择了生命周期最长的RUNTIME
@Constraint
是最关键的,它表示这个注解是一个验证注解,并且指定了一个实现验证逻辑的验证器message()
指明了验证失败后返回的消息,此方法为@Constraint
要求groups()
和payload()
也为@Constraint
要求,可默认为空,详细用途可以查看@Constraint
文档
2、创建验证器:验证器类需要实现ConstraintValidator泛型接口
有了注解之后,就需要一个验证器来实现验证逻辑:这个就是上面 @Constraint 通过使用validatedBy 指定的类
public class AgeValidator implements ConstraintValidator<Odd,Integer> {
@Override
public void initialize(Odd constraintAnnotation) {
}
@Override
public boolean isValid(Integer age, ConstraintValidatorContext constraintValidatorContext) {
return age % 2 != 0;
}
}
- 验证器有两个类型参数,第一个是所属的注解(名称Odd),第二个是注解作用地方的类型,这里因为作用在
age
上,因此这里用了Integer
initialize()
可以在验证开始前调用注解里的方法,从而获取到一些注解里的参数isValid()
就是判断是否合法的地方
3、使用方式:定义一个实体类
@Data
public class User {
// 性别只能从 man or women 里取
@Check(paramValues = {"man", "women"}
private String sex;
// 年龄只能为奇数
@Odd(message = "年龄不对")
private Integer age;
}
也可以自定义message,就像上面 age 那样。
另外需要注意的是:在需要启用验证的地方一定要加上@Valid
注解,这时候如果请求里的Student
年龄不是奇数,就会得到一个400
响应:
@RestController
public class StudentResource {
@PostMapping("/student")
public String addStudent(@Valid @RequestBody User user) {
return "Student Created";
}
}
{
"timestamp": "2018-08-15T17:01:44.598+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"Odd.student.age",
"Odd.age",
"Odd.int",
"Odd"
],
"arguments": [
{
"codes": [
"student.age",
"age"
],
"arguments": null,
"defaultMessage": "age",
"code": "age"
}
],
"defaultMessage": "Age Must Be Odd",
"objectName": "student",
"field": "age",
"rejectedValue": 12,
"bindingFailure": false,
"code": "Odd"
}
],
"message": "Validation failed for object='student'. Error count: 1",
"path": "/student"
}
也可以手动来处理错误,加上一个BindingResult
来接收验证结果即可:
@RestController
public class StudentResource {
@PostMapping("/student")
public String addStudent(@Valid @RequestBody Student student, BindingResult validateResult) {
if (validateResult.hasErrors()) {
return validateResult.getAllErrors().get(0).getDefaultMessage();
}
return "Student Created";
}
}
这样我们就可以使用自定义参数校验来补充JSR303默认校验不够的情况咯,挺好用的。