许多API都需要为数不少的“模板式”的代码,比如,如果想写一个JAX-RPC(JAX-RPC即Java API for XML-Based RPC-译注)的web service,你需要提供一对接口及其实现程序。如果程序本身能被表明哪些方法可以由远程访问的注释所“修饰”的话,那么这种“模板式”的代码就可以用工具自动生成。
另一些API需要在维护程序的同时维护一些“辅助”文件,比如,维护JavaBean的同时需要维护BeanInfo类,Enterprise JavaBeans (EJB) 则需要部署说明档(deployment descriptor)。如果这些辅助信息能在程序中作为注解来维护,就会方便地多,并且也不容易犯错。
Java平台已经提供了几种不同的注解机制。比如transient
修饰符就是一种特别的注解,标明被修饰的字段在序列化的子系统应该被忽略,@deprecated 作为javadoc的特殊标签,标明该方法不应该再用。在5.0版本中,Java平台提供了一种一般性的注解(即元数据metadata)机制,允许你自定义注解类型和使用该类型。该机制包含注解的定义语法、声明语法、读取注解的API、注解的代理类文件以及注解的一个处理工具。
注解并不直接对程序语义产生影响,但是会对工具或库在处理程序的方式上时产生影响,进而影响程序在运行时的语义。注解可以从源文件或类文件中读取,甚至在运行时以反射方式读取。
注解是对javadoc标签的补充。一般而言,如果标注的目的是影响或产生文档,那么就该用javadoc标签,否则就用注解。典型的应用程序的程序员永远不必自己定义一个注解类型,但其实真要做也不难。注解类型的定义跟接口定义相似,只是需要在inteface关键字前面加一个at符(@),声明一个方法即为注解类型定义一个元素。方法声明时不允许有参数或throw语句,返回值类型被限定为原始数据类型、字符串String、Class、枚举enums、注解类型,或前面这些的数组,方法可以有默认值,下面是一个定义注解类型的例子:
/** * 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) { ... }
注解类型如果不含任何元素,被称为“标记”注解类型,如:
/** * Indicates that the specification of the annotated API element * is preliminary and subject to change. */ public @interface Preliminary { }
“标记”注解类型允许忽略括号,如下:
@Preliminary public class TimeTravel { ... }
如果注解只有一个元素,那么该元素应该被命名为value,如下:
/**
* Associates a copyright notice with the annotated API element.
*/
public @interface Copyright {
String value();
}
如果注解只有一个元素,并且元素名为value,那么元素名和等号(=)都可以忽略不写,如下:
@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)
) 表示注解由虚拟机保留,可以在运行时通过反射读取;第二个(@Target(ElementType.METHOD)
) 表示注解只能用在方法上。
下面是一个例子程序,其中的一些方法由上面所定义的注解来修饰:
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
这个测试工具无疑是个玩具程序,但它表明了注解的强大,并能很容易的扩展为一个有用的工具。