Java Annotation ( 注解 )


JDK 5 开始, Java 增加了对元数据 (MetaData) 的支持,也就是 Annotation (注解) 。Annotation 其实是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。

 

通过使用注解,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或部署。

Annotation 提供了一种为程序元素设置元数据的方法,从某些方面来看, Annotation 就像修饰符一样,可用于修饰包、接口、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被存储在 Annotation 的 "name=value" 对中。

Annotation 是一个接口,程序可以通过反射来获取指定程序元素的 Annotation 对象,然后通过 Annotation 对象来取得注解里的元数据。

Annotation 不影响程序代码的执行,无论增加、删除 Annotation ,代码都始终如一地执行。如果希望让程序中的 Annotation 在运行时起一定作用,只有通过某种配套的工具对 Annotation 中的信息进行访问和处理。
访问和处理 Annotation 的工具统称 APT (Annotation Processing Tool)



1 基本 Annotation
---------------------------------------------------------------------------------------------------
Annotation 必须使用工具来处理,工具负责提取 Annotation 里包含的元数据,工具还会根据这些元数据增加额外的功能。
使用 Annotation 时要在其前面增加 @ 符号,并把该 Annotation 当成一个修饰符使用,用于修饰它支持的程序元素。

5 个基本的 Annotation:
        @Overrid
        @Deprecated
        @SuppressWarnings
        @SafeVarargs            : Java 7 新增的
        @FunctionalInterface    :Java 8 新增的
        
        
限定重写父类方法:@Overrid
---------------------------
@Overrid 就是用来指定方法覆盖的,它可以强制一个子类必须覆盖父类的方法。

例子:

public class Fruit
 {
     public void info()
     {
         System.out.println("水果的info方法...");
     }
 }
 class Apple extends Fruit
 {
     // 使用@Override指定下面方法必须重写父类方法
     @Override
     public void inf0()
     {
         System.out.println("苹果重写水果的info方法...");
     }
 }



@Overrid 的作用是告诉编译器检查这个方法,保证父类要包含一个被该方法重写的方法,否则就会编译出错。
@Overrid 只能修饰方法,不能修饰其他程序元素。


标记已过时:@Deprecated
------------------------
@Deprecated 用于标识某个程序元素(类、方法)已过时,当其他程序使用已过时的类、方法时,编译器将会给出警告。
例子:

class Apple
 {
     // 定义info方法已过时
     @Deprecated
     public void info()
     {
         System.out.println("Apple的info方法");
     }
 }
 public class DeprecatedTest
 {
     public static void main(String[] args)
     {
         // 下面使用info方法时将会被编译器警告
         new Apple().info();
     }
 }



@Deprecated 的作用与文档注释中的 @deprecated 标记的作用基本相同,但它们的用法不同,前者是 JDK 5 才支持的注解,无须放在文档注释语法 (/** ... */部分 )中,而是直接用于修饰程序中的程序单元,如方法、类、接口等。


抑制编译器警告: @SuppressWarnings
-----------------------------------
@SuppressWarnings 指示被该 Annotation 修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告。
@SuppressWarnings 会一直作用于该程序元素的所有子元素。例如,使用 @SuppressWarnings 修饰某个类取消显示某个编译器警告,同时又修饰该类里的某个方法取消另一个编译器警告,那么该方法将会同时取消显示这两个编一起去警告。

通常情况下,如果程序中使用没有泛型限制的集合会引起编译器警告,为了避免这种编译器警告,可以使用 @SuppressWarnings 修饰。
例子:

// 关闭整个类里的编译器警告
 @SuppressWarnings(value="unchecked")
 public class SuppressWarningsTest
 {
     public static void main(String[] args)
     {
         List<String> myList = new ArrayList();     // ①
     }
 }


当使用 @SuppressWarnings Annotation 来关闭编译器警告时,一定要在括号里使用 name=value 的形式为该 Annotation 的成员变量设置值。



Java 7 的“堆污染”警告与 @SafeVarargs
--------------------------------------
如下代码可能导致运行时异常

List list = new ArrayList<Integer>();
 list.Add(20); //添加元素时引发 unchecked 异常

 // 下面的代码引起 “未经检查的转换” 的警告。编译、运行时完全正常
 List<String> ls = list;  ①
 // 但只要访问ls 里的元素,如下面代码就会引起运行时异常
 System.out.println(ls.get(0));


Java 把引发这种错误的原因称为“堆污染” (Heap pollution),当把一个不带泛型的对象赋给一个带泛型的变量时,往往就会发生这种“堆污染”,如 ① 代码所示。

对于形参数可变的方法,该形参的类型又是泛型,这将更容易导致“堆污染”,例如:

public class ErrorUtils
 {
     @SafeVarargs
     public static void faultyMethod(List<String>... listStrArray)
     {
         // Java语言不允许创建泛型数组,因此listArray只能被当成List[]处理
         // 此时相当于把List<String>赋给了List,已经发生了“擦除”
         List[] listArray = listStrArray;
         List<Integer> myList = new ArrayList<Integer>();
         myList.add(new Random().nextInt(100));
         // 把listArray的第一个元素赋为myList
         listArray[0] = myList;
         String s = listStrArray[0].get(0);
     }
 }


由于该方法有个形参 List<String>...类型,个数可变的形参相当于数组,但Java又不支持泛型数组,因此程序只能把 List<String>...当成 List[]处理,这里就发生了“堆污染”。
Java 7 会在定义该方法时就发出“堆污染”警告,这样保证开发者更早地注意到程序可能存在的漏洞。
但在有些时候,开发者不希望看到这个警告,则可以用如下三种方式来抑制警告:
    1.使用 @SafeVarargs 修饰引发该警告的方法或构造器
    2.使用 @SuppressWarnings("unchecked")修饰。
    3.编译时使用 -Xlint:varargs 选项
弟三种方式比较少用,通常可以选择第一种或第二种方式,尤其是使用 @SafeVarargs 修饰引发警告的方法或构造器。


Java 8 的函数式接口与 @FunctionalInterface
-------------------------------------------
Java 8 规定:如果接口中只有一个抽象方法(可以包含多个默认方法或多个 static 方法),该接口就是函数式接口。
@FunctionalInterface 就是用来指定某个接口必须是函数式接口。

@FunctionalInterface
 public interface FunInterface
 {
     static void foo()
     {
         System.out.println("foo类方法");
     }
     default void bar()
     {
         System.out.println("bar默认方法");
     }
     void test(); // 只定义一个抽象方法

 //    void abc();
 }


编译上面的程序,可能丝毫看不出程序中 @FunctionalInterface 有任何作用, @FunctionalInterface 只是告诉编译器检查这个接口,保证该接口只能包含一个抽象方法,否则就会编译出错。

@FunctionalInterface 主要是帮助程序员避免一些低级错误,例如,在 FunInterface 接口中再增加一个抽象方法 abc(),编译程序时将出现错误。

@FunctionalInterface 只能修饰接口,不能修饰其他程序元素。

*
*
*

2 JDK 的元 Annotation
----------------------------------------------------------------------------------------
java.lang.annotation 下的6个 Meta Annotation (元 Annotation)


使用 @Retention
----------------------------
@Retention 只能用于修饰 Annotation 定义,用于指定被修饰的 Annotation 可以保留多长时间,@Retention 包含一个 RetentionPolicy 类型的 value 成员变量,所以使用@Retention 时必须为该 value 成员变量指定值。

value 成员变量的值只能是如下三个:
    RetentionPolicy.CLASS:    编译器将把 Annotation 记录在 class 文件中。当运行 Java 程序时, JVM 不可获取 Annotation 信息,这是默认值。
    RetentionPolicy.RUNTIME:编译器将把 Annotation 记录在 class 文件中。当运行 Java 程序时, JVM 也可获取 Annotation 信息,程序可以通过反射获取该 Annotation 信息。
    RetentionPolicy.SOURCE: Annotation 只保留在源代码中,编译器直接丢弃这种 Annotation。

// 定义下面的 Testable Annotation 保留到运行时
     @Retention(value=RetentionPolicy.RUNTIME)
     public @interface Testable{}
     
     //定义下面的 Testable Annotation 将被编译器直接丢弃
     @Retention(RetentionPolicy.SOURCE)
     public @interface Testable{}


    
上面代码中使用 @Retention 元 Annotation 时,并未通过 value=RetentionPolicy.SOURCE 的方式来为该成员变量指定值,这是因为但 Annotation 的成员变量名为 value 时,程序中可以直接在 Annotation 后的括号里指定该成员变量的值,无须指定 name=value 的形式。

如果使用注解时只需要为 value 成员变量指定值,则使用该注解时可以直接在该注解后的括号里指定 value 成员变量的值,无须使用 "name=变量值" 的形式。    
    

使用 @Target
------------------
@Target 也只能修饰一个 Annotation 定义,它用于指定被修饰的 Annotation 能用于修饰哪些程序单元。
@Target 元 Annotation 也包含一个名为 value 的成员变量,该成员变量的值只能是如下几个:
    ElementType.ANNOTATION_TYPE: 指定该策略的 Annotation 只能修饰 Annotation
    ElementType.CONSTRUCTOR: 指定该策略的 Annotation 只能修饰构造器
    ElementType.FIELD: 指定该策略的 Annotation 只能修饰成员变量
    ElementType.LOCAL_VARIABLE: 指定该策略的 Annotation 只能修饰局部变量
    ElementType.METHOD: 指定该策略的 Annotation 只能修饰方法
    ElementType.PACKAGE: 指定该策略的 Annotation 只能修饰 包定义
    ElementType.PARAMETER: 指定该策略的 Annotation 可以修饰参数
    ElementType.TYPE: 指定该策略的 Annotation 可以修饰类、接口 (包括注解类型)或枚举定义
例子:

//@ActionListenerFor Annotation 只能修饰成员变量
     @Target(ElementType.FIELD)
     public @interface ActionListenerFor{}
     
     // @Testable Annotation 只能修饰方法
     @Target(ElementType.METHOD)
     public @interface Testable{}


    
使用 @Documented
----------------------------------------
@Documented 用于指定被该元 Annotation 修饰的 Annotation 类将被 javadoc 工具提取成文档
如果定义 Annotation 类时使用了 @Documented 修饰,则所有使用该 Annotation 修饰的程序元素的 API 文档中将会包含该 Annotation 的说明

下面定义了一个 Tesble Annotation, 程序使用 @Documented 来修饰 @Testable Annotation 定义,所以该 Annotation 将被 javadoc 工具提取。

@Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.METHOD)
 // 定义Testable Annotation将被javadoc工具提取
 @Documented
 public @interface Testable
 {
 }



上面代码中指定了 javadoc 工具生成的 API 文档将提取 @Testable 的使用信息。
下面代码定义了一个 MyTest 类,该类中的 info() 方法使用了 @Testable 修饰

public class MyTest
 {
     // 使用@Test修饰info方法
     @Testable
     public void info()
     {
         System.out.println("info方法...");
     }
 }




使用 @Inherited
--------------------------------------
@Inherited 元 Annotation 指定被它修饰的 Annotation 将具有继承性-----如果某个类使用了 @Xxx注解修饰(定义该 Annotation 时使用了 @inherited 修饰),则其子类将自动被 @Xxx 修饰

@Target(ElementType.TYPE)
 @Retention(RetentionPolicy.RUNTIME)
 @Inherited
 public @interface Inheritable
 {
 }


上面代码表明 @Inheritable 具有继承性, 如果某个类使用了 @Inheritable 修饰,则该类的子类将自动使用 @Inheritable 修饰

// 使用@Inheritable修饰的Base类
 @Inheritable
 class Base
 {
 }
 // TestInheritable类只是继承了Base类,
 // 并未直接使用@Inheritable Annotiation修饰
 public class InheritableTest extends Base
 {
     public static void main(String[] args)
     {
         // 打印TestInheritable类是否具有@Inheritable修饰
         System.out.println(InheritableTest.class
             .isAnnotationPresent(Inheritable.class)); //输出: true
     }
 }

 *
 *
 *



3 自定义 Annotation
---------------------------------------------------------------------------------------------
定义新的 Annotation 类型使用 @interface 关键字 (在原有 interface 关键字前增加 @ 符号)
例子:

//定义一个简单的 Annotation 类型
     public @interface Test
     {
     }


定义了新的 Annotation 之后,就可以在程序的任何地方使用该 Annotation, 通常会把 Annotation 放在所有修饰符之前,而且由于使用 Annotation 时可能还需要为成员变量指定值,因而 Annotation 的长度可能较长,所以通常把 Annotation 单独另放一行。

//使用 @Test 修饰类定义
 @Test
 public class MyClass
 {
     ...
 }


默认情况下, Annotation 可以用于修饰任何程序元素。

Annotation 不仅可以是这种简单的 Annotation, 还可以带成员变量, Annotation 的成员变量在 Annotation 定义中以无形参的方法形式声明,其方法名和返回值定义了该成员变量的名称和类型
例子:

public @interface MyTag
 {
     // 定义带两个成员变量的 Annotation
     // Annotation 中的成员变量以方法的形式定义
     String name();
     int age();
 }


上面定义 Annotation 的代码与定义接口的语法非常类似,只是使用 @interface 关键字来定义,而接口使用 interface 定义。

一旦在 Annotation 里定义了成员变量之后,使用该 Annotation 时就应该为该 Annotation 的成员变量指定值

public class Test
 {
     //使用带成员变量的 Annotation 时,需要为成员变量赋值
     @MyTag(name="xx", age=6)
     public void info()
     {
     }
 }



也可以在定义 Annotation 的成员变量时为其指定初始值(默认值),指定成员变量的初始值使用 default 关键字。

public @interface MyTag
 {
     //定义了两个成员变量的 Annotation
     //使用 default 为两个成员变量指定初始值
     String name() default "yeeku" ;
     int age() default 32;
 }



如果为 Annotation 的成员变量指定了默认值,使用该 Annotation 时则可以不为这些成员变量指定值,而是直接使用默认值
例子:

public class Test
 {
     //使用带成员变量的 Annotation, 因为它的成员变量有默认值,所以可以不为它的成员变量指定值
     @MyTag
     public void info()
     {
     }
 }



当然也可以在使用 MyTag Annotation 时为成员变量指定值,默认值不会起作用。

根据 Annotation 是否可以包含成员变量,可以把 Annotation 分为如下两类:
    标记 Annotation: 没有定义成员变量的 Annotation 类型被称为标记。这种 Annotation 仅利用自身的存在与否来提供信息,如 @Overrid, @Test 等 Annotation
    元数据 Annotation: 包含成员变量的 Annotation, 因为它们可以接受更多的元数据,所以被称为元数据 Annotation
    
    
提取 Annotation 信息
------------------------------------------------
使用 Annotation 修饰了类、方法、成员变量等成员之后,这些 Annotation 不会自己生效,必须由开发者提供相应的工具来提取并处理 Annotation 信息
Java 使用 java.lang.annotation.Annotation 接口来代表程序元素前面的注解,该接口是所有注解的父接口。
Java 5 在 java.lang.reflect 包下新增 AnnotatedElement 接口,该接口代表程序中可以接受注解的程序元素,有如下几个实现类:
    Class: 类定义
    Constructor: 构造器定义
    Field: 类的成员变量定义
    Method: 类的方法定义
    Package: 包定义
    
java.lang.reflect 包所提供的反射 API 增加了读取运行时 Annotation 的能力。
只有在定义 Annotation 是使用了 @Retention(RetentionPolicy.RUNTIME) 修饰,该 Annotation 才会在运行时可见, JVM 才会在装载 *.class 文件时读取保存在 class 文件中的 Annotation.

AnnotatedElement接口是所有程序元素的父接口,所以程序通过反射获取了某个类的 AnnotatedElement 对象 (如 Class, Method, Constructor 等)之后,程序就可以调用该对象的 AnnotatedElement 接口方法来访问 Annotation 信息。

// 获取 Test 类的 info方法的所有注解
 Annotation[] aArray = Class.forName("Test").getMethod("info",null).getAnnotations();
 // 遍历所有注解
 for( Annotation an : aArray)
 {
     System.out.println(an);
 }


如果需要获取某个注解里的元数据,则可以将注解强制类型转换成所需的注解类型,然后通过注解对象的抽象方法来访问这些元数据。

例子:

//获取 tt 对象的 info 方法所包含的所有注解
     Annotation[] annotation = Class.forName("Test").getMethod("info",null).getAnnotations();
     //遍历每个注解对象
     for (Annotation tag : annotation)
     {
         //如果 tag 注解是 MyTag1 类型
         if ( tag instanceof MyTag1)
         {
             System.out.println("Tag is: " + tag);
             //将 tag 强制类型转换为 MyTag1
             //输出 tag 对象的name 和 age 两个成员变量的值
             System.out.println("tag.name(): " + ((MyTag1)tag).name());
             System.out.println("tag.age(): " + ((MyTag1)tag).age());
         }
         //如果 tag 注解是 MyTag2 类型
         if ( tag instanceof MyTag2)
         {
             System.out.println("Tag is: " + tag);
             //将 tag 强制类型转换为 MyTag2
             //输出 tag 对象的name 和 age 两个成员变量的值
             System.out.println("tag.name(): " + ((MyTag2)tag).name());
             System.out.println("tag.age(): " + ((MyTag2)tag).age());
         }
     }




Java 8 新增的重复注解
---------------------------------------------------------------------------
Java 8 之前,同一个程序元素前最多只能使用一个相同类型的 Annotation; 如果需要在同一个元素前使用相同类型的 Annotation, 则必须使用 Annotation “容器”
Java 8 之前只能这样写:

@Results({@Result(name="failure", location="failed.jsp"),
 @Result(name="success", location="succ/jsp")})
 public Action FooAction{...}



实质是,@Results注解只包含一个名字为 value, 类型为 Result[] 的成员变量,程序指定的多个 @Result将作为 @Results 的value属性(数组类型)的数组元素。

Java 8 开始,传统的语法得到简化。

开发重复注解需要使用 @Repeatable 修饰

// 指定该注解信息会保留到运行时
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.TYPE)
 @Repeatable(FkTags.class)
 public @interface FkTag
 {
     // 为该注解定义2个成员变量
     String name() default "疯狂软件";
     int age();
 }



使用 @Repeatable 时必须为value成员变量指定值,该成员变量的值应该是一个“容器”注解----该“容器”注解可以包含多个 @FkTag, 因此还需要定义如下的"容器"注解

// 指定该注解信息会保留到运行时
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.TYPE)
 public @interface FkTags
 {
     // 定义value成员变量,该成员变量可接受多个@FkTag注解
     FkTag[] value();
 }



注意:“容器”注解的保留期必须和它所包含的注解的保留期相同或更长,否则编译器报错

@FkTag(age=5)
 @FkTag(name="疯狂Java" , age=9)
 //@FkTags({@FkTag(age=5),
 //    @FkTag(name="疯狂Java" , age=9)})
 public class FkTagTest
 {
     public static void main(String[] args)
     {
         Class<FkTagTest> clazz = FkTagTest.class;
         /* 使用Java 8新增的getDeclaredAnnotationsByType()方法获取
             修饰FkTagTest类的多个@FkTag注解 */
         FkTag[] tags = clazz.getDeclaredAnnotationsByType(FkTag.class);
         // 遍历修饰FkTagTest类的多个@FkTag注解
         for(FkTag tag : tags)
         {
             System.out.println(tag.name() + "-->" + tag.age());
         }
         /* 使用传统的getDeclaredAnnotation()方法获取
             修饰FkTagTest类的@FkTags注解 */
         FkTags container = clazz.getDeclaredAnnotation(FkTags.class);
         System.out.println(container);
     }
 }



重复注解只是一种简化写法,这种简化写法是一种假象:多个重复注解其实会被作为“容器”注解的value成员变量的数组元素



Java 8 新增的 Type Annotation
------------------------------------------
Java 8 为 ElementType 枚举增加了 TYPE_PARAMTER, TYPE_USE 两个枚举值,这样就允许定义枚举时使用 @Target(ElementType.TYPE_USE) 修饰,这种注解被称为 Type Annotation (类型注解)
Type Annotation 可用在任何用到类型的地方