Annotation提供了一种为程序元素设置元数据的方法,从某些方面来看,Annotation就像修饰符一样,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被存储在Annotation的“name=value”对中。
Annotation是一个接口,程序可以通过反射来获取指定程序元素的Annotation对象,然后通过Annotation对象来取得注解里的元数据。
Annotation能被用来为程序元素(类、方法、成员变量等)设置元数据。值得指出的是,Annotation不影响程序代码的执行,无论增加、删除Annotation,代码都始终如一的执行。如果希望让程序中Annotation在运行时起一定的作用,只有通过某种配套的工具对Annotation中的信息进行访问和处理,访问和处理Annotation的工具统称APT(Annotation Processing Tool)。
1、基本Annotation
Annotation必须使用工具来处理,工具负责提取Annotation里包含的元数据,工具还会根据这些元数据增加额外的功能。Java提供了5个基本Annotation:
@Override
@Deprecated
@SupressWarnings
@SafeVarargs
@FunctionalInterface
上面5个基本Annotation中的@SafeVarargs是Java7新增的、@FunctionalInterface是Java8新增的。这5个基本的Annotation都定义在java.lang包下,可通过查阅API了解更多细节。
Java8的函数式接口与@FunctionalInterface
Java8规定:如果接口中只有一个抽象方法(可以包含多个默认方法或多个static方法),该接口就是函数式接口。@FunctionalInterface就是用来指定某个接口必须是函数式接口。
函数式接口就是为Java8的Lambda表达式准备的,Java8允许使用Lambda表达式创建函数式接口的实例,因此Java8专门增加了@FunctionalInterface。
@FunctionalInterface public interface FunInterface { static void Foo() { System.out.println("foo类方法"); } default void bar() { System.out.println("bar默认方法"); } void test(); //只定义一个抽象方法 }
@FunctionalInterface只是告诉编译器检查这个接口,保证该接口只能包含一个抽象方法(抽象方法只能有一个,否则会报错),否则就会编译出错。
@FunctionalInterface只能修饰接口,不能修饰其他程序元素。
2、JDK的元Annotation
JDK除了在java.lang下提供了5个基本的Annotation之外,还在java.lang.annotation包下提供了6个Meta Annotation(元Annotation),其中有5个元Annotation都用于修饰其他的Annotation定义。其中@Repeatable专门用于定义Java8新增的重复注解。
使用@Inherited
@Inherited元Annotation指定被它修饰的Annotation将具有继承性--如果某个类使用了@Xxx注解(定义该Annotation时使用了@Inherited修饰)修饰,则其子类将自动被@Xxx修饰。
下面使用@Inherited元Annotation修饰@Inheritable定义,则该Annotation将具有继承性。
import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface Inheritable { }
如果某个类使用了@Inheritable修饰,则该类的子类将自动使用@Inheritable修饰。
@Inheritable class Base { } public class InheritableTest extends Base{ public static void main(String[] args) { System.out.println(InheritableTest.class.isAnnotationPresent(Inheritable.class)); } }
Base类使用了@Inheritable修饰,而该Annotation具有继承性,所以其子类也将自动使用@Inheritable修饰。运行上面程序,会看到输出:true.
如果将InheritableTest.java程序中的粗体字代码注释掉或者删除,将会导致@Inheritable不具有继承性。运行上面程序,将看到输出:false。
3、自定义Annotation
定义新的Annotation类型使用@interface关键字(在原有的interface关键字前增加@符号)定义一个新的Annotation类型与定义一个接口非常像,如下代码可定义一个简单的Annotation类型。
//定义一个简单的Annotation类型 public @interface Test { }
定义了该Annotation之后,可以在程序的任何地方使用该Annotation,使用Annotation的语法非常类似于public、final这样的修饰符,通常可用于修饰程序中的类、方法、变量、接口等定义。通常会把Annotation放在所有修饰符之前,而且由于使用Annotation时可能还需要为成员变量指定值,因而Annotation的长度可能较长,所以通常把Annotation另放一行,如下程序所示:
@Test public class MyClass { ... }
Annotation还可以带成员变量,Annotation的成员变量在Annotation定义中以无形参的方法形式来声明,其方法名和返回值定义了该成员变量的名字和类型。如:
public @interface MyTag { //定义带两个成员变量的Annotation //Annotation中的成员变量以方法的形式来定义 String name(); int age(); }
一旦在Annotation里定义了成员变量之后,使用该Annotation时就应该为该Annotation的成员变量指定值。如:
public class Test { //使用带成员变量的Annotation时,需要为成员变量赋值 @MyTag(name="xx", age=6) public void info() { } }
也可以在定义Annotation的成员变量时为其指定初始值(默认值),指定成员变量的初始值可使用default关键字。
public @interface MyTag { //定义带两个成员变量的Annotation //Annotation中的成员变量以方法的形式来定义 String name() default "Jason"; int age() default 26; }
根据Annotation是否可以包含成员变量,可以把Annotation分为如下两类:
标记Annotation:没有定义成员变量的Annotation类型被称为标记。这种Annotation仅利用自身的存在与否来提供信息,如@Override。
元数据Annotation:包含成员变量的Annotation,因为它们可以接受更多的元数据,所以也被称为元数据Annotation。
4、提取Annotation信息
使用Annotation修饰了类、方法、成员变量等成员之后,这些Annotation不会自己生效,必须由开发者提供相应的工具来提取并处理Annotation信息。
Java使用Annotation接口来代表程序元素签名的注解,该接口是所有注解的父接口。
java.lang.reflect包下主要包含一些实现反射功能的工具类,从Java5开始,java.lang.reflect包所提供的反射API增加了读取运行时Annotation的能力。只有当定义Annotation时使用了@Retention(RetentionPolicy.RUNTIME)修饰,该Annotation才会在运行时可见,JVM才会在装载*.class文件时读取保存在class文件中的Annotation。
AnnotatedElement接口是所有程序元素(如Class、Method、Constructor等)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象(如Class、Method、Constructor等)之后,程序就可以调用该对象的几个方法来访问Annotation信息。
5、使用Annotation的示例
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) //定义一个标记注解,不包含任何成员变量,即不可传入元数据 public @interface Testable { }
@Retention注解指定Testable注解可以保留到运行时(JVM可以提取到该Annotation的信息),而@Target注解指定@Testable只能修饰方法。
package com.turing.annotation.test; public class MyTest { @Testable public static void m1() { } public static void m2() { } @Testable public static void m3() { throw new IllegalArgumentException("参数出错了!"); } public static void m4() { } @Testable public static void m5() { } public static void m6() { } @Testable public static void m7() { throw new RuntimeException("程序业务出现异常!"); } public static void m8() { } }
仅仅使用注解来标记程序元素对程序是不会有任何影响的,这也是Java注解的一条重要原则。为了让程序中的这些注解起作用,接下来必须为这些注解提供一个注解处理工具。
import java.lang.reflect.Method; public class ProcessorTest { public static void process(String clazz) throws SecurityException, ClassNotFoundException { int passed = 0; int failed = 0; // 遍历clazz对应的类里的所有方法 for (Method m : Class.forName(clazz).getMethods()) { // 如果该方法使用了@Testable修饰 if (m.isAnnotationPresent(Testable.class)) { try { m.invoke(null); passed++; } catch (Exception ex) { System.out.println("方法" + m + "运行失败,异常:" + ex.getCause()); failed++; } } } // 统计测试结果 System.out.println( "共运行了: " + (passed + failed) + "个方法,其中: " + "失败了:" + failed + "个, " + "成功了: " + passed + "个!"); } }
主程序:
public class RunTests { public static void main(String[] args) { try { ProcessorTest.process("com.turing.annotation.test.MyTest"); } catch (SecurityException | ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
Outputs:
方法public static void com.turing.annotation.test.MyTest.m3()运行失败,异常:java.lang.IllegalArgumentException: 参数出错了! 方法public static void com.turing.annotation.test.MyTest.m7()运行失败,异常:java.lang.RuntimeException: 程序业务出现异常! 共运行了: 4个方法,其中: 失败了:2个, 成功了: 2个!