許多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
這個測試工具無疑是個玩具程序,但它表明了注解的強大,並能很容易的擴展為一個有用的工具。