Java注解简介篇

摘要

本文详细介绍java注解是什么,如何声明java注解,如何解析java注解。最后介绍JDK提供的几大基本注解,使用这些基本注解可自定义用户注解。

一、注解是什么

1)是元数据

元数据被定义为:描述数据的数据,对数据及信息资源的描述性信息。

具体到Java语言,类型,方法,属性,参数等程序元素是java编程中必不可少的数据。 JDK5开始java增加了对元数据的支持——注解(Annotation),用以描述以上的程序元素。

2)只是元数据

java注解一点也不神秘,它仅仅是修饰程序元素(类,方法,成员变量,包,构造器)的特殊标记。

正如我们肉眼所见那样,作为元数据,这个特殊标记仅仅对程序元素进行进一步说明;注解本身并不会影响程序逻辑,增加,删除,修改程序元素上的java注解,代码都始终如一的执行。

3)解析元数据

注解已经被Spring大量应用,大有化腐朽为神奇的韵味,通过配置一些简单的注解,实现神奇的复杂功能。既然注解只是元数据,对程序不产生影响,那么Spring注解是如何影响程序的呢?

事实上,所有的java注解,都配套的解析工具。这些解析工具通过读取程序元素上的注解,按照一定的规则对程序施加影响,这类工具统称APT(Annotatoin Processing Tool)。不管是JDK原生注解,第三方如spring注解,每一个注解必定可以找到解析它的代码。

二、声明java注解

关于此部分内容,JDK官方提供了详细介绍:Annotation Types

1)声明

注解声明产生新的注解类型,注解是一个特殊的接口类型。正常的接口声明与注解声明的区别在于,接口声明的关键字是interface,而注解声明使用@interface关键字。另外,注解成员只有返回类型有限的方法,不存在属性。

声明语法:

AnnotationTypeDeclaration:
{InterfaceModifier} @ interface Identifier AnnotationTypeBody

以下是一个简单的注解声明:

public @interface Description {
}
2)成员

注解的成员只有方法,每个方法为注解定义一个元素。注解有且仅有通过方法定义的元素。

定义成员的语法如下

AnnotationTypeElementDeclaration:
{AnnotationTypeElementModifier} UnannType Identifier ( ) [Dims] [DefaultValue] ;

注解中的方法成员返回类型有限,仅允许以下类型,否则会发生编译时错误:

  1. 基本类型
  2. java.lang.String
  3. 具体的Class类型,或者带类型通配符的Class(Class<?>)
  4. 枚举类型
  5. 注解类型
  6. 以上类型的数组

以下自定义注解类型枚举了注解方法可能的返回类型:

public @interface Description {
    // 基本类型
    int age() default 18;
    // java.lang.String
    String name();
    // 具体class
    Class<Long> specificClass() default Long.class;
    // 通配
    Class<?> classes() default String.class;
    // 枚举
    CEnum cEnum();
    // 其他注解类型
    Override otherAnno();
    enum CEnum {}
}

三、JDK对注解的支持

jdk提供接口java.lang.annotation.Annotation支持注解声明,在反射包下,提供接口java.lang.reflect.AnnotatedElement支持注解解析。

1)Annotation

首先看下Annotation接口的声明:

public interface Annotation {
     boolean equals(Object obj);
     int hashCode();
     String toString();
     Class<? extends Annotation> annotationType();
}

关于Annotation 接口,有以下说明:

  1. Annotation 接口是所有注解类型都会隐式继承的接口;
  2. 手动显式继承Annotation 接口的接口不是注解;
  3. Annotation 接口本身也不是注解。

由于继承性,所有的注解也拥有了Annotation 的方法。

2)AnnotatedElement

AnnotatedElement 接口表示当前JVM中一个“被注解的程序元素”。在Java中,所有实现AnnotatedElement 接口的类,都是可被注解修饰的。

该接口提供了一系列方法,获取程序元素上的注解:

AnnotatedElement方法

Java中,实现这个接口的类有:

AnnotatedElement实现类

因此,java程序中,注解几乎可修饰所有常见的元素,如:

  1. AccessibleObject(可访问对象,如:方法、构造器、属性等)
  2. Class(类,就是你用Java语言编程时每天都要写的那个东西)
  3. Constructor(构造器,类的构造方法的类型)
  4. Executable(可执行的,如构造器和方法)
  5. Field(属性,类中属性的类型)
  6. Method(方法,类中方法的类型)
  7. Package(包,你每天都在声明的包的类型)
  8. Parameter(参数,主要指方法或函数的参数,其实是这些参数的类型)

得益于此,可以通过反射,获取程序元素上的注解,对程序施加影响。

四、注解类型

按照注解是否具有成员,以及使用方式,注解可分为:

  1. 标记型注解
  2. 数据型注解
  3. 元注解

标记型注解

标记型注解不具备方法,仅仅利用自身的存在与否反映信息,如@Override

数据型注解

数据型注解具备方法,能够提供更加丰富的信息,如@Target

元注解

根据注解的使用方式,如果注解是用于修饰其他注解的,那么这个注解就是一个元注解。如@Target

五、基础元注解

jdk在java.lang.annotation包下提供了几个基本的元注解,这些注解是自定义注解的基石。

这些注解包括:

  1. @Documented
  2. @Target
  3. @Retention
  4. @Inherited
  5. @Repeatable(jdk8+)

下面分别对这些重要的注解进行详细介绍。

1)@Documented

Documented是标记型注解,只能修饰其他注解。在使用javadoc等工具生成java文档时,若使用具有@Document的注解修饰程序元素,在java文档中,这些注解会作为文档的一部分说明程序元素。

如自定义注解被@Document修饰

@Documented
@Target(value = {ElementType.TYPE})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Description {
    String value() default "";
}

@Description 修饰的类:

@Description(value = "描述")
public class AnnotationTest {

那么类AnnotationTest 产生的javadoc文档中,注解会作为说明的一部分:

@document效果
2)@Target

Target是数据型注解,只能修饰其他注解,指示注解可修饰的程序元素。

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

包含一个ElementType数组类型返回值的方法,指定注解可修饰的多个程序元素类型。通过观察枚举ElementType可知注解可修饰的程序元素集合。

默认:注解未使用@Target修饰时,默认修饰除TYPE_PARAMETER(类型参数)之外的所有程序元素。

4)@Retention

Retention是数据型注解,只能修饰其他注解,指示被修饰的注解能够保留多久;只有直接用于修饰注解时保留策略有效,作为注解的成员时保留策略无效。

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

包含一个RetentionPolicy类型返回值的方法,通过观察其源码,保留策略包括:

  1. SOURCE--注解会被编译器丢弃,不可反射读;
  2. CLASS--编译器保留,VM和运行时丢弃,不可反射读;
  3. RUNTIME--编译器记录入class文件,VM和运行时都会保留;因此这种类型是可以使用反射读取的。

默认:注解未使用@Retention修饰时,保留策略默认为RetentionPolicy.CLASS

4)@Inherited

Inherited是标记型注解,只能修饰其他注解,指示注解类型的自动继承;注解类型的继承与我们熟知的类继承,接口继承有很大的区别。

当用户在一个程序元素类上,使用AnnotatedElement的相关注解查询方法,查询元注解Inherited修饰的其他注解类型A时,如果这个类本身并没有被注解A修饰,那么会自动查询这个类的父类是否被注解A修饰。查询过程会沿着类继承链一直向上查找,直到注解A被找到,或者到达继承链顶层(Object)。

如果元注解Inherited修饰的其他注解,修饰了除类之外的其他程序元素,那么继承性将会失效。

下面的Demo演示了注解的这种继承性:

public class InheritedTest {

    @Target(value = {ElementType.METHOD, ElementType.TYPE})
    @Retention(value = RetentionPolicy.RUNTIME)
    @Inherited // 声明注解具有继承性
    @interface AInherited {
        String value() default "";
    }

    @AInherited("父类")
    class SuperClass {}

    class ChildClass extends SuperClass {

    }

    public static void main(String[] args) {
        AInherited annotation = ChildClass.class.getAnnotation(AInherited.class);
        System.out.println(annotation); 
        // output: @annotations.InheritedTest$AInherited(value=父类)
    }
}

若自定义注解 AInherited 没有被Inherited 修饰,子类ChildClass.class.getAnnotation(AInherited.class)将会返回null

若子类ChildClass在类上自行指定了与父类相同类型的注解AInherited,那么ChildClass.class.getAnnotation(AInherited.class)将会返回子类声明的注解

属性和方法注解的继承

属性和方法注解的继承,与类注解的继承完全不同,与元注解Inherited毫无关系,忠实于方法/属性本身的继承。

以下示例说明属性/方法注解的继承:

public class InheritedTest {

    @Target(value = {ElementType.METHOD, ElementType.TYPE, ElementType.FIELD})
    @Retention(value = RetentionPolicy.RUNTIME)
    @interface AInherited {
        String value() default "";
    }

    @AInherited("父类")
    class SuperClass {
        @AInherited("父类方法foo")
        public void foo() {}
        @AInherited("父类方法bar")
        public void bar(){}
        @AInherited("父类的属性")
        public String field;
    }

    class ChildClass extends SuperClass {
        @Override
        public void foo() {
            super.foo();
        }
    }

    public static void main(String[] args) throws NoSuchMethodException, NoSuchFieldException {
        Method foo = ChildClass.class.getMethod("foo");
        System.out.println(Arrays.toString(foo.getAnnotations()));
        // 子类ChildClass重写了父类方法foo,并且@Override注解只在源码阶段保留,所以没有任何注解

        Method bar = ChildClass.class.getMethod("bar");
        System.out.println(Arrays.toString(bar.getAnnotations()));
        // bar方法未被子类重写,从父类继承到了原本注解

        Field field = ChildClass.class.getField("field");
        System.out.println(Arrays.toString(field.getAnnotations()));
        // 同上
    }
}
5)@Repeatable

元注解Repeatable是JDK8开始引入的一个特别有意思的注解。Repeatable指示它修饰的注解是可重复的。Repeatable的值是指示这个可重复注解的容器注解类型,容器类型强制包含一个名为value(),返回类型为可重复注解的数组。

通常情况下,一个程序元素,只能使用同一个注解修饰一次,否者会发生编译时错误。Repeatable允许其修饰的注解,可多次作用于同一个程序元素。

Repeatable较为抽象,下面通过示例讲解:

public class RepeatableTest {

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Repeatable(DescList.class)
    @interface Desc {
        String value();
    }

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @interface DescList {
        Desc[] value(); // 必须包含返回类型为可重复注解类型的数组,名为value的方法
        Class<?> c() default String.class;
    }

    @Desc("foo")
    @Desc("foo1")
    public static void foo() {
    }
    @DescList(value = {@Desc("bar"), @Desc("bar1")})
    public static void bar() {
    }

    public static void main(String[] args) throws NoSuchMethodException {
        Method foo = RepeatableTest.class.getMethod("foo");
        System.out.println("get annotations:" + Arrays.toString(foo.getAnnotations()));
        System.out.println("by annotation type: " + Arrays.toString(foo.getAnnotationsByType(Desc.class)));
        System.out.println("by annotation type: " + Arrays.toString(foo.getAnnotationsByType(DescList.class)));

        Method bar = RepeatableTest.class.getMethod("bar");
        System.out.println("get annotations:" + Arrays.toString(bar.getAnnotations()));
        System.out.println("by annotation type: " + Arrays.toString(bar.getAnnotationsByType(Desc.class)));
        System.out.println("by annotation type: " + Arrays.toString(bar.getAnnotationsByType(DescList.class)));
    }
}

示例中,foo和bar方法使用了不同的方式,达到了重复使用@Desc修饰的效果。在此也可以一窥AnnotatedElement的getAnnotation系列方法,与getAnnotationsByType系列方法的区别。

在getAnnotation下,得到的注解类型都是容器注解类型;
在getAnnotationsByType下,得到的注解类型是指定的注解类型。

六、总结

总结如下:

  1. java注解是JDK对元数据的支持,注解本身不会对程序产生影响,使用APT工具解析注解,可以对程序施加影响。
  2. 接口java.lang.annotation.Annotation与接口java.lang.reflect.AnnotatedElement是JDK注解的两大基石,前者是所有注解的隐式父接口,后者是APT工具反射读取注解的关键所在。
  3. JDK提供几个基本元注解为自定义注解提供支持。

推荐阅读更多精彩内容