注解的原理与实现

3,611 阅读5分钟

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

注解的基本原理

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

注解的使用示例

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

public class Person {
    private Integer id;
    private String name;
    private Integer age;
    
    //Getter and Setter
}

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

public String addPerson(Person person){
    if(person == null){
        return "参数为空";
    }

    if(person.getId() == null || "".equals(person.getId())){
        return "Person's id is null";
    }

    if(person.getName() == null || "".equals(person.getName())){
        return "Person's name is null.";
    }
    
    if(person.getName().length() < 3){
        return "Person's name length must lager 3.";
    }

    if(person.getAge()  == 0){
        return "Person's age is null.";
    }

    if(person.getAge() <= 0 || person.getAge() >= 150){
        return "Person's age error.";
    }
}

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

public class Person {
    @NotNull(message = "传入的Id为空值")
    @NotEmpty(message = "传入的Id为空字符串")
    private String id;

    @NotNull(message = "传入的Name为空值")
    @NotEmpty(message = "传入的Name为空字符串")
    @Length(min = 3, max = 30, message = "姓名长度必须3-30之间")
    private String name;

    @NotNull(message = "传入的Age为空值")
    @Min(value = 0, message = "年龄应该在0-150之间")
    @Max(value = 150, message = "年龄应该在0-150之间")
    private Integer age;
    
    //Getter and Setter.
}

@Length注释的实现

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

step1.定义注解 @Length

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Length{
    // 允许的字符串长度最小值
    int min();

    // 允许的字符串长度最大值
    int max();

    // 自定义错误提示
    String errorMsg();
}

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(),代码中会逐行用注释去解释想要达到的目的,认真看一下每一行的注释:

public class LengthValidator {
    public static String validateField(Object object) throws IllegalAccessException {
        // 获取字段值
        // 对本文来说就是Person的id、name、age三个字段
        Field[] fields = object.getClass().getDeclaredFields();

        // 逐个字段校验,看看哪个字段标了注解
        for (Field field: fields){
            // if判断:检查字段上面有没有标注@Length注解
            if(field.isAnnotationPresent(Length.class)){
                // 通过反射获取到该字段上标注的@Length的注解的详细信息
                Length length = field.getAnnotation(Length.class);
                // 让我们在反射时看到私有变量
                field.setAccessible(true);
                // 获取实际字段的值
                int value = ((String)field.get(object)).length();
                // 将实际字段的值和注解的标记值进行对比
                if(value < length.min() || value > length.max()){
                    return length.errorMsg();
                }
            }
        }

        return null;
    }
}

step3.使用自定义注解

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

public class Person {
    private String id;

    @Length(min = 3, max = 30, errorMsg = "姓名长度必须3-30之间")
    private String name;

    private Integer age;
    
    //Getter and Setter
}

然后使用即可:

public class AnnotationTest {
    public static void main(String[] args) throws IllegalAccessException {
        Person person = new Person();
        person.setName("13");
        person.setAge(10);
        person.setId("001");

        String validateField = LengthValidator.validateField(person);
        if(validateField == null)
            System.out.println(person);
        else
            System.out.println(validateField);
    }
}

mark