注解Annotation原理详解及其应用示例

一、什么是注解

  注解也叫元数据,例如我们常见的@Override和@Deprecated,注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。

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

The common interface extended by all annotation types

所有的注解类型都继承自这个普通的接口(Annotation)

  看一个 JDK 内置注解的定义:

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

  这是注解 @Override 的定义,其实它本质上就是:

public interface Override extends Annotation{}

  注解的本质就是一个继承了 Annotation 接口的接口。一个注解准确意义上来说,只不过是一种特殊的注释而已,如果没有解析它的代码,它可能连注释都不如。

  而解析一个类或者方法的注解往往有两种形式:一种是编译期直接的扫描,一种是运行期反射。

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

  运行期反射就是在运行时,根据反射机制获取类、方法、属性等元素,根据注解标记进行代码逻辑处理,依赖解析注解的代码。

二、注解的作用

  在看注解的用途之前,有必要简单的介绍下XML和注解区别,

注解:是一种分散式的元数据,与源代码紧绑定。

xml:是一种集中式的元数据,与源代码无绑定

  注解的主要用途:

  • 生成文档,通过代码里标识的元数据生成javadoc文档。
  • 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
  • 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
  • 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。

三、注解分类

  常用的注解可以分为三类:

  • JDK内置注解:Java自带的标准注解,包括@Override(标明重写某个方法)、@Deprecated(标明某个类或方法过时)和@SuppressWarnings(标明要忽略的警告),使用这些注解后编译器就会进行检查。
  • 元注解:元注解是用于定义注解的注解,包括@Retention(标明注解被保留的阶段)、@Target(标明注解使用的范围)、@Inherited(标明注解可继承)、@Documented(标明是否生成javadoc文档)。
  • 自定义注解:可以根据自己的需求定义注解。

四、元注解

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

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

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

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

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

  1、Target

  @Target 用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法的?修饰类的?还是用来修饰字段属性的。@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 传值:

@Target(value = {ElementType.FIELD})
// 若定义注解时,注解名为value(),那么在传参时可以省略注解名,如本例 ElementType[] value()
@Target( {ElementType.FIELD})
// 若定义注解时定义的是数组类型,那么如果入参只有一个时,可以省略{},如本例 ElementType[] value():
@Target( ElementType.FIELD)

  被这个 @Target 注解修饰的注解将只能作用在成员字段上,不能用于修饰方法或者类。其中,ElementType 是一个枚举类型,有以下一些值:

  • ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
  • ElementType.FIELD:允许作用在属性字段上
  • ElementType.METHOD:允许作用在方法上
  • ElementType.PARAMETER:允许作用在方法参数上
  • ElementType.CONSTRUCTOR:允许作用在构造器上
  • ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
  • ElementType.ANNOTATION_TYPE:允许作用在注解上
  • ElementType.PACKAGE:允许作用在包上

  如源码所示(最新的源码多了 TYPE_PARMETER、TYPE_USE、MODULE三种类型):

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE,

    /**
     * Module declaration.
     *
     * @since 9
     */
    MODULE
}

  2、Retention

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

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

  同样的,它也有一个 value 属性:

RetentionPolicy value();

  这里的 RetentionPolicy 依然是一个枚举类型,它有以下几个枚举值可取:

  • RetentionPolicy.SOURCE:当前注解编译期可见,不会写入 class 文件
  • RetentionPolicy.CLASS:类加载阶段丢弃,会写入 class 文件
  • RetentionPolicy.RUNTIME:永久保存,可以反射获取

  如源码所示:

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

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

  • SOURCE:只能在编译期可见,编译后会被丢弃。
  • CLASS:会被编译器编译进 class 文件中,无论是类或是方法,乃至字段,他们都是有属性表的,而 JAVA 虚拟机也定义了几种注解属性表用于存储注解信息,但是这种可见性不能带到方法区,类加载时会予以丢弃。
  • RUNTIME:是永久存在的可见性。

  3、Documented

  @Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。

  4、Inherited

  @Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是会被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。

  注意:@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。

  当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。

  如本示例中,TestInherited自定义注解类中增加@Inherited注解,如下:

/**
 * 定义一个可以注解在Class,interface,enum上的注解
 * 增加了@Inherited注解代表允许继承
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface TestInherited {
    /**
     * 定义注解的一个元素 并给定默认值
     * @return
     */
    String value() default "我是定义在类接口枚举类上的注解元素value的默认值";
}

  定义父类测试类InheritedFatherTest:

@TestInherited
public class InheritedFatherTest {

    public static void main(String[] args) {
        // 获取类上的注解MyAnTargetType
        TestInherited t = InheritedFatherTest.class.getAnnotation(TestInherited.class);
        System.out.println("类上的注解值 === " + t.value());
    }
}

  父类运行结果:

类上的注解值 === 我是定义在类接口枚举类上的注解元素value的默认值
Process finished with exit code 0

  定义子类测试类InheritedChildTest继承父类InheritedFatherTest:

public class InheritedChildTest extends InheritedFatherTest {

    public static void main(String[] args) {
        // 获取类上的注解MyAnTargetType
        TestInherited t = InheritedChildTest.class.getAnnotation(TestInherited.class);
        System.out.println("类上的注解值 === "+t.value());
    }
}

  子类运行结果:

类上的注解值 === 我是定义在类接口枚举类上的注解元素value的默认值
Process finished with exit code 0

  如果TestInherited自定义注解类去掉注解@Inherited,然后运行子类,结果如下:

Exception in thread "main" java.lang.NullPointerException
    at annotation.InheritedChildTest.main(InheritedChildTest.java:26)
Process finished with exit code 1

五、JDK内置注解

  1、Override

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

  它没有任何的属性,所以并不能存储任何其他信息。它只能作用于方法之上,编译结束后将被丢弃。是一种典型的『标记式注解』,仅被编译器可知,编译器在对 java 文件进行编译成字节码的过程中,一旦检测到某个方法上被修饰了该注解,就会去匹对父类中是否具有一个同样方法签名的函数,如果不是,自然不能通过编译。

  2、Deprecated

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
    /**
     * Returns the version in which the annotated element became deprecated.
     * The version string is in the same format and namespace as the value of
     * the {@code @since} javadoc tag. The default value is the empty
     * string.
     *
     * @return the version string
     * @since 9
     */
    String since() default "";

    /**
     * Indicates whether the annotated element is subject to removal in a
     * future version. The default value is {@code false}.
     *
     * @return whether the element is subject to removal
     * @since 9
     */
    boolean forRemoval() default false;
}

  依然是一种『标记式注解』,永久存在,可以修饰所有的类型,作用是,标记当前的类或者方法或者字段等已经不再被推荐使用了,可能下一次的 JDK 版本就会删除。当然,编译器并不会强制要求你做什么,只是告诉你 JDK 已经不再推荐使用当前的方法或者类了,建议你使用某个替代者。

  3、SuppressWarnings

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

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@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();
}

  它有一个 value 属性需要你主动的传值,这个 value 代表一个什么意思呢,这个 value 代表的就是需要被压制的警告类型。例如:

public static void main(String[] args) {
    Date date = new Date(2018, 7, 11);
}

  这么一段代码,程序启动时编译器会报一个警告。

Warning:(8, 21) java: java.util.Date 中的 Date(int,int,int) 已过时

  而如果我们不希望程序启动时,编译器检查代码中过时的方法,就可以使用 @SuppressWarnings 注解并给它的 value 属性传入一个参数值来压制编译器的检查。

@SuppressWarning("deprecated")
public static void main(String[] args) {
    Date date = new Date(2018, 7, 11);
}

  这样你就会发现,编译器不再检查 main 方法下是否有过时的方法调用,也就压制了编译器对于这种警告的检查。当然,JAVA 中还有很多的警告类型(比如:unchecked、unused等),他们都会对应一个字符串,通过设置 value 属性的值即可压制对于这一类警告类型的检查。注:@SuppressWarning("all")可以屏蔽所有警告。

六、自定义注解

  1、注解定义

  自定义注解比较简单,通过类似以下的语法即可自定义一个注解。

public @interface SelfAnnotation {}

  自定义注解可以添加元素,注解支持以下元素类型(可以通过default设置默认值):

  • 8种基本数据类型。 如: int ,char ,boolean , 但 不能是包装类型!
  • java.lang.String
  • java.lang.Class
  • java.lang.enum
  • java.lang.Annotation
  • 以上5种类型的一维数组类型: int[],String[],Class[] 等等。
public @interface SelfAnnotation {

    int age() default 18;
    String name() default "我是定义在类接口枚举类上的注解元素value的默认值";
    Class clazz();
    String[] strs();
}

  当然,自定义注解的时候也可以选择性的使用元注解进行修饰,这样你可以更加具体的指定你的注解的生命周期、作用范围等信息。

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

    int age() default 18;
    String name() default "我是定义在类接口枚举类上的注解元素value的默认值";
    Class clazz();
    String[] strs();
}

  2、使用注解

  使用时如示例:

public class DemoClass {

    public void method1(){
        System.out.print("this is method1 ----- .");
    }

    @SelfAnnotation(clazz=DemoClass.class, strs={"aaa","bbb"})
    public void method2(){
        System.out.print("this is method2 ----- .");
    }
}

  3、通过反射获取注解

  前面说过,注解只不过是一种特殊的注释而已,需要有解析它的代码,在运行期探究和使用编译期的内容(编译期配置的注解),要用到Java中的灵魂技术——反射。

  如果我们要获得的注解是配置在方法上的,那么我们要从Method对象上获取;如果是配置在属性上,就需要从该属性对应的Field对象上去获取,如果是配置在类型上,需要从Class对象上去获取。注解在谁身上,就从谁身上去获取!

  • 方法1:<T extends Annotation> T getAnnotation(Class<T> annotationClass): 返回改程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。之后再调用该注解的注解类型元素方法就可以获得配置时的值数据。
  • 方法2:Annotation[] getAnnotations(): 返回该程序元素上存在的所有注解,它会返回给我们一个注解数组,需要注意的是该数组的类型是Annotation类型,这个Annotation是一个来自于java.lang.annotation包的接口。。
  • 方法3:boolean isAnnotationPresent(Class<?extends Annotation> annotationClass): 判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false.
  • 方法4:Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释。(如果没有注释直接存在于此元素上,则返回长度为零的一个数组。)该方法的调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响。
  • 方法5:Annotation getDeclaredAnnotation():返回直接存在于此元素上的指定注解。

  如示例:

public class TestClass {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, InvocationTargetException {
        //得到字节码对象
        Class clazz = Demo.class;
        //
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            if (method.isAnnotationPresent(SelfAnnotation.class)) {
                method.invoke(clazz.newInstance(), null);
            }
        }

        System.out.println();

        for (Method method : methods) {
            SelfAnnotation selfAnnotation = method.getAnnotation(SelfAnnotation.class);
            if (selfAnnotation != null) {
                System.out.println(clazz.getName() + "上使用了属性注解:" + method.getName() + ",注解的值为: " + selfAnnotation.name());
                System.out.println(clazz.getName() + "上使用了属性注解:" + method.getName() + ",注解的值为: " + selfAnnotation.age());
            }
        }
    }
}

  输出结果如下:

this is method2 ----- .
annotation.selfannotation.Demo上使用了属性注解:method2,注解的值为: 我是定义在类接口枚举类上的注解元素value的默认值
annotation.selfannotation.Demo上使用了属性注解:method2,注解的值为: 18

Process finished with exit code 0

七、注解反射原理

  注解本质上是继承了 Annotation 接口的接口,而当你通过反射,也就是我们这里的 getAnnotation 方法去获取一个注解类实例的时候,其实 JDK 是通过动态代理机制生成一个实现我们注解(接口)的代理类。可以通过设置一个虚拟机启动参数,用于捕获 JDK 动态代理类。

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

  代理类实现SelfAnnotation注解接口并重写其所有方法,包括SelfAnnotation注解中元素值(age()、name()等)的方法以及SelfAnnotation接口从 Annotation 接口继承而来的方法。而这个关键的 InvocationHandler 实例是谁:AnnotationInvocationHandler 是 JAVA 中专门用于处理注解的 Handler。

  总结如下:

  首先,我们通过键值对的形式可以为注解属性赋值,像这样:@SelfAnnotation(clazz=DemoClass.class, strs={"aaa","bbb"})

  接着,你用注解修饰某个元素,编译器将在编译期扫描每个类或者方法上的注解,会做一个基本的检查,你的这个注解是否允许作用在当前位置,最后会将注解信息写入元素的属性表。

  然后,当你进行反射的时候,虚拟机将所有生命周期在 RUNTIME 的注解取出来放到一个 map 中,并创建一个 AnnotationInvocationHandler 实例,把这个 map 传递给它。

  最后,虚拟机将采用 JDK 动态代理机制生成一个目标注解的代理类,并初始化好处理器

  那么这样,一个注解的实例就创建出来了,它本质上就是一个代理类。一句话概括就是,通过方法名返回注解属性值

原文地址:https://www.cnblogs.com/jing99/p/11729101.html