Java声明详解(Annotations)

时间:2022-09-25 12:24:25
很多API需要相当数量的样板代码. 例如, 为了写一个JAX-RPC Web服务, 你必须提供一个接口及其实现。
这些样板代码可以被工具自动生成,如果程序被声明修饰以便指出那些方法可以远程访问。

其他API要求在程序之外同时维护额外的配置文件。 例如avaBeans要求维护一个bean的同时一个BeanInfo
类被维护, EJB要求一个部署描述符。将这些额外文件中的信息,让程序使用声明自我维护将会更方便和不容出错


Java平台一直有各种ad hoc声明机制。 例如transient修饰符是一个ad hoc声明,其标识一个field应该
被序列化子系统忽略,@deprecated javadoc标签是一个ad hoc声明,其标识method不再被使用。
到Java 5, 平台有一个用于通用目的的声明(也叫metadata)设施,其允许你定义自己的声明类型。 此设施由
如下组成:
定义声明类型的语法; 使用声明类型的语法; 用于读取申明的API;代表声明的包java.lang.annotation
一个声明处理工具。

声明不直接影响程序的行为,但是 他们影响工具和类库对待程序的方式, 这倒反过来又影响运行程序的行为。
可以从源文件、类文件读取声明,也可以在运行时通过反射读取。

声明也补充了javadoc标签。通常,如果一个标记用于影响或者产生文档,那么他应该是一个javadoc标签或者
一个声明。

典型的应用程序从不必须定义一个声明类型,但是这样做也不困难。 定义声明类型和普通的接口定义类似。
在interface关键字前面加一个@。 每个方法定义定义了此申明类型的一个element。 方法定义必须没有任何
参数或throws子句,返回值类型被限制在原生类型、 String、Class、enums、annotations、 前面类型
的数组,可以有默认值。 如下是声明类型定义的一个例子:

/**
 * Describes the Request-For-Enhancement(RFE) that led
 * to the presence of the annotated API element.
 */
public @interface RequestForEnhancement {
    int    id();
    String synopsis();
    String engineer() default "[unassigned]";
    String date()    default "[unimplemented]";
}

一旦声明类型被定义, 你可以使用他注解其他的定义了。 一个声明是一个特定种类的修饰符, 可以使用在其他
修饰符(例如public、static、final)可以使用的任意地方。 按照惯例, 声明在其他修饰符之前。声明由
一个@后面跟一个声明类型和括号内的元素值对列表。 元素值必须是编译时常量。下面是一个方法定义,其带一个
使用上面定义的声明类型的注解:

@RequestForEnhancement(
    id       = 2868724,
    synopsis = "Enable time-travel",
    engineer = "Mr. Peabody",
    date     = "4/1/3007"
)
public static void travelThroughTime(Date destination) { ... }

没有element的声明类型被看作一个标记声明类型,例如:

/**
 * Indicates that the specification of the annotated API element
 * is preliminary and subject to change.
 */
public @interface Preliminary { }

允许省略标记声明的括号,如下所示:

@Preliminary public class TimeTravel { ... }

对于单个element的声明,element应该被命名为value, 如下所示:

/**
 * Associates a copyright notice with the annotated API element.
 */
public @interface Copyright {
    String value();
}

对于单个element并且element名为value的声明,在使用时允许省略element名和等号,如下所示:

@Copyright("2002 Yoyodyne Propulsion Systems")
public class OscillationOverthruster { ... }

为了展示这一切, 我们将构建一个简单的基于声明的测试框架。首选我们需要一个标记声明,用于标识一个方法
需要测试,应该被测试工具运行:

import java.lang.annotation.*;

/**
 * Indicates that the annotated method is a test method.
 * This annotation should be used only on parameterless static methods.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test { }

注意声明类型定义是自我注解的。这些声明被叫做元声明(meta-annotations)。 第一个
(@Retention(RetentionPolicy.RUNTIME))标识Test类型的注解被VM保留,因此在运行时可以通过反射
读。第二个(@Target(ElementType.METHOD))标识Test声明类型只能够注解方法定义。

下面是一个样例程序, 一些方法被上面的接口注解。

public class Foo {
    @Test public static void m1() { }
    public static void m2() { }
    @Test public static void m3() {
        throw new RuntimeException("Boom");
    }
    public static void m4() { }
    @Test public static void m5() { }
    public static void m6() { }
    @Test public static void m7() {
        throw new RuntimeException("Crash");
    }
    public static void m8() { }
}

下面是一个测试工具:

import java.lang.reflect.*;

public class RunTests {
   public static void main(String[] args) throws Exception {
      int passed = 0, failed = 0;
      for (Method m : Class.forName(args[0]).getMethods()) {
          if (m.isAnnotationPresent(Test.class)) {
            try {
               m.invoke(null);
               passed++;
            } catch (Throwable ex) {
               System.out.printf("Test %s failed: %s %n", m, ex.getCause());
               failed++;
            }
         }
      }
      System.out.printf("Passed: %d, Failed %d%n", passed, failed);
   }
}

此测试工具使用一个类名作为其命令行参数,遍历类中的所有方法,试图调用每个被Test声明注解的方法。
使用反射查询方式找出是否一方法有Test声明被高亮显示。如果一个被调用的方法抛出异常,那么此测试被看成
失败,一个失败报告被打印。最后一个概括被打印,显示成功和失败的测试数量。下面是用Foo类调用测试工具的
显示结果:

$ java RunTests Foo
Test public static void Foo.m3() failed: java.lang.RuntimeException: Boom
Test public static void Foo.m7() failed: java.lang.RuntimeException: Crash
Passed: 2, Failed 2

虽然这个测试工具明显是一个玩具,但是他演示了声明的威力并且可以很容易地扩展,以克服其局限性。