Java程序设计16——Annotatio注释

  Annotation是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用Annotation,程序开发人员可以在不改变原有逻辑的情况下,在源文件嵌入一些补充信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署。 
  Annotation提供了一种为程序元素设置元数据的方法,从某些方面来看,Annotation就像修饰符一样被使用,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被存储在Annotation的"name=value"对中。
  注意:Annotation是一个接口,程序可以通过反射来获取指定程序元素的Annotation对象。然后通过Annotation对象来取得注释里的元数据。需要注意本章中使用Annotation的地方,有的Annotation指的是java.lang.Annotation接口,有的指的是注释本身。
  Annotation能被用来作为程序元素(类、方法、成员变量等)设置元数据。需要指出的是:Annotation不能影响程序代码的执行,无论增加、删除Annotation,代码都始终如一地执行。如果希望让程序中的Annotation能在运行时起一定的作用,只有通过某种配套的工具对Annotation中的信息进行访问和处理,访问和处理Annotation的工具统称为APT(Annotation Processing Tool)。

1 基本Annotation                                                    

  Annotation必须使用工具来处理,工具负责提取Annotation里包含的元数据,工具还会根据这些元数据增加额外的功能。在系统学习新的Annotation语法之前,先看一下Java提供的三个基本Annotation的用法:使用Annotation时要在其前面增加@符号,并把该Annotation当成一个修饰符使用,用于修饰它支持的程序元素。
三个基本的Annotation如下

1.@Override
2.@Deprecated
3.@SuppressWarnings

限定重写父类方法:@Override
@Override就是用来指定方法覆写的,它可以强制一个子类必须要覆写父类的方法。如下程序中使用@Override指定子类Apple的info方法必须重写父类方法。

 1 package chapter16;
 2 
 3 class Fruit {
 4     public void foo(){
 5         System.out.println("水果的info方法....");
 6     }
 7 };
 8 public class Apple extends Fruit{
 9     //使用@Override指定下面的方法必须重写父类方法
10     @Override
11     public void foo(){
12         System.out.println("苹果重写水果的info方法....");
13     }
14     public static void main(String[] args){
15         Fruit f = new Apple();
16         f.foo();
17     }
18 }
19 
20 输出结果:
21 苹果重写水果的info方法....

  编译上面程序,可能看不出程序中的@Override有何作用,因为@Override Annotation的作用是告诉编译器检查这个方法,并从父类查找是包含一个被该方法重写的方法,否则就编译出错。这个Annotation主要是帮助我们避免一些低级错误。例如我们把上面的Apple类的info方法不小心写成inf()这样的低级错误,可能会导致后期排错时的巨大障碍。但是如果你没有调用重写的方法,那就是调用了父类的方法,这样产生的结果就是另一种结果,编译会通过,但是结果是不符合预期的。

 1 package chapter16;
 2 
 3 class Fruit {
 4     public void foo(){
 5         System.out.println("水果的info方法....");
 6     }
 7 };
 8 public class Apple extends Fruit{
 9     //使用@Override指定下面的方法必须重写父类方法
10     @Override
11     public void fool(){
12         System.out.println("苹果重写水果的info方法....");
13     }
14     public static void main(String[] args){
15         Fruit f = new Apple();
16         f.foo();
17     }
18 }
19 
20 预期结果:水果的info方法....

标示已过时:@Deprecated
@Deprecated用于表示某个程序元素(类、方法等)已过时,当其他程序使用已过时的类、方法时,编译器将会给警告。如下程序指定Apple类中的info方法已过时,其他程序中使用Apple类的info方法时编译器将会给出警告。

那些是被@Deprecated注解标记的方法或者属性或类等,意思是“已过时”。如果你是新写代码,那么不推荐你这么做,有更好的替代方案,如果是老系统,那么告知你你这个方法已过时,不过JDK还将继续对他支持。被注解掉@Deprecated,表示是方法过时有新的方法替代,在jdk文档中可以找到相对应的新方法。

 

可以看到上面被deprecated的方法被划上了横线

上面应用程序使用了一个被deprecated的方法,表名该方法以及过时,在新版的jdk有替代的更好的方法。

抑制编译器警告:@SuppressWarnings
@SuppressWarnings指示被Annotation标识的程序元素(以及在该程序元素中的所有子元素)取消显示指示的编译器警告。@SuppressWarnings会一直作用于该程序元素的所有子元素,例如使用@SuppressWarnings标识一个类来取消显示某个编译器警告,同时又标识该类里某个方法取消显示另一个编译器警告,那么将在此方法同时取消这两个编译器警告。
通常情况下,如果程序中使用没有泛型限制的集合将会引起编译器警告,为了避免这种编译器警告,可以使用@SuppressWarningsAnnotation,下面程序取消了没有使用泛型的编译器警告。

 1 package chapter16;
 2 
 3 import java.util.*;
 4 
 5 @SuppressWarnings(value="unchecked");
 6 
 7 public class SuppressWarningsTest {
 8     public static void main(String[] args){
 9         List<String> l = new ArrayList();
10     }
11 }

程序使用@SuppressWarnings来关闭SuppressWarningsTest类里的所有编译器警告,编译上面程序时将不会砍掉任何编译器警告。
注意:使用@SuppressWarnings Annotation来关闭编译器警告时,一定需要在括号里使用name=value对来为该Annotation的成员变量设置值。

2 自定义Annotation                                              

  上面介绍的3个Annotation是java.lang包下的三个标准Annotation,下面介绍自定义的Annotation,并利用Annotation完成一些实际功能。

2.1 定义Annotation         

  定义新的Annotation类型使用@interface关键字(在原有的interface关键字前增加@符号),它用于定义新的Annotation类型。定义一个新的Annotation类型与定义一个接口非常像。如下即可定义一个简单的Annotation

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

  定义了该Annotation之后,就可以在程序任何地方来使用该Annotation,使用Annotation时的语法非常类似于public、final这样的修饰符。通常可用于修饰程序中的类、方法、变量、接口等定义,通常我们会把Annotation放在所有修饰符之前,而且由于使用Annotation时可能还需要为其成员变量指定值,因而Annotation的长度可能较长,所以通常把Annotation另放一行。如下所示。

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

  默认情况下,Annotation可用于修饰任何程序元素,包括类、接口、方法等,如下程序使用@TestAnnotation来修饰方法。

1 public class MyClass{
2     //使用@Test Annotation修饰方法
3     @Test
4     public void info(){
5         .....
6     }
7     .....
8 }

  Annotation不仅可以是这种简单的Annotation,Annotation还可以带成员变量,Annotation的成员变量在Annotation定义中以无参数方法的形式来声明。其方法名和返回值定义了该成员的名字和类型。如下代码可以定义一个有成员变量的Annotation:

1 package chapter10;
2 
3 public @interface MyTag {
4     //定义了两个成员变量的Annotation
5     //Annotation的成员变量定义方式类似于传统的方法
6     String name();
7     int age();
8 }

  上面的注释和接口定义是很像的,注释使用@interface来定义,而接口用interface定义。但是对于变量的定义是有点区别的,上面定义的是变量,但不是方法。一旦在Annotation里定义了成员变量之后,使用该Annotation时应该为该Annotation的成员变量指定值,如下所示:

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

  我们还可以在定义Annotation的成员变量时为其指定初始值,指定成员变量的初始值可使用default关键字。如下代码定义了MyTag Annotation,该Annotation里包含了两个成员变量:name和age,这两个成员变量使用了default指定了默认值。

 1 package chapter10;
 2 
 3 public @interface MyTag {
 4     //定义了两个成员变量的Annotation
 5     //Annotation的成员变量定义方式类似于传统的方法
 6     String name() default "yeeku";
 7     int age() default 3;
 8 }
 9     如果为Annotation的成员变量指定了默认值,使用该Annotation则可以不为这些成员变量指定值,而是直接使用默认值,如下代码所示:
10     public class Test{
11         //使用带成员变量的Annotation
12         //因为它的成员变量有默认值,所以可以无须为成员变量指定值
13         @MyTag
14         public void info(){
15         ...
16         }
17         ....
18     }

  当然我们也可以在使用MyTagAnnotation时为成员变量指定值,如果为MyTag的成员变量指定了值,则默认值不起作用。根据我们介绍的Annotation是否可以包含成员变量,可以把Annotation分成两类:
1.标记Annotation:一个没有成员定义的Annotation类型被称为标记。这种Annotation仅使用自身的存在与否来为我们提供信息。如前面介绍的@Override、@Test等Annotation
2.元数据Annotation,那些包含成员变量的Annotation。因为它们可接受更多元数据,所以也被称为元数据Annotation

2.2 提取Annotation的信息 

  Java使用Annotation接口来代表程序元素前面的注释,该接口是所有Annotation类型的父接口。除此之外,Java在java.lang.reflect包下新增了AnnotatedElement接口,该接口代表程序中可以接收注释的程序元素,该接口主要由如下几个实现类:
Class:类定义
Constructor:构造器定义
Field:类的成员变量定义
Method:类的方法定义
Package:类的包定义
  java.lang.reflect包下主要包含了一些实现反射功能工具类,实际上,java.lang.reflect包所提供的反射APL扩充了读取运行时Annotation的能力。当一个Annotation类型被定义为运行时Annotation后,该注释才是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
  AnnotatedElement接口是所有程序元素(如Class、Method、Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象如Class、Method、Constructor之后,程序就可以调用该对象的如下三个方法来访问Annotation信息:

1.getAnnotation(Class<T> annotationClass):返回该程序元素上存在的、指定类型的注释,如果该类型的注释不存在,则返回null。
2.Annotation[] getAnnotation():返回该程序元素上存在的所有注释
3.boolean isAnnotationPresent(Class<? extends Annotation> annotationClass):判断该程序元素上是否包含指定类型的注释,存在则返回true,否则返回false
View Code

  为了获取程序中程序元素如Class、Method等,必须使用反射知识。

下面程序片段获取Test类的info方法里的所有注释,并将这些注释打印出来:

 1 //获取Test类的info方法的所有注释
 2     Annotation[] aArray = Class.forName("Test").getMethod("info").getAnnotations();
 3     //遍历所有注释
 4     for(Annotation an : aArray){
 5         System.out.println(an);
 6     }
 7     如果我们需要获取某个注释里的元数据,则可以将注释强制类型转换成所需的注释类型,然后通过注释对象的抽象方法来访问这些元数据,如下所示:
 8     //获取tt对象的info方法所包含的所有注释
 9     Annotation[] annotation = tt.getClass().getMethod("info").getAnnotation();
10     //遍历每个注释对象
11     for(Annotation tag:annotation){
12         //如果tag注释是MyTag1类型
13         if(tag instanceof MyTag1){
14             System.out.println("Tag is: " + tag);
15             //将tag强制类型转换为MyTag1,
16             //并调用tag对象的method1和method2两个方法
17             System.out.println("tag.name(): " + ((MyTag1)tag).method1());
18             System.out.println("tag.age(): " + ((MyTag1)tag).method2());
19         }
20         //如果tag注释是MyTag2类型
21         if(tag instanceof MyTag2){
22             System.out.println("Tag is: " + tag);
23             //将tag强制类型转换为MyTag2,
24             //并调用tag对象的method1和method2两个方法
25             System.out.println("tag.name(): " + ((MyTag2)tag).method1());
26             System.out.println("tag.age(): " + ((MyTag2)tag).method2());
27         }
28     }

使用Annotation的例子
  下面介绍两个使用Annotation的例子,第一个Annotation Testable没有任何成员变量,仅是一个标记Annotation,它的作用是标记哪些方法是可测试的。

 1 package chapter10;
 2 
 3 import java.lang.annotation.ElementType;
 4 import java.lang.annotation.Retention;
 5 import java.lang.annotation.RetentionPolicy;
 6 import java.lang.annotation.Target;
 7 
 8 //使用JDK的元数据Annotation:Retention
 9 @Retention(RetentionPolicy.RUNTIME)
10 //使用JDK的元数据Annotation:Target
11 @Target(ElementType.METHOD)
12 public @interface Testable{
13         
14 }

  上面程序定义了一个标记TestableAnnotation,定义该Annotation时使用了@Retention和@Target两个系统元注释,其中@Retention注释指定Testable注释可以保留多久,而@Target注释指定Testable注释能修饰的目标(只能是方法)。

  上面的Testable Annotation用于标识哪些方法是可测试的,该Annotation可以作为JUnit测试框架的补充,在JUnit框架中它要求测试用例的测试方法必须以test开头。如果使用Testable注释则可把任何方法标记为可测试的。
  如下MyTest测试用例里定义了8个方法,这8个方法没有太大的区别,其中4个方法使用@Testable注释来标记这些方法是可测试的。

 1 public class MyTest{
 2     //使用@Testable标记注释指定该方法是可测试的
 3     @Testable
 4     public static void m1(){
 5     }
 6     public static void m2(){
 7     }   
 8     //使用@Testable标记注释指定该方法是可测试的
 9     @Testable
10     public static void m3(){        
11         throw new RuntimeException("Boom");  
12     }
13     public static void m4(){
14     }       
15     //使用@Testable标记注释指定该方法是可测试的
16     @Testable
17     public static void m5(){
18     }  
19     public static void m6(){
20     }
21     //使用@Testable标记注释指定该方法是可测试的
22     @Testable
23     public static void m7(){            
24         throw new RuntimeException("Crash");   
25     }        
26     public static void m8(){
27     }
28 }

  如前所述,仅仅使用注释来标识程序元素对程序是不会有任何影响的,这也是Java注释的一条重要原则,为了让程序中这些注释起作用,我们必须为这些注释提供一个注释处理工具。
下面的注释处理工具会分析目标类,如果目标类中方法使用了@Testable注释修饰,则通过反射来运行该测试方法

 1 package chapter10;
 2 
 3 
 4 import java.lang.reflect.*;
 5 
 6 public class TestProcessor{
 7     public static void process(String clazz)
 8         throws ClassNotFoundException{
 9         int passed = 0;
10         int failed = 0;
11         //遍历obj对象的所有方法
12         for (Method m : Class.forName(clazz).getMethods()){
13             //如果包含@Testable标记注释
14             if (m.isAnnotationPresent(Testable.class)){
15                 try{
16                     //调用m方法
17                     m.invoke(null);
18                     //passed加1
19                     passed++;
20                 }
21                 catch (Exception ex){
22                     System.out.printf("方法" + m + "运行失败,异常:" + ex.getCause() + "\n");
23                     failed++; 
24                 }
25             }
26         }
27         //统计测试结果
28         System.out.printf("共运行了:" + (passed + failed)+ "个方法,其中:\n" + 
29             "失败了:" + failed + "个,\n" +  
30             "成功了:" + passed + "个!\n"); 
31     }
32 }

  TestProcessor类里只包含一个process方法,该方法可接受一个字符串参数,该方法将会分析clazz参数所代表的类,并运行该类里的、使用了@Testable注释修饰的方法。
  该程序的主类非常简单,提供主方法,使用TestProcessor来分析目标类即可。

 1 package chapter10;
 2 
 3 import java.lang.reflect.*; 
 4 
 5 public class RunTests{
 6     public static void main(String[] args) throws Exception{
 7         //处理MyTest类
 8         TestProcessor.process("MyTest");
 9     }
10 }

  通过这个运行结果可以看出,程序中的@Testable Annotation起作用了,MyTest类里以@Testable注释修饰的方法被正常测试了。
  通过上面例子可以看出,JDK的注释很简单,我们为源代码中添加一些特殊标记,这些特殊标记可通过反射获取,一旦程序访问到这些标记后,程序就可以做出相应的处理。

3 JDK的元Annotation                            

  JDK除了在java.lang下提供了3个基本Annotation之外,还在java.lang.annotation包下提供四个Meta Annotation(元Annotation),这四个Annotation都是用于修饰其他Annotation定义

3.1 使用@Retention           

  @Retention只能用于修饰一个Annotation定义,用于指定该Annotation可以保留多长时间,@Retention包含一个RetentionPolicy类型的value成员变量,所以使用@Retention时必须为该value成员变量指定值。
value成员变量的值只能是如下三个:
1.RetentionPolicy.CLASS:编译器将把注释记录在class文件中。当运行Java程序时,JVM不再保留注释,这是默认值
2.RetentionPolicy.RUNTIME:编译器把注释记录在class文件中。当运行Java程序时,JVM也会保留注释,程序可以通过反射获取该注释
3.RetentionPolicy.SOURCE:编译器直接丢弃这种策略的注释。
  在前面程序中因为我们要通过反射获取注释信息,所以我们制定value属性值为RetentionPolicy.RUNTIME。使用@Retention元数据Annotation可采用如下代码为value指定值:

//定义下面的Testable Annotation的保留到运行时
@Retention(value=RetentionPolicy.RUNTIME)
public @interface Testable{}

也可采用如下代码来为value指定值

//定义下面的Testable Annotation将被编译器直接丢弃
@Retention(RetentionPolicy.SOURCE)
public @interface Testable{}

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

3.2 使用@Target             

@Target也是用于修饰一个Annotation定义,它用于指定被修饰的Annotation能用于修饰哪些程序元素。@Target Annotation也包含一个名为value的成员变量,该成员变量的值只能是如下几个:
1.ElementType.ANNOTATION_TYPE:指定该策略的Annotation只能修饰Annotation
2.ElementType.CONSTRUCTOR:指定该策略的Annotation能修饰构造器
3.ElementType.FIELD:指定该策略的Annotation只能修饰成员变量
4.ElementType.LOCAL_VARIABLE:指定该策略的Annotation只能修饰具备变量
5.ElementType.METHOD:指定该策略的Annotation只能修饰方法定义
6.ElementType.PACKAGE:指定该策略的Annotation只能修饰包定义
7.ElementType.PARAMETER:指定该策略的Annotation可以修饰参数
8.ElementType.TYPE:指定该策略的Annotation可以修饰类、接口(包括注释类型)或枚举定义
  与使用@Retention类似的是,使用@Target也可以直接在括号里指定value值,可以无须使用name=value的形式。

3.3 使用@Documented   

@Documented用于指定该元Annotation修饰的Annotation类将被javadoc工具提成文档,如果定义Annotation类时使用了@Documented修饰,则所有使用该Annotation修饰的程序元素的API文档将会包含该Annotation说明。
下面代码定义了一个Testable Annotation,使用@Documented来修饰@Testable Annotation定义,所以该Annotation将被javadoc工具所提取。

 1 import java.lang.annotation.*;
 2 
 3 @Retention(RetentionPolicy.RUNTIME)    
 4 @Target(ElementType.METHOD)
 5 //定义Testable Annotation将被javadoc工具提取
 6 //@Documented 
 7 public @interface Testable{
 8 }
 9 所有使用@Testable Annotation的地方都会被javadoc工具提取到API文档中。
10 
11 public class MyTest{
12     // 使用@Test修饰info方法
13     @Testable
14     public void info(){
15         System.out.println("info方法...");
16     }
17 }
View Code

3.4 使用@Inherited        

  @Inherited元Annotation指定被它修饰的Annotation将具有继承性:如果某个类使用了A Annotation(定义该Annotation使用了@Inherited修饰)修饰,则其子类将自动具有A注释
  下面使用@Inherited元数据注释定义一个Inheritable Annotation,该Annotation将具有继承性。

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

}
View Code

  上面代码表明@Inheritable Annotation具有继承性,如果某个类使用了该Annotation修饰,则该类的子类将自动具有@Inheritable Annotation
下面程序定义一个Base基类,该基类使用了@Inheritable修饰,则Base类的子类将自动具有@Inheritable Annotation

//使用@Inheritable修饰的Base类
class Base{

}
//TestInheritable类只是继承了Base类
//并未直接使用@Inheritable Annotation修饰
public class TestInheritable extends Base{
    public static void main(String[] args){
        //打印TestInheritable类是否具有Inheritable Annotation
        System.out.println(TestInheritable.class.isAnnotationPresent(Inheritable.class))
    }
}
运行结果是:true,表面TestInheritable具有@Inheritable Annotation
View Code
posted @ 2013-07-30 22:09  朗道二级相变  阅读(490)  评论(0编辑  收藏  举报