一文助你了解java注解到底是怎么回事

以前开发的时候很难理解spring的各种注解,光背背就令人头大,只知道在某些类、某些方法上面加上一个@然后跟个约定俗成的值就实现了对应的功能,至于:

  • 怎么加这个注解就执行对应功能了呢,谁去执行
  • 注解从哪来的
  • 注解到底是个什么玩样儿

想不通,也懒得去想

最近在复习了反射、class对象等内容加深印象之后,回过头来再看注解,有种恍然大悟的感觉,不少底层概念过一遍直接就能读懂,就像多年的老便秘突然通畅了,于是脸也红润了,脚也利索了,能在敲(掉)代(头)码(发)这条路上走的更远了…

简单说下接下来要讲的几点,以便你们有个数:

  • 注解大概分为哪几类
  • 注解是怎么定义的
  • 如何使用自定义注解
  • 编译器是怎么执行注解的

注解的分类

Java内置注解

举例:
@Override:这个注解代表重写,但实际并不需要手动在重写方法上添加,编译器在编译时会自动添加
@Deprecated:标明已经过时的方法或者类
@SuppressWarnnings:关闭一些对方法、类的警告,简单讲,“我知道代码有问题,但你不要说出来
把JDK中主要三个注解单拎出来作为一类是想跟框架提供的注解分开,不容易搞混,不然有人以为注解是spring特有的就尴尬了

第三方框架提供的注解

主流框架Spring中就定义了大量的注解来使得程序编写更加简捷方便,比如**@Bean代表需要把目标类注册到spring的IOC容器中,随用随拿,@Repository用来标记数据访问组件,@Controller标记控制层组件,@Before代表方法执行前执行,@After**代表方法执行后执行等等。
这类也是算自定义注解,只不过它是框架定义的

自定义注解

程序员在实际开发中为了满足业务中某些切面设计要求而设定的注解,只要符合自定义注解规范即可。

元注解

简单讲就是定义注解的注解,就跟描述类的类一样,其他类型的注解只需知道是什么作用,会用就行,但想彻底了解注解就得从元注解入手。要创建一个自定义注解,元注解是必不可少的,下面就简单说下元注解的定义规则,先不写例子,就看下jdk提供的注解**@Override**的源码:
image.png
了解上面注解的分类之后说下注解的实际使用,主要分三步走

①定义注解-》②使用注解-》③解析注解

注解是怎么定义的

首先有一个**@interface规定语法来跟编辑器说这是一个注解,接着还需加上@Target@Retention**这两个元注解,各自的含义解释:

@Target

同一个注解总不能什么地方都能用吧,方法、属性、构造函数还是有区别的,得分一下,target就是指定该注解能被使用的位置(挑一些常用的解释下):

  • 类或接口:ElementType.TYPE
  • 字段:ElementType.FIELD
  • 方法:ElementType.METHOD
  • 构造方法:ElementType.CONSTRUCTOR
  • 方法参数:ElementType.PARAMETER

位置不能乱用,否则编译期就不会通过
image.png

@Target支持设置多个位置,比如想放在类或者方法上,那么可以这样写:
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 如果是单个,则允许省去{}符号

@Retention

定义了注解的生命周期,默认是RetentionPolicy.CLASS:

  • 仅编译期:RetentionPolicy.SOURCE 不会被jvm编译进class文件中
  • 仅class文件:RetentionPolicy.CLASS 在class文件中有效
  • 运行期:RetentionPolicy.RUNTIME 运行时有效

有些人因为不了解class对象所以对编译加载class文件没概念,我这边简单解释下:
一个java文件在编译期会变成一个.class文件,类加载器会加载.class文件,解析到内存中(方法区),方法区里会存放关于这个类的所有信息,后续创建实例对象、获取对象方法成员变量构造函数等都需依据方法区里的这些类信息(反射),同一个类只会有一个class对象,换成图大概是这样:
image.png
注解也是一种类,继承自 java.lang.annotation.Annotation ,所以也会被编译成class文件,就拿上面生命周期最短的RetentionPolicy.SOURCE来说,它是指这类注解在变成class文件之前就被**注解处理器(Annotation Processor)**去掉了,等于说不会被编译到class文件中。
生命周期第二短的RetentionPolicy.Class 会被编译到class文件中,不过在加载后该类型的注解就会被丢弃,而RetentionPolicy.RUNTIME不光会被编译到class文件,在加载之后也会被保留,在运行期间可以反射读取对应的一些方法和变量信息。

所以生命周期范围大小是:
RetentionPolicy.SOURCE(编译) < RetentionPolicy.Class(类加载) < RetentionPolicy.RUNTIME(运行)

在实际应用中,需要程序在运行过程中去解析一个class对象,反射获取变量方法来执行一些操作,所以 RetentionPolicy.RUNTIME是最恰当的(连反射都反射不到,自定义注解拿来干啥?)。

其他的元注解

还有一些其他的元注解我觉得可以一笔带过,因为真的不常用:

  • @Repeatable 自定义注解是否可重复,就是说加这个元注解之后,在同一个方法(打比方)上面可以添加多个相同的自定义注解
  • @Inherited 定义子类是否可继承父类定义的注解,就是说父类定义了一个注解,子类继承父类后也会自动继承该注解
  • @Documented 将注解中的元素包含到Javadoc中去,用来生成javadoc用

注解的属性

说完了注解的基本定义,往下看:
image.png
在实际的开发中,可以看到这些第三方框架定义的注解中有很多参数需要填入,那这些属性值是怎么定义的?
定义属性值的格式:

@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SignCheck {
    String value() default "啦啦啦";
}
复制代码

感觉有点奇怪,为什么定义一个参数后面会跟 () ,这不是方法的格式吗?其实你这样可以理解,String value()只是一个接口中待实现的方法,在实际的使用过程中比如反射获取注解对象信息时

SignCheck signCheck = [某个Class对象].getAnnotation(SignCheck.class);//通过反射获取直接注解对象
复制代码

会在内存中生成一个实现该注解接口的子类对象,这个return看起来就很好理解了

//实际不会产生以下代码,按照上述思路假设
public class SignCheckImpl implements SignCheck{
    public String value(){
        return 给注解赋的value值;
    }
}
复制代码

自定义注解中如果定义另一个属性值叫 value() ,那么在实际使用过程中注解不加这个属性值也能赋予自定义注解变量,但如果属性值定义了多个,就必须一一指明对应:

@SignCheck("lala") //等价于@SignCheck(value = "lala")
public void annotationtest(){
    ...
}
复制代码

注解的属性值支持很多类型,除了String,还有八种基本数据类型,Class、枚举类,甚至是注解都可以。
上面的自定义注解扩充下:

@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SignCheck {

    //基本数据类型
    int iValue();
    double douValue();
    long lonValue();
    float floValue();
    char chValue();
    boolean booValue();
    short shoValue();
    byte byteValue();

    //字符串类型,注意跟char的区别
    String stringValue();

    //注解类
    ExampleAnnoation annocationValue();

    //Class
    Class<?> classValue();

    //枚举类
    WeekEnum enumValue();

    //还有一些数组,下面就举一个例子
    int[] iListValue();

}

enum WeekEnum{
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}

@interface ExampleAnnoation{

}
复制代码

如何使用自定义注解

这个不会陌生,因为平常潜移默化的时候已经在用了,只需要在对的地方(target指定的类/方法/变量等)加上相应的注解和变量就行了,比如上面 SignCheck 注解可以用在方法和类上,那么我们随便找一个方法,在其头部加上注解并声明变量:
image.png
赋值后是这样的:

@SignCheck(iValue = 1, 
           douValue = 1.0d, 
           lonValue = 4124L, 
           floValue = 451f, 
           chValue = 'H', 
           booValue = true, 
           shoValue = 2, 
           byteValue = 120,
           stringValue = "这是字符串",
           annocationValue = @ExampleAnnoation,
           classValue = String.class,//随便一个class对象
           enumValue = WeekEnum.Wednesday,
           iListValue = {1,2,3,4,5})
public void annotationtest(){

}
复制代码

注解是如何被解析的

定义了注解,也在对应的方法上使用了注解,但实际上对加了注解的程序没有任何影响,它仅仅是一个标记而已,因为在运行(RetentionPolicy.RUNTIME)的时候自定义注解才会被JVM读取并执行的,现在只是定义了注解,没有任何代码跟JVM说明这个 @SignCheck 注解会起到什么作用。
所以第一步的定义注解只是个半成品,接下来的才是重头戏,也是萌新最难理解的部分:(又要扯Class对象了,在写反射、多态、动态代理都扯过这个,真的太特么重要了)
Class源码中会发现在2464行处有一个内部私有类 ReflectionData (反射数据)
image.png
里面有 Field 字段类实例、 Method 方法类实例、 Constructor 构造器类实例以及接口类实例,每个属性都是为了描述一个class类而设定的,这些属性可以通过反射机制绕过对象获得类的变量/方法/构造器等
image.png
而在FieldMethodConstructor 等类中都能找到一个叫 getAnnotation() 的方法(反射获取注解信息,实际使用当然不止这一种方法):
image.png
image.png
image.png
image.png
知道了可以通过反射获取方法、变量、类等对应实例的自定义注解信息,那么接下来就好办了:

利用反射获取注解信息

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RunBefore {
    String run_name() default "这是默认的值";
}
复制代码
public class AnnocationTest {

    public static void main(String[] args) throws NoSuchMethodException {
        Class<AnnocationTest> annocationTestClass = AnnocationTest.class;
        //反射获取testAnnocation方法 这里只举例Method类
        Method method = annocationTestClass.getMethod("testAnnocation");

        //判断是否存在某个注解对象
        boolean isExist = method.isAnnotationPresent(RunBefore.class);
        System.out.println("是否存在目标注解:" + isExist);

        //反射获取目标注解对象
        RunBefore runBefore = method.getAnnotation(RunBefore.class);
        System.out.println("注解对象属性:" + runBefore.run_name());
    }

    @RunBefore(run_name = "测试注解运行")
    public void testAnnocation() {

    }
}
复制代码

输出:

是否存在目标注解:true
注解对象属性:测试注解运行

从上面的语句你会有所发现:**所有的自定义注解类都继承 Annotation **,为什么这么说?看下面图中的源码:
image.png

//反射获取目标注解对象(用了自定义注解RunBefore去接收)
RunBefore runBefore = method.getAnnotation(RunBefore.class);
复制代码

getAnnotation方法返回的参数要求继承Annotation,而实际测试代码中显示可以直接用注解类 RunBefore 实例接收出参,这是不是在说明注解类都继承Annotation类?进一步说明注解也是一种类,拥有类的特性

验证反射

为证明设置@Retention为RetentionPolicy.RUNTIME是为了能够让注解类加载到内存并且在运行期间被jvm加载和反射获取到对应信息,我们把它改成 @Retention(RetentionPolicy.CLASS) ,看看会出现什么问题:
image.png
程序员运行过程中未能通过反射找到注解实例,说明生命周期RetentionPolicy.CLASS不会把注解类加载运行内存中,只是在编译期中编译到class字节码中而已。

多注解的获取

上面的例子只是举了反射方法类 Method 上的注解,其他的诸如Field、Class、Constructor操作API都类似,可以自己敲一遍,之所以拿方法类来举例是因为觉得方法类反射会麻烦点,比如testAnnocation方法改成这样:

public void testAnnocation(@NotNull @Range(minlength = 5,maxlength = 10) String name
    ,@CanBeNull int age) {
}
复制代码

一个方法中包多个参数,一个参数包含多个注解(构造器也会这种情况),那么就需要定义一个二维数组来接收这些反射数据,有一个 getParameterAnnotations 方法,如下图:
image.png
那应该这么接收了:

Class<AnnocationTest> annocationTestClass = AnnocationTest.class;
//反射获取testAnnocation方法
Method method = annocationTestClass.getMethod("testAnnocation", String.class, int.class);
// 获取所有参数的Annotation:
Annotation[][] annos = method.getParameterAnnotations();
for (Annotation[] anno : annos) {
    for (Annotation annotation : anno) {
        if (annotation instanceof StringRange) {
            //StringRange注解
            System.out.println("找到了StringRange注解");
            System.out.println("minlength:" + ((StringRange) annotation).minlength());
            System.out.println("maxlength:" + ((StringRange) annotation).maxlength());
        }
        if (annotation instanceof NotNull) {
            //NotNull注解
            System.out.println("找到了NotNull注解");
        }
        if (annotation instanceof CanBeNull) {
            //CanBeNull注解
            System.out.println("找到了CanBeNull注解");
        }
    }
}
复制代码

双重遍历,先遍历方法method中的param参数,然后再遍历params参数中的各个注解,根据返回的类型判断来找到对应的注解和输出对应的值:

找到NotNull注解
找到StringRange注解
minlength:5
maxlength:10
找到CanBeNull注解

来,上手,码一个自定义注解

上面讲的都是规范,最后还是要自己手动码会比较清楚,假设我们定义一个 @AgeRange 这么个注解,用来限制方法入参:

@Target({ElementType.PARAMETER}) //注意这个ElementType
@Retention(RetentionPolicy.RUNTIME)
public @interface AgeRange {
    int minValue() default 1;//最小年龄1
    int maxValue() default 99;//最大年龄99
}
复制代码

然后在 AnnocationTest 类中有这么一个方法使用该注解:

public void outputAge(@AgeRange int age) {
    System.out.println("执行处理年龄" + age + "正常的业务逻辑:");
}
复制代码

写一个自定义方法来读取注解和处理:

public void checkAnnocation(int age) throws Exception {
    Class<AnnocationTest> annocationTestClass = AnnocationTest.class;
    //反射获取testAnnocation方法
    Method method = annocationTestClass.getMethod("outputAge", int.class);

    //反射获取目标注解对象
    Annotation[][] annotations = method.getParameterAnnotations();

    for (Annotation[] anno : annotations) {
        for (Annotation annotation : anno) {
            if (annotation instanceof AgeRange) {
                System.out.println("找到了AgeRange注解... ...开始解析");
                AgeRange ageRange = (AgeRange)annotation;
                int minValue = ageRange.minValue();
                int maxValue = ageRange.maxValue();
                if (age < ((AgeRange) annotation).minValue()
                    || age > ((AgeRange) annotation).maxValue()) {
                    System.out.println("不符合规范的年龄,应在" + minValue + "和" + maxValue + "之间,当前值为:" + age);
                    //这边可以定义一个自定义异常,全局捕捉,统一处理和返回
                    throw new MyException(ExceptionEntity.UNVAILD_METHOD_PARAMS);
                }
            }

            //这里还可以扩充其他注解
        }
    }
}
复制代码

所以如果在调用 outputAge 方法时jvm执行 checkAnnocation 校验方法就可以得到下面这个结果:
image.png
红框框起来的是执行校验这一步,相信看到这里你会有两点疑惑,这也是网上绝大多数的千篇一律讲解注解知识点时都漏掉的:

  • checkAnnocation 方法为什么还需要传age参,难道不能通过反射来动态获得方法被@AgeRange修饰的age值么
  • 为什么还需要显式执行下反射注解方法 checkAnnocation 来校验,添加了注解为什么不是系统自动执行

第一个问题:其实很好理解,反射拿到method参数没什么问题,但是这些参数信息(比如类型、名称)都是静态的,方法的参数是出现在方法被调用的时候,不过可以通过做一个动态代理来获取这些参数值,比如JDK动态代理里面实现 InvocationHandler 接口中有一个核心方法 invoke :

/**
* dynamic_proxy:代理类代理的真实代理对象com.sun.dynamic_proxy.$Proxy0
* method:我们所要调用某个对象真实的方法的Method对象
* args:指代代理对象方法传递的参数
*/
public Object invoke(Object proxy, Method method, Object[] args) {
		...
}
复制代码

Object[] args就是代理对象传递过来的方法参数,我们可以对这个args做做文章,思路大概就是创建一个代理对象,这个对象包含AnnocationTest目标对象的全部方法(包括参数),并且在invoke方法里对拿到代理对象的参数值进行自定义注解校验,遇到不符合条件直接抛出异常就行了。不懂JDK动态代理的,看我这篇文章就行了:

全文7000余字,由浅到深详细讲解JDK动态代理

第二个问题:自定义注解为什么没有自动执行,还需要我们手动写一个反射方法去获取,像spring框架上的注解没见得有这种代码啊?上面已经说了,注解只是对于java代码来讲只是一种标签,没有任何效力,只有在解析并执行针对于该注解的自定义方法时才能体现自定义注解的实际意义。

所以注解的意义是执行,不是定义,定义对于程序来讲只是一个标签。
**
像在spring项目中,这种执行过程可以被放在拦截器、放在aop中,由spring容器找到这些自定义注解并自动执行,那spring是如何找到这些自定义注解,如何解析并执行这些注解,等我下次有空再讲讲吧,估计还要讲源码,而且有些概念我还没彻底理清,注解这块内容就到这里。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享