美文网首页Java 杂谈
Java注解简介篇

Java注解简介篇

作者: 理查德成 | 来源:发表于2019-04-14 17:56 被阅读7次

    摘要

    本文详细介绍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,并且Source注解只在源码阶段保留,所以没有任何注解
    
            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提供几个基本元注解为自定义注解提供支持。

    相关文章

      网友评论

        本文标题:Java注解简介篇

        本文链接:https://www.haomeiwen.com/subject/gruswqtx.html