第三十九條:注解優(yōu)先于命名模式

根據(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)玩荠,就有很多工作要做了蚪腋。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市姨蟋,隨后出現(xiàn)的幾起案子屉凯,更是在濱河造成了極大的恐慌,老刑警劉巖眼溶,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件悠砚,死亡現(xiàn)場離奇詭異,居然都是意外死亡堂飞,警方通過查閱死者的電腦和手機(jī)灌旧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绰筛,“玉大人枢泰,你說我怎么就攤上這事÷霖” “怎么了衡蚂?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長骏庸。 經(jīng)常有香客問我毛甲,道長,這世上最難降的妖魔是什么具被? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任玻募,我火速辦了婚禮,結(jié)果婚禮上一姿,老公的妹妹穿的比我還像新娘七咧。我一直安慰自己,他們只是感情好叮叹,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布艾栋。 她就那樣靜靜地躺著,像睡著了一般衬横。 火紅的嫁衣襯著肌膚如雪裹粤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機(jī)與錄音遥诉,去河邊找鬼拇泣。 笑死,一個(gè)胖子當(dāng)著我的面吹牛矮锈,可吹牛的內(nèi)容都是我干的霉翔。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼苞笨,長吁一口氣:“原來是場噩夢啊……” “哼债朵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起瀑凝,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤序芦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后粤咪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谚中,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年寥枝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宪塔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡囊拜,死狀恐怖某筐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情冠跷,我是刑警寧澤南誊,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站蔽莱,受9級特大地震影響弟疆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜盗冷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望同廉。 院中可真熱鬧仪糖,春花似錦、人聲如沸迫肖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蟆湖。三九已至故爵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間隅津,已是汗流浹背诬垂。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工劲室, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人结窘。 一個(gè)月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓很洋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親隧枫。 傳聞我的和親對象是個(gè)殘疾皇子喉磁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容