目录
API开发中经常会遇到一些对请求数据进行验证的情况,这时候如果使用注解就有两个好处。
- 一是验证逻辑和业务逻辑分离,代码清晰;
- 二是验证逻辑可以轻松复用,只需要在要验证的地方加上注解就可以。
以后开发过程中,关于参数合法性的校验,如果有已实现的注解(例如@NotNull、@NotEmpty等)可以直接使用,没有现成的可用,尽量使用自定义注解,使得业务逻辑与校验分离,校验逻辑复用,代码结构清晰。
需求:接收一个Student
对象,并希望对象里的age
域的值是奇数。下面以此需求为例,解析自定义校验注解。
1. 自定义校验注解步骤
- 一个自定义的注解,并且指定验证器
- 一个验证器的实现
2. 自定义校验注解
@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 {};
}
2.1 基本语法
注解里面定义的是:注解类型元素!
- 访问修饰符必须为public,不写默认为public;
- 该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型(体现了注解的嵌套效果)以及上述类型的一位数组;
- 该元素的名称一般定义为名词,如果注解中只有一个元素,请把名字起为value(使用的时候可以省略指定key,默认即为value);
- ()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法;
default
代表默认值,值必须和第2点定义的类型一致;- 如果没有默认值,代表后续使用注解时必须给该类型元素赋值。
可以看出,注解类型元素的语法非常奇怪,即又有属性的特征(可以赋值),又有方法的特征(打上了一对括号)。但是这么设计是有道理的:注解在定义好了以后,使用的时候操作元素类型像在操作属性,解析的时候操作元素类型像在操作方法。
2.2 几个元注解
- @Target注解,是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的。它使用一个枚举类型定义,包括:TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE、ANNOTATION_TYPE等;
- @Retention注解,翻译为持久力、保持力。即用来修饰自定义注解的生命力。可以有RetentionPolicy.SOURCE(仅保存在源码中,会被编译器丢弃),RetentionPolicy.CLASS(在class文件中可用,会被VM丢弃)以及RetentionPolicy.RUNTIME(在运行期也被保留),这里选择了生命周期最长的RetentionPolicy.RUNTIME;
- @Documented注解,是被用来指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中。
- @Inherited注解,是指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解。@Inherited注解只对那些@Target被定义为ElementType.TYPE的自定义注解起作用。
@Constraint
是最关键的,它表示这个注解是一个验证注解,并且指定了一个实现验证逻辑的验证器message()
指明了验证失败后返回的消息,此方法为@Constraint
要求groups()
和payload()
也为@Constraint
要求,可默认为空,详细用途可以查看@Constraint
文档
3. 创建验证器
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;
}
}
其中,
- 验证器ConstraintValidator有两个类型参数,第一个是所属的注解,第二个是注解作用地方的类型,这里因为作用在
age
上,因此这里用了Integer;
initialize()
可以在验证开始前调用注解里的方法,从而获取到一些注解里的参数,这里用不到;isValid()
就是判断是否合法的地方,参数age就是所验证字段的实际值;
4. 使用注解
注解和验证器创建好之后,就可以使用注解了
@RestController
public class StudentResource {
@PostMapping("/student")
public String addStudent(@Valid @RequestBody Student student) {
return "Student Created";
}
}
public class Student {
@Odd
private int age;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
在需要启用验证的地方加上@Valid
注解,这时候如果请求里的Student
年龄不是奇数,就会得到一个400
响应,这时候注意设置全局异常处理,异常响应体如下:
{
"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"
}
5. 利用反射获取注解
public class TestAnnotation {
public static void main(String[] args){
try {
//获取Student的Class对象
Class stuClass = Class.forName("pojos.Student");
//说明一下,这里形参不能写成Integer.class,应写为int.class
Method stuMethod = stuClass.getMethod("study",int.class);
if(stuMethod.isAnnotationPresent(TestAnnotation.class)){
System.out.println("Student类上配置了TestAnnotation注解!");
//获取该元素上指定类型的注解
TestAnnotation testAnnotation = stuMethod.getAnnotation(TestAnnotation.class);
System.out.println("name: " + TestAnnotation.name() + ", age: " + TestAnnotation.age()
+ ", score: " + TestAnnotation.score()[0]);
}else{
System.out.println("Student类上没有配置TestAnnotation注解!");
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
其中,
- 如果我们要获得的注解是配置在方法上的,那么我们要从Method对象上获取;如果 是配置在属性上,就需要从该属性对应的Field对象上去获取,如果是配置在类型 上,需要从Class对象上去获取。总之在谁身上,就从谁身上去获取!
- isAnnotationPresent(Class annotationClass) 方法是专门判 断该元素上是否配置有某个指定的注解;
- getAnnotation(Class<A> annotationClass) 方法是获取该元素上指定的注解。之后再调用该注解的注解类型元素方法就可以获得配置时的值数据;
- 反射对象上还有一个方法 getAnnotations() ,该方法可以获得该对象身上配置的所有的注解。它会返回给我们一个注解数组,需要注意的是该数组的类型是Annotation类型,这个Annotation是一个来自于java.lang.annotation包的接口。