java注解类型命名_第三十九条:注解优先于命名模式
根據經驗,一般使用命令模式表明有些程序元素需要通過某種工具或者框架進行特殊處理。例如,在Java4發行版本之前,JUnit測試框架原本要求用戶一定要用test作為測試方法名稱的開頭。這種方法可行,但是有幾個很嚴重的缺點。首先,文字拼寫錯誤會導致失敗,且沒有任何提示。例如,假設不小心將一個測試方法命名為tsetSafeyOverride而不是testSafeyOverride。JUnit3不會提示,但也不會執行測試,造成錯誤的安全感。
命名模式的第二個缺點是,無法確保它們只用于相應的程序元素上。例如,假設將某個類稱作TestSafeyMechanisms,是希望JUnit3會自動地測試它所有地方法,而不管它們叫什么名稱。JUnit3還是不會提示,但也同樣不會執行測試。
命名模式的第三個缺點是,它們沒有提供將參數值與程序元素關聯起來的好方法。例如,假設想要支持一種測試類別,它只在拋出特殊異常時才會成功。異常類型本質上時測試的一個參數。你可以利用某種具體的命名模式,將異常類型名稱編碼到測試方法中,但是這樣的代碼很不雅觀,也很脆弱(見第62條)。編譯器不知道要去檢驗準備命名異常的字符串是否真正命名成功。如果命名的類不存在,或者不是一個異常,你也要到試著運行測試時才會發現。
注解很好的解決了所有這些問題,JUnit從Java4開始使用。在本條目中,我們要編寫自己的試驗測試框架,展示一下注解的使用方法。假設想要定義一個注解類型來指定簡單的測試,它們自動運行,并在拋出異常時失敗。以下就是這樣的一個注解類型,命名為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注解進行了注解。注解類型聲明的這種注解被稱作元注解。@Retention(RetentionPolicy.RUNTIME)元注解表明Test注解在運行時也應該存在,否則測試工具就無法知道Test注解。@Target(ElementType.METHOD)元注解表明,Test注解只在方法聲明中才是合法的:它不能運用到類聲明、域聲明或者其他程序元素上。
注意Test注解聲明上方的注釋:“User only on parameterless static method”(只用于無參的靜態方法)。如果編譯器能夠強制這一限制最好,但是它做不到,除非編寫一個注解處理器,讓它來完成。關于這個主題的更多信息,請參閱javax.annotation.processing的文檔。在沒有這類注解處理器的情況下,如果將Test注解放在實例方法的聲明中,或者放在帶有一個或者多個參數的方法中,測試程序還是可以編譯,讓測試工具運行時來處理這個問題。
下面就是現實應用中的Test注解,稱作標記注解,因為它沒有參數,只是標注被注解的元素。如果程序員拼錯了Test,或者Test注解應用到程序元素而非方法聲明,程序就無法編譯:
// 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個靜態方法,其中4個被注解為測試。這4個中有2個拋出了異常:m3和m7,另外兩個則沒有:m1和m5。但是其中一個沒有拋出異常的被注解方法:m5,是一個實例方法,因此不屬于注解的有效使用。總之,Sample包含4項測試:一項會通過,兩項會失敗,另一項無效。沒有用Test注解進行標注的另外4個方法會被測試工具忽略。
Test注解對Sample類的語義沒有直接的影響。它們只負責提供信息供相關的程序使用。更一般的講,注解永遠不會改變被注解代碼的含義,但是使它可以通過工具進行特殊的處理,例如像這種簡單的測試運行類:
// 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);
}
}
測試運行工具在命令行上使用完全匹配的類名,并通過調用Method.invoke反射的運行的運行類中所有標注了Test注解的方法。isAnnotationPresent方法告知該工具運行哪些方法。如果測試方法拋出異常,反射機制就會將它封裝在InvocationTargetException中。該工具捕捉到這個異常,并打印失敗報告,包含測試方法拋出的原始異常,這些信息是通過getCause方法從InvocationTargetException中提取出來的。
如果嘗試通過反射測試方法時拋出InvocationTargetException之外的任何異常。表明編譯時沒有捕捉到Test注解的無效用法。這種用法包括實例方法的注解,或者帶有一個或多個參數的方法的注解,或者不可訪問的方法的注解。測試運行類中的第二catch塊捕捉到這些Test用法錯誤,并打印出相關的錯誤消息。下面就是RunTests在Sample上運行時打印的輸出:
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
現在我們要針對只在拋出特殊異常時才成功的測試添加支持。為此需要一個新的注解類型:
// 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();
}
這個注解的參數類型是Class extends Throwable>。這個通配符類型有點繞口。它在英語中的意思是:某個擴展Throwable的類的Class對象,它允許注解的用戶指定任何異常(或錯誤)類型。這種用法是有限制的類型令牌(詳見第33條)的第一個示例。下面就是實際應用中的這個注解。注意類名稱被用作了注解參數的值:
// 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)
}
現在我們要修改一下測試運行工具來處理新的注解。這其中包括將以下代碼添加到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注解的代碼,但有一處不同:這段代碼提取了注解參數的值,并用它檢驗該測試拋出的異常是否為正確的類型。沒有顯式的轉換,因此沒有出現ClassCastException的危險。編譯過的測試程序確保它的注解參數表示的是有效的異常類型,需要提醒一點:有可能注解參數在編譯時是有效的,但是表示特定異常類型的類文件在運行時卻不存在。在這種希望很少出現的情況下,測試運行類會拋出TypeNotPresenException異常。
將上面的異常測試示例再深入一點,想象測試可以在拋出任何一種指定異常時能夠通過。注解機制有一種工具,使得支持這種用法變得十分容易。假設我們將ExceptionTest注解的參數類型改成Class對象的一個數組:
// Annotation type with an array parameter
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class extends Exception>[] value();
}
注解數組參數的語法十分靈活。它是進行過優化的單元素數組。使用了ExceptionTest新版的數組參數之后,之前的所有ExceptionTest注解仍然有效,并產生單元素的數組。為了指定多元素的數組,要用花括號將元素包圍起來,并用逗號將它們隔開:
// Code containing an annotation with an array parameter
@ExceptionTest({
IndexOutOfBoundsException.class,
NullPointerException.class })
public static void doublyBad() {
List list = new ArrayList<>();
// The spec permits this method to throw either
// IndexOutOfBoundsException or NullPointerException
list.addAll(5, null);
}
修改測試運行工具來處理新的ExceptionTest相當簡單。下面的代碼代替了原來的代碼:
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開始,還有另一種方法可以進行多值注解。它不是用一個數組參數聲明一個注解類型,而是用@Repeatable元注解對注解的聲明進行注解,表示該注解可以被重復的應用個單個元素。這個元注解只有一個參數,就是包含注解類型的類對象,它唯一的參數是一個注解類型數組。下面的注解聲明就是把ExceptionTest注解改成使用這個方法之后的版本。注意包含的注解類型必須利用適當的保留策略和目標進行注解,否則聲明將無法編譯:
// 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測試方法用重復注解代替數組值注解之后的代碼:
// Code containing a repeated annotation
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() { ... }
處理可重復的注解要非常小心。重復的注解會產生一個包含注解類型的合成注解。getAnnotationsByType方法掩蓋了這個事實,可以用于訪問可重復注解類型的重復和非重復。但isAnnotationPresent使它變成了顯式的,即重復的注解不是注解類型(而是所包含的注解類型)的一部分。如果一個元素具有某種類型的重復注解,并且用isAnnotationPresent方法檢驗該元素是否具有該類型的注解,會發現它沒有。用這種方法檢驗是否存在注解類型,會導致程序默默的忽略掉重復的注解。同樣的,用這種方法檢驗是否存在包含的注解類型,會導致程序默默的忽略掉非重復的注解。為了利用isAnnotationPresent檢測重復和非重復的注解,必須檢查注解類型及其包含的注解類型。下面是Runtests程序改成使用ExceptionTest注解時有關部分的代碼:
// 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);
}
}
假如可重復的注解,提升了源代碼的可讀性,邏輯上是將同一個注解類型的多個實例應用到了一個指定的程序元素。如果你覺得它們增強了源代碼的可讀性就是用它們,但是記住在聲明和處理可重復注解的代碼中會出現更多的樣板代碼,并且處理可重復的代碼容易出錯。
本條目中的測試框架只是一個試驗。但它清楚的示范了注解相對于命名模式的優越性。這只是揭開了注解功能的冰山一角。如果是在編寫一個需要程序員給源文件添加信息的工具,就要定義一組適當的注解類型。既然有了注解,就完全沒有理由再使用命名模式了。
也就是說,除了“工具鐵匠”(toolsmiths,即平臺框架程序員)之外,大多數程序員都不必定義注解類型。但是所有的程序員都應該使用Java平臺所提供的預定義的注解類型(詳見第40條和第27條)。還要考慮使用IDE或者靜態分析工具所提供的任何注解。這種注解可以提升由這些工具所提供的診斷信息的質量。但是要注意這些注解還沒有標準化,因此如果變換工具或者形成標準,就有很多工作要做了。
總結
以上是生活随笔為你收集整理的java注解类型命名_第三十九条:注解优先于命名模式的全部內容,希望文章能夠幫你解決所遇到的問題。