根據(jù)經(jīng)驗(yàn),一般使用命令模式
表明有些程序元素需要通過某種工具或者框架進(jìn)行特殊處理橘茉。例如工腋,在Java4發(fā)行版本之前,JUnit測試框架原本要求用戶一定要用test作為測試方法名稱的開頭畅卓。這種方法可行擅腰,但是有幾個(gè)很嚴(yán)重的缺點(diǎn)。首先翁潘,文字拼寫錯(cuò)誤會導(dǎo)致失敗趁冈,且沒有任何提示。例如拜马,假設(shè)不小心將一個(gè)測試方法命名為tsetSafeyOverride而不是testSafeyOverride渗勘。JUnit3不會提示,但也不會執(zhí)行測試一膨,造成錯(cuò)誤的安全感呀邢。
命名模式的第二個(gè)缺點(diǎn)是,無法確保它們只用于相應(yīng)的程序元素上豹绪。例如价淌,假設(shè)將某個(gè)類稱作TestSafeyMechanisms申眼,是希望JUnit3會自動地測試它所有地方法,而不管它們叫什么名稱蝉衣。JUnit3還是不會提示括尸,但也同樣不會執(zhí)行測試。
命名模式的第三個(gè)缺點(diǎn)是病毡,它們沒有提供將參數(shù)值與程序元素關(guān)聯(lián)起來的好方法濒翻。例如,假設(shè)想要支持一種測試類別啦膜,它只在拋出特殊異常時(shí)才會成功有送。異常類型本質(zhì)上時(shí)測試的一個(gè)參數(shù)。你可以利用某種具體的命名模式僧家,將異常類型名稱編碼到測試方法中雀摘,但是這樣的代碼很不雅觀,也很脆弱(見第62條)八拱。編譯器不知道要去檢驗(yàn)準(zhǔn)備命名異常的字符串是否真正命名成功阵赠。如果命名的類不存在,或者不是一個(gè)異常肌稻,你也要到試著運(yùn)行測試時(shí)才會發(fā)現(xiàn)清蚀。
注解很好的解決了所有這些問題,JUnit從Java4開始使用爹谭。在本條目中枷邪,我們要編寫自己的試驗(yàn)測試框架,展示一下注解的使用方法旦棉。假設(shè)想要定義一個(gè)注解類型來指定簡單的測試齿风,它們自動運(yùn)行,并在拋出異常時(shí)失敗绑洛。以下就是這樣的一個(gè)注解類型救斑,命名為Test:
// Marker annotation type declaration
import java.lang.annotation.*;
/**
* Indicates that the annotated method is a test method. * Use only on parameterless static methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
Test注解類型的聲明就是它自身通過Retention和Target注解進(jìn)行了注解。注解類型聲明的這種注解被稱作元注解真屯。@Retention(RetentionPolicy.RUNTIME)
元注解表明Test注解在運(yùn)行時(shí)也應(yīng)該存在脸候,否則測試工具就無法知道Test注解。@Target(ElementType.METHOD)
元注解表明绑蔫,Test注解只在方法聲明中才是合法的:它不能運(yùn)用到類聲明运沦、域聲明或者其他程序元素上。
注意Test注解聲明上方的注釋:“User only on parameterless static method”(只用于無參的靜態(tài)方法)配深。如果編譯器能夠強(qiáng)制這一限制最好携添,但是它做不到,除非編寫一個(gè)注解處理器篓叶,讓它來完成烈掠。關(guān)于這個(gè)主題的更多信息羞秤,請參閱javax.annotation.processing的文檔。在沒有這類注解處理器的情況下左敌,如果將Test注解放在實(shí)例方法的聲明中瘾蛋,或者放在帶有一個(gè)或者多個(gè)參數(shù)的方法中,測試程序還是可以編譯矫限,讓測試工具運(yùn)行時(shí)來處理這個(gè)問題哺哼。
下面就是現(xiàn)實(shí)應(yīng)用中的Test注解,稱作標(biāo)記注解叼风,因?yàn)樗鼪]有參數(shù)取董,只是標(biāo)注被注解的元素豺妓。如果程序員拼錯(cuò)了Test版扩,或者Test注解應(yīng)用到程序元素而非方法聲明,程序就無法編譯:
// Program containing marker annotations
public class Sample {
@Test public static void m1() { } // Test should pass
public static void m2() { }
@Test public static void m3() { // Test should fail
throw new RuntimeException("Boom");
}
public static void m4() { }
@Test public void m5() { } // INVALID USE: nonstatic method
public static void m6() { }
@Test public static void m7() { // Test should fail
throw new RuntimeException("Crash");
}
public static void m8() { }
}
Sample類有7個(gè)靜態(tài)方法,其中4個(gè)被注解為測試懈贺。這4個(gè)中有2個(gè)拋出了異常:m3和m7,另外兩個(gè)則沒有:m1和m5坡垫。但是其中一個(gè)沒有拋出異常的被注解方法:m5梭灿,是一個(gè)實(shí)例方法,因此不屬于注解的有效使用冰悠”ざ剩總之,Sample包含4項(xiàng)測試:一項(xiàng)會通過溉卓,兩項(xiàng)會失敗皮迟,另一項(xiàng)無效。沒有用Test注解進(jìn)行標(biāo)注的另外4個(gè)方法會被測試工具忽略桑寨。
Test注解對Sample類的語義沒有直接的影響伏尼。它們只負(fù)責(zé)提供信息供相關(guān)的程序使用。更一般的講尉尾,注解永遠(yuǎn)不會改變被注解代碼的含義爆阶,但是使它可以通過工具進(jìn)行特殊的處理,例如像這種簡單的測試運(yùn)行類:
// Program to process marker annotations
import java.lang.reflect.*;
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch (Exception exc) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
}
}
測試運(yùn)行工具在命令行上使用完全匹配的類名沙咏,并通過調(diào)用Method.invoke反射的運(yùn)行的運(yùn)行類中所有標(biāo)注了Test注解的方法辨图。isAnnotationPresent方法告知該工具運(yùn)行哪些方法。如果測試方法拋出異常肢藐,反射機(jī)制就會將它封裝在InvocationTargetException中故河。該工具捕捉到這個(gè)異常,并打印失敗報(bào)告吆豹,包含測試方法拋出的原始異常鱼的,這些信息是通過getCause方法從InvocationTargetException中提取出來的杉女。
如果嘗試通過反射測試方法時(shí)拋出InvocationTargetException之外的任何異常。表明編譯時(shí)沒有捕捉到Test注解的無效用法鸳吸。這種用法包括實(shí)例方法的注解熏挎,或者帶有一個(gè)或多個(gè)參數(shù)的方法的注解,或者不可訪問的方法的注解晌砾。測試運(yùn)行類中的第二catch塊捕捉到這些Test用法錯(cuò)誤坎拐,并打印出相關(guān)的錯(cuò)誤消息。下面就是RunTests在Sample上運(yùn)行時(shí)打印的輸出:
public static void Sample.m3() failed: RuntimeException: Boom Invalid @Test: public void Sample.m5()
public static void Sample.m7() failed: RuntimeException: Crash Passed: 1, Failed: 3
現(xiàn)在我們要針對只在拋出特殊異常時(shí)才成功的測試添加支持养匈。為此需要一個(gè)新的注解類型:
// Annotation type with a parameter
import java.lang.annotation.*;
/**
* Indicates that the annotated method is a test method that * must throw the designated exception to succeed.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
這個(gè)注解的參數(shù)類型是Class<? extends Throwable>哼勇。這個(gè)通配符類型有點(diǎn)繞口。它在英語中的意思是:某個(gè)擴(kuò)展Throwable的類的Class對象呕乎,它允許注解的用戶指定任何異常(或錯(cuò)誤)類型积担。這種用法是有限制的類型令牌(詳見第33條)的第一個(gè)示例。下面就是實(shí)際應(yīng)用中的這個(gè)注解猬仁。注意類名稱被用作了注解參數(shù)的值:
// Program containing annotations with a parameter
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() { // Test should pass
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() { // Should fail (wrong exception)
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() { } // Should fail (no exception)
}
現(xiàn)在我們要修改一下測試運(yùn)行工具來處理新的注解帝璧。這其中包括將以下代碼添加到main方法中:
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (InvocationTargetException wrappedEx) {
Throwable exc = wrappedEx.getCause();
Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value();
if (excType.isInstance(exc)) {
passed++;
} else {
System.out.printf("Test %s failed: expected %s, got %s%n", m, excType.getName(), exc);
}
} catch (Exception exc) {
System.out.println("Invalid @Test: " + m);
}
}
這段代碼類似于用來處理Test注解的代碼,但有一處不同:這段代碼提取了注解參數(shù)的值湿刽,并用它檢驗(yàn)該測試拋出的異常是否為正確的類型的烁。沒有顯式的轉(zhuǎn)換,因此沒有出現(xiàn)ClassCastException的危險(xiǎn)诈闺。編譯過的測試程序確保它的注解參數(shù)表示的是有效的異常類型渴庆,需要提醒一點(diǎn):有可能注解參數(shù)在編譯時(shí)是有效的,但是表示特定異常類型的類文件在運(yùn)行時(shí)卻不存在雅镊。在這種希望很少出現(xiàn)的情況下襟雷,測試運(yùn)行類會拋出TypeNotPresenException異常。
將上面的異常測試示例再深入一點(diǎn)仁烹,想象測試可以在拋出任何一種指定異常時(shí)能夠通過耸弄。注解機(jī)制有一種工具,使得支持這種用法變得十分容易晃危。假設(shè)我們將ExceptionTest注解的參數(shù)類型改成Class對象的一個(gè)數(shù)組:
// Annotation type with an array parameter
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}
注解數(shù)組參數(shù)的語法十分靈活叙赚。它是進(jìn)行過優(yōu)化的單元素?cái)?shù)組。使用了ExceptionTest新版的數(shù)組參數(shù)之后僚饭,之前的所有ExceptionTest注解仍然有效震叮,并產(chǎn)生單元素的數(shù)組。為了指定多元素的數(shù)組鳍鸵,要用花括號將元素包圍起來苇瓣,并用逗號將它們隔開:
// Code containing an annotation with an array parameter
@ExceptionTest({
IndexOutOfBoundsException.class,
NullPointerException.class })
public static void doublyBad() {
List<String> list = new ArrayList<>();
// The spec permits this method to throw either
// IndexOutOfBoundsException or NullPointerException
list.addAll(5, null);
}
修改測試運(yùn)行工具來處理新的ExceptionTest相當(dāng)簡單。下面的代碼代替了原來的代碼:
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
Class<? extends Exception>[] excTypes = m.getAnnotation(ExceptionTest.class).value();
for (Class<? extends Exception> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("Test %s failed: %s %n", m, exc);
}
}
從Java8開始偿乖,還有另一種方法可以進(jìn)行多值注解击罪。它不是用一個(gè)數(shù)組參數(shù)聲明一個(gè)注解類型哲嘲,而是用@Repeatable元注解對注解的聲明進(jìn)行注解,表示該注解可以被重復(fù)的應(yīng)用個(gè)單個(gè)元素媳禁。這個(gè)元注解只有一個(gè)參數(shù)眠副,就是包含注解類型的類對象,它唯一的參數(shù)是一個(gè)注解類型數(shù)組竣稽。下面的注解聲明就是把ExceptionTest注解改成使用這個(gè)方法之后的版本囱怕。注意包含的注解類型必須利用適當(dāng)?shù)谋A舨呗院湍繕?biāo)進(jìn)行注解,否則聲明將無法編譯:
// Repeatable annotation type
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
下面是doublyBad測試方法用重復(fù)注解代替數(shù)組值注解之后的代碼:
// Code containing a repeated annotation
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() { ... }
處理可重復(fù)的注解要非常小心毫别。重復(fù)的注解會產(chǎn)生一個(gè)包含注解類型的合成注解娃弓。getAnnotationsByType方法掩蓋了這個(gè)事實(shí),可以用于訪問可重復(fù)注解類型的重復(fù)和非重復(fù)岛宦。但isAnnotationPresent使它變成了顯式的台丛,即重復(fù)的注解不是注解類型(而是所包含的注解類型)的一部分。如果一個(gè)元素具有某種類型的重復(fù)注解砾肺,并且用isAnnotationPresent方法檢驗(yàn)該元素是否具有該類型的注解挽霉,會發(fā)現(xiàn)它沒有。用這種方法檢驗(yàn)是否存在注解類型债沮,會導(dǎo)致程序默默的忽略掉重復(fù)的注解炼吴。同樣的,用這種方法檢驗(yàn)是否存在包含的注解類型疫衩,會導(dǎo)致程序默默的忽略掉非重復(fù)的注解
。為了利用isAnnotationPresent檢測重復(fù)和非重復(fù)的注解荣德,必須檢查注解類型及其包含的注解類型闷煤。下面是Runtests程序改成使用ExceptionTest注解時(shí)有關(guān)部分的代碼:
// Processing repeatable annotations
if (m.isAnnotationPresent(ExceptionTest.class) || m.isAnnotationPresent(ExceptionTestContainer.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
ExceptionTest[] excTests = m.getAnnotationsByType(ExceptionTest.class);
for (ExceptionTest excTest : excTests) {
if (excTest.value().isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed) System.out.printf("Test %s failed: %s %n", m, exc);
}
}
假如可重復(fù)的注解,提升了源代碼的可讀性涮瞻,邏輯上是將同一個(gè)注解類型的多個(gè)實(shí)例應(yīng)用到了一個(gè)指定的程序元素鲤拿。如果你覺得它們增強(qiáng)了源代碼的可讀性就是用它們,但是記住在聲明和處理可重復(fù)注解的代碼中會出現(xiàn)更多的樣板代碼署咽,并且處理可重復(fù)的代碼容易出錯(cuò)近顷。
本條目中的測試框架只是一個(gè)試驗(yàn)。但它清楚的示范了注解相對于命名模式的優(yōu)越性宁否。這只是揭開了注解功能的冰山一角窒升。如果是在編寫一個(gè)需要程序員給源文件添加信息的工具,就要定義一組適當(dāng)?shù)淖⒔忸愋汀?strong>既然有了注解慕匠,就完全沒有理由再使用命名模式了
饱须。
也就是說,除了“工具鐵匠”(toolsmiths台谊,即平臺框架程序員)之外蓉媳,大多數(shù)程序員都不必定義注解類型譬挚。但是所有的程序員都應(yīng)該使用Java平臺所提供的預(yù)定義的注解類型
(詳見第40條和第27條)。還要考慮使用IDE或者靜態(tài)分析工具所提供的任何注解酪呻。這種注解可以提升由這些工具所提供的診斷信息的質(zhì)量减宣。但是要注意這些注解還沒有標(biāo)準(zhǔn)化,因此如果變換工具或者形成標(biāo)準(zhǔn)玩荠,就有很多工作要做了蚪腋。