Java Annotaions (注解)的本质和实现原理(上)

引言

曾几何时,XML 一直是 Java 各大框架配置元数据(meta data) 的主要途径。但作为一种集中式的元数据管理工具,配置项与作用代码距离太过 “遥远”,非常不利于代码的维护和调试。再加上 XML 本身复杂的语法结构,往往令码农们大感头疼。一种与作用代码耦合在一起的元数据配置方式呼之欲出。于是 注解 (Annotations)就在 JDK 5 中正式出现在开发者的视线之中了。

日常使用 Spring Boot 的同学对于注解的使用肯定再熟悉不过了。但大家有没有想过这些注解,例如 @Configuration @Component @Bean 等等之类的,其本质到底是什么,又是怎样发挥自己特定的作用呢?

注解的本质

oracle 官网对注解的定义为:

Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.

注解是元数据的一种形式,它提供有关程序的数据,该数据不属于程序本身。 注解对其注释的代码操作没有直接影响。

从这个定义里我们可以看出,首先注解携带的是元数据,其次,它可能会引起一些和元数据相关的操作,但不会对被注释的代码逻辑产生影响

而在JDK的Annotation接口中有一行注释如此写到:

/**
 * The common interface extended by all annotation types. 
 * ...
 */
public interface Annotation {...}
复制代码

这说明其他注解都扩展自 Annotation 这个接口,也就是说注解的本质就是一个接口。

以 Spring Boot 中的一个注解为例:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
    String value() default "";
}
复制代码

它实际上相当于:

public interface Component extends Annotation{...}
复制代码

@interface 可以看成是一个语法糖。

注解的要素

我们依然来看 @Component 这个例子:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
    String value() default "";
}
复制代码

在注解定义上有几个注解@Target, @Retention, @Documented,被称为 元注解

所谓元注解就是说明注解的注解,这就好比 Python 中的元类。

Java 中的元注解共有以下几个:

@Target

顾名思义,这个注解标识了被修饰注解的作用对象。我们看看它的源码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}
复制代码

可以看到,这个注解的 value 值是一个数组,这也就意味着注解的作用对象可以有多个。 其取值范围都在ElementType这个枚举之中:

public enum ElementType {
    /** 类、接口、枚举定义 */
    TYPE,
    /** 字段,包括枚举值 */
    FIELD,
    /** 方法 */
    METHOD,
    /** 参数 */
    PARAMETER,
    /** 构造方法 */
    CONSTRUCTOR,
    /** 局部变量 */
    LOCAL_VARIABLE,
    /** 元注解 */
    ANNOTATION_TYPE,
    /** 包定义 */
    PACKAGE...
}
复制代码

不同的值代表被注解可修饰的范围,例如TYPE只能修饰类、接口和枚举定义。这其中有个很特殊的值叫做 ANNOTATION_TYPE, 是专门表示元注解的。

在回过头来看 @Component 这个例子, Target 取值为 TYPE。熟悉 Spring Boot 的同学也一定知道,@Component 确实是不能放到方法或者属性前面的。

@Retention

@Retention 注解指定了被修饰的注解的生命周期。定义如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}
复制代码

可以看到这个注解带一个 RetentionPolicy 的枚举值:

public enum RetentionPolicy {
    SOURCE,
    CLASS,
    RUNTIME
}
复制代码
  • SOURCE 表示注解编译时可见,编译完后就被丢弃。这种注解一般用于在编译器做一些事情;
  • CLASS 表示在编译完后写入 class 文件,但在类加载后被丢弃。这种注解一般用于在类加载阶段做一些事情;
  • RUNTIME 则表示注解会一直起作用。

@Documented

这个注解比较简单,表示是否添加到 java doc 中。

@Inherited

这个也比较简单,表示注解是否被继承。这个注解不是很常用。

注意:元注解只在定义注解时被使用!

注解的架构

从上面的元注解可以了解到,一个注解可以关联多个 ElementType,但只能有一个 RetentionPolicy:

Java 内置注解

Java 中有三个常用的内置注解,其实相信大家都用过或者见过。不过在了解了注解的真实面貌以后,不妨重新认识一下吧!

@Override

它的定义为:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
复制代码

可见这个注解没有任何取值,只能修饰方法,而且RetentionPolicy 为 SOURCE,说明这是一个仅在编译阶段起作用的注解。

它的真实作用想必大家一定知道,就是在编译阶段,如果一个类的方法被 @Override 修饰,编译器会在其父类中查找是否有同签名函数,如果没有则编译报错。可见这确实是一个除了在编译阶段就没什么用的注解。

@Deprecated

它的定义为:

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

这个注解也没有任何取值,能修饰所有的类型,永久存在。这个注解的作用是,告诉使用者被修饰的代码不推荐使用了,可能会在下一个软件版本中移除。这个注解仅仅起到一个通知机制,如果代码调用了被@Deprecated 修饰的代码,编译器在编译时输出一个编译告警。

@SuppressWarnings

它的定义为:

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    /**
     * The set of warnings that are to be suppressed by the compiler in the
     * annotated element.  Duplicate names are permitted.  The second and
     * successive occurrences of a name are ignored.  The presence of
     * unrecognized warning names is <i>not</i> an error: Compilers must
     * ignore any warning names they do not recognize.  They are, however,
     * free to emit a warning if an annotation contains an unrecognized
     * warning name.
     *
     * <p> The string {@code "unchecked"} is used to suppress
     * unchecked warnings. Compiler vendors should document the
     * additional warning names they support in conjunction with this
     * annotation type. They are encouraged to cooperate to ensure
     * that the same names work across multiple compilers.
     * @return the set of warnings to be suppressed
     */
    String[] value();
}
复制代码

这个注解有一个字符串数组的值,需要我们使用注解的时候传递。可以在类型、属性、方法、参数、构造函数和局部变量前使用,声明周期是编译期。

这个注解的主要作用是压制编译告警的。

例如

public static void main(String[] args) {
    Date date = new Date(2020, 5, 22);
}
复制代码

我们可以看到,Date 的这个构造函数是被@Deprecated 修饰的:

@Deprecated
public Date(int year, int month, int date) {
    this(year, month, date, 0, 0, 0);
}
复制代码

所以上面的代码在编译时会报一个Warning

java: java.util.Date 中的 Date(int,int,int) 已过时
复制代码

为了不让编译器输出这个 Warning, 就需要在上述的 main 方法前面增加一个 @SuppressWarnings 注解:

@SuppressWarning(value = "deprecated")
public static void main(String[] args) {
    Date date = new Date(2020, 5, 22);
}
复制代码

注解输入的字符串deprecated 表明让编译器忽略 @Deprecated注解引发的编译告警。

总结和预告

本篇主要介绍了 Java 注解的本质、要素以及元注解、三种 Java 内置注解。

下篇我们将站在JVM的角度深入分析注解的功能实现原理,并手写一个自己的注解。

猜你喜欢

转载自juejin.im/post/5eca251f518825431d1261e2