注解的原理与实现

注解这个东西自从SpringBoot以来一直是Java开发者们必备的生存技巧呀,我们平时几乎大部分时间都是面向注解编程,通过注解我们可以节约大量的时间。用过了这么多的注解,那么我们否有关注过注解的实现原理呢?所以本篇文章主要是讲述注解的有关操作,自己实现一个注解来体会注解的实现原理,注解也不是特别高深的东西,掌握了自然就明白了。

注解的基本原理

注解本来的意思就是用来做标注用:可以在类、字段变量、方法、接口等位置进行一个特殊的标记,为后续做一些诸如: 代码生成、数据校验、资源整合等工作做铺垫。所以注解就是做标记用的,注解一旦对代码标注完成,后续就可以结合Java强大的反射机制,在运行时动态地获取到注解的标注信息,从而可以执行很多其他逻辑,完成我们想要的自动化工作。所以,反射机制很重要。

注解的使用示例

假设我们现在有个Person类,这个Person类要当做参数传入,我们要对参数进行校验:

1public class Person {
2    private Integer id;
3    private String name;
4    private Integer age;
5    
6    //Getter and Setter
7}

如果没有注解,那么我们就需要写这样一长串的if else校验:

 1public String addPerson(Person person){
 2    if(person == null){
 3        return "参数为空";
 4    }
 5
 6    if(person.getId() == null || "".equals(person.getId())){
 7        return "Person's id is null";
 8    }
 9
10    if(person.getName() == null || "".equals(person.getName())){
11        return "Person's name is null.";
12    }
13    
14    if(person.getName().length() < 3){
15        return "Person's name length must lager 3.";
16    }
17
18    if(person.getAge()  == 0){
19        return "Person's age is null.";
20    }
21
22    if(person.getAge() <= 0 || person.getAge() >= 150){
23        return "Person's age error.";
24    }
25}

所以,可以参考一下如何使用注解来校验这些参数:

 1public class Person {
 2    @NotNull(message = "传入的Id为空值")
 3    @NotEmpty(message = "传入的Id为空字符串")
 4    private String id;
 5
 6    @NotNull(message = "传入的Name为空值")
 7    @NotEmpty(message = "传入的Name为空字符串")
 8    @Length(min = 3, max = 30, message = "姓名长度必须3-30之间")
 9    private String name;
10
11    @NotNull(message = "传入的Age为空值")
12    @Min(value = 0, message = "年龄应该在0-150之间")
13    @Max(value = 150, message = "年龄应该在0-150之间")
14    private Integer age;
15    
16    //Getter and Setter.
17}

@Length注释的实现

本篇文章中,我们就来实现一下@Length这个注解,这个注解学会了,其他注解也都是一样的:

step1.定义注解 @Length

 1import java.lang.annotation.ElementType;
 2import java.lang.annotation.Retention;
 3import java.lang.annotation.RetentionPolicy;
 4import java.lang.annotation.Target;
 5
 6@Target({ElementType.FIELD})
 7@Retention(RetentionPolicy.RUNTIME)
 8public @interface Length{
 9    // 允许的字符串长度最小值
10    int min();
11
12    // 允许的字符串长度最大值
13    int max();
14
15    // 自定义错误提示
16    String errorMsg();
17}

1、注解的定义有点像定义接口interface,但唯一不同的是前面需要加一个@符号

2、注解的成员变量只能使用基本类型、String或者enum枚举,比如int可以,但Integer这种包装类型就不行

3、像上面@Target、@Retention这种加在注解定义上面的注解,我们称为“元注解”, 元注解就是专门用于给注解添加注解的注解,简单理解就是:元注解就是天生就有的注解,可直接用于注解的定义上

4、@Target(xxx)用来说明该自定义注解可以用在什么位置,比如:

  • ElementType. FIELD:说明自定义的注解可以用于类的变量
  • ElementType. METHOD:说明自定义的注解可以于类的方法
  • ElementType. TYPE:说明自定义的注解可以用于类本身、接口或enum类型
  • 其实还有很多,如果记不住的话还是建议现用现查

5、@Retention (xxx)用说明你自定义注解的生命周期,比如:

  • @Retention (RetentionPolicy.RUNTIME):表示注解可以一直保留到运行时,因此可以通过反射获取注解信息
  • @Retention (RetentionPolicy.CLASS):表示注解被编译器编译进class文件,但运行时会忽略
  • @Retention (RetentionPolicy.SOURCE):表示注解仅在源文件中有效, 编译时就会被忽略

所以声明周期从长到短分别为:RUNTIME > CLASS > SOURCE,一般来说,如果需要在运行时去动态获取注解的信息,还是得用RUNTIME,就像本文所用。

step2.获取注解并对其验证

在运行时想获取注解所代包含的信息,该怎么办?我们得用Java的反射相关的知识!下面写了一个验证函数validate(),代码中会逐行用注释去解释想要达到的目的,认真看一下每一行的注释:

 1public class LengthValidator {
 2    public static String validateField(Object object) throws IllegalAccessException {
 3        // 获取字段值
 4        // 对本文来说就是Person的id、name、age三个字段
 5        Field[] fields = object.getClass().getDeclaredFields();
 6
 7        // 逐个字段校验,看看哪个字段标了注解
 8        for (Field field: fields){
 9            // if判断:检查字段上面有没有标注@Length注解
10            if(field.isAnnotationPresent(Length.class)){
11                // 通过反射获取到该字段上标注的@Length的注解的详细信息
12                Length length = field.getAnnotation(Length.class);
13                // 让我们在反射时看到私有变量
14                field.setAccessible(true);
15                // 获取实际字段的值
16                int value = ((String)field.get(object)).length();
17                // 将实际字段的值和注解的标记值进行对比
18                if(value < length.min() || value > length.max()){
19                    return length.errorMsg();
20                }
21            }
22        }
23
24        return null;
25    }
26}

step3.使用自定义注解

此时,Person类只需要加上此注解

 1public class Person {
 2    private String id;
 3
 4    @Length(min = 3, max = 30, errorMsg = "姓名长度必须3-30之间")
 5    private String name;
 6
 7    private Integer age;
 8    
 9    //Getter and Setter
10}

然后使用即可:

 1public class AnnotationTest {
 2    public static void main(String[] args) throws IllegalAccessException {
 3        Person person = new Person();
 4        person.setName("13");
 5        person.setAge(10);
 6        person.setId("001");
 7
 8        String validateField = LengthValidator.validateField(person);
 9        if(validateField == null)
10            System.out.println(person);
11        else
12            System.out.println(validateField);
13    }
14}

mark