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
为了获取程序中程序元素如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 }
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{ }
上面代码表明@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