Java 注解

564 阅读8分钟

注解是什么?

  • 还记得Class是什么吗?
    • Class是Java类的说明书
    • 你(通过反射)或者JVM阅读该说明书,创建类的实例
  • 注解就是说明书中的⼀小段信息/文本/标记
    • 可以携带参数
    • 可以在运行时被阅读

java.lang.annotation.Annotation接口中有这么一句话,用来描述『注解』。

The common interface extended by all annotation types
所有的注解类型都继承自这个普通的接口(Annotation)

我们来看一个JDK内置的注解@override

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

其实它的本质就是:

public interface Override extends Annotation{
}

没错,注解的本质就是继承了Annotation接口的接口

作用

如果说注释是写给人看的,那么注解就是写给程序看的,它更像一个标签,贴在一个类,一个方法或者一个字段上,它的目的是为当前读取该注解的程序提供判断依据。比如程序只要读到了加了@Test注解的方法,就知道该方法是待测试的方法。

注解仅仅是一段信息,它自己无法工作,换句话说,没有东西处理它们,注解没有任何卵用。

Annotaion的结构

元注解

『元注解』是用于修饰注解的注解,通常用在注解的定义上,例如:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

这是 @Override 注解的定义,你可以看到其中的 @Target@Retention 两个注解就是我们所谓的『元注解』,『元注解』一般用于指定某个注解生命周期以及作用目标等信息。

JAVA 中有以下几个『元注解』:

  • @Target:注解的作用目标
  • @Retention:注解的生命周期
  • @Documented:注解是否应当被包含在 JavaDoc 文档中
  • @Inherited:是否允许子类继承该注解

@Target

@Target 用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法的?修饰类的?还是用来修饰字段属性的。它的基本定义如下

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

我们可以通过ElementType枚举类来指定@Targetvalue属性,注意 @Target可以设置多个值

public enum ElementType {
    TYPE,               /* 类、接口(包括注释类型)或枚举声明  */
    FIELD,              /* 字段声明(包括枚举常量)  */
    METHOD,             /* 方法声明  */
    PARAMETER,          /* 参数声明  */
    CONSTRUCTOR,        /* 构造方法声明  */
    LOCAL_VARIABLE,     /* 局部变量声明  */
    ANNOTATION_TYPE,    /* 注释类型声明  */
    PACKAGE             /* 包声明  */
}
@Target({
        ElementType.TYPE,
        ElementType.CONSTRUCTOR,
        ElementType.FIELD,
        ElementType.METHOD,
        ElementType.PARAMETER,
        ElementType.TYPE_PARAMETER
})
public @interface 洗涤手段 {
}

@Retention

@Retention 用于指明当前注解的生命周期,它的基本定义如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}

我们可以通过RetentionPolicy这个枚举类来给@Retention来设置值,

public enum RetentionPolicy {
    SOURCE,            /* Annotation信息仅存在于编译器处理期间, 不会记录在.class字节码文件中 */
    CLASS,             /* 注释将被记录在由编译器的.class类文件,但不必由虚拟机在运行时被保留。 这是默认行为  */
    RUNTIME            /* 注释将被记录在由编译器的.class类文件,并在运行时由虚拟机保留的,所以他们可能会反射性地读取 */
}

@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}

@Documented

@Documented注解修饰的注解,当我们执行 JavaDoc 文档打包时会被保存进 doc 文档,反之将在打包时丢弃。

@Inherited

@Inherited注解修饰的注解是具有可继承性的,也就说我们的注解修饰了一个类,而该类的子类将自动继承父类的该注解。

Jdk内置注解

@Override

它没有任何的属性,所以并不能存储任何其他信息。它只能作用于方法之上,编译结束后将被丢弃。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

所以你看,它就是一种典型的『标记式注解』,仅被编译器可知,编译器在对 java 文件进行编译成字节码的过程中,一旦检测到某个方法上被修饰了该注解,就会去匹对父类中是否具有一个同样方法签名的函数,如果不是,自然不能通过编译。

@Deprecated

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

依然是一种『标记式注解』,永久存在,可以修饰所有的类型,作用是,标记当前的类或者方法或者字段等已经不再被推荐使用了,可能下一次的 JDK 版本就会删除。

当然,编译器并不会强制要求你做什么,只是告诉你 JDK 已经不再推荐使用当前的方法或者类了,建议你使用某个替代者

@SuppressWarnings

@SuppressWarnings 主要用来压制 java 的警告,它的基本定义如下:

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

例如:

@SuppressWarnings("unchecked")

注解的属性

注解属性支持哪些数据类型

  • 所有基本类型(int,float,boolean,byte,double,char,long,short)
  • String
  • Class
  • enum
  • Annotation
  • 上述类型的数组
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Reference{
    boolean next() default false;
}
public @interface AnnotationElementDemo {
    //枚举类型
    enum Status {FIXED,NORMAL};
    //声明枚举
    Status status() default Status.FIXED;
    //布尔类型
    boolean showSupport() default false;
    //String类型
    String name()default "";
    //class类型
    Class<?> testCase() default Void.class;
    //注解嵌套
    Reference reference() default @Reference(next=true);
    //数组类型
    long[] value();
}

注解属性默认值的限制

编译器对元素的默认值有些过分挑剔。首先,元素不能有不确定的值。也就是说,元素必须要么具有默认值,要么在使用注解时提供元素的值。其次,对于非基本类型的元素,无论是在源代码中声明,还是在注解接口中定义默认值,都不能以null作为值,这就是限制,没有什么利用可言,但造成一个元素的存在或缺失状态,因为每个注解的声明中,所有的元素都存在,并且都具有相应的值,为了绕开这个限制,只能定义一些特殊的值,例如空字符串或负数,表示某个元素不存在

快捷方式

所谓的快捷方式就是注解中定义了名为value的元素,并且在使用该注解时,如果该元素是唯一需要赋值的一个元素,那么此时无需使用key=value的语法,而只需在括号内给出value元素所需的值即可。这可以应用于任何合法类型的元素,记住,这限制了元素名必须为value,简单案例如下:

//定义注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface IntegerVaule{
    int value() default 0;
    String name() default "";
}
//使用注解
public class QuicklyWay {
    //当只想给value赋值时,可以使用以下快捷方式
    @IntegerVaule(20)
    public int age;

    //当name也需要赋值时必须采用key=value的方式赋值
    @IntegerVaule(value = 10000,name = "MONEY")
    public int money;

}

处理注解的方式

  • 编译器直接扫描
  • 运行期反射

编译期处理

典型的就是注解 @Override,一旦编译器检测到某个方法被修饰了 @Override 注解,编译器就会检查当前方法的方法签名是否真正重写了父类的某个方法,也就是比较父类中是否具有一个同样的方法签名。 这一种情况只适用于那些编译器已经熟知的注解类,比如 JDK 内置的几个注解,而你自定义的注解,编译器是不知道你这个注解的作用的,当然也不知道该如何处理,往往只是会根据该注解的作用范围来选择是否编译进字节码文件,仅此而已

运行期反射

上文已经说过,注解就像一个标签,是贴在程序代码上供另一个程序读取的。所以三者关系是:

要牢记,只要用到注解,必然有三角关系: 定义注解, 使用注解,读取注解。 仅仅完成前两步,是没什么卵用的,就好比你写了一本武林秘籍,却没有人去学它,那还不如一把菜刀。

接下来我们要写一个程序来读取自定义注解,其中用到了ByteBuddy这个库

定义注解

使用注解

读取注解

运行结果

定位注解处理类

Class类的getAnnotation方法添加一个条件断点 image.png

总结

  • 注解就像标签,是程序判断执行的依据。比如,程序读到@Test就知道这个方法是待测试方法,而@Before的方法要在测试方法之前执行
  • 注解需要三要素:定义、使用、读取并执行
  • 注解分为自定义注解、JDK内置注解和第三方注解(框架)。自定义注解一般要我们自己定义、使用、并写程序读取,而JDK内置注解和第三方注解我们只要使用,定义和读取都交给它们
  • 大多数情况下,三角关系中我们只负责使用注解,无需定义和执行,框架会将注解类和读取注解的程序隐藏起来,除非阅读源码,否则根本看不到。平时见不到定义和读取的过程,光顾着使用注解,久而久之很多人就忘了注解如何起作用了!