TIP 35 注解优先于命名模式
命名模式的问题
- 文字拼写错误会导致失败,且没有任何提示。
- 无法确保它们只用于相应的程序元素上。
- 它们没有提供将参数值与程序元素关联起来的好方法。
注解可以很好地解决这些问题。假设想要定义个注解类型来指定简单的测试,它们自动运行,并在抛出异常时失败:
/**
* 只能用于无参的静态方法.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
Test注解类型的声明就是它自身通过Retention和Target注解进行了注解。注解类型声明中的这种注解被成为元注解(meta-annotation)。
@Retention(RetentionPolicy.RUNTIME)
表明Test注解应该在运行时保留。如果没有保留,测试工具就无法知道Test注解。
@Target(ElementType.METHOD)
表明,Test注解只在方法声明中才是合法的。
注释说明这个注解只能用于无参的静态方法。但是很遗憾,编译器不能强制这一限制。
下面就是现实应用中的Test注解:
public class Sample {
@Test
public static void m1(){}
public static void m2(){}
@Test
public static void m3(){
throw new RuntimeException("Boom");
}
public static void m4(){}
@Test
public void m5(){}
public static void m6(){}
@Test
public static void m7(){
throw new RuntimeException("Crash");
}
public static void m8(){}
}
测试类:
public class RunTest {
public static void main(String args[]) throws Exception {
int tests = 0;
int passed = 0;
//请注意这里forName的参数,必须写上类的全名,包含包名
Class testClass = Class.forName("douvril.effect.tip34.Sample");
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 e){
System.out.println("INVALID @Test: "+m);
}
}
}
System.out.println("Passed:"+passed+" , Failed:"+(tests - passed));
}
}
运行结果:
public static void douvril.effect.tip34.Sample.m7() failed: java.lang.RuntimeException: Crash
public static void douvril.effect.tip34.Sample.m3() failed: java.lang.RuntimeException: Boom
INVALID @Test: public void douvril.effect.tip34.Sample.m5()
Passed:1 , Failed:3
可以看到只有一个方法通过了测试,就是m1方法。而Boom和Crash的则是m3和m7方法。
至于非法调用的m5,则是在调用时捕获到了Exception ,而非 InvocationTargetException —— 这表明方法内部并没有抛出异常。如果在捕获Exception的字句中,打印该异常信息则会发现其实是java.lang.NullPointerException
。
因为m5不是静态方法,所以调用时必须有一个Sample实例作为参数,而我们传入的是null ——– 而这正式Test声明的注释内容所要求的。
现在我们要针对只在抛出特殊异常时才成功的测试添加支持,为此声明一个新的注解类型:
/**
* 必须抛出一个指定的异常才算成功
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1(){
int i = 0;
i = i / i; //测试会通过,因为正好会抛出这个异常
}
@ExceptionTest(ArithmeticException.class)
public static void m2(){
int[] a = new int[0];
int i = a[1]; //测试会失败,因为抛出的是数组越界异常,不是指定的异常
}
@ExceptionTest(ArithmeticException.class)
public static void m3(){} //测试会失败,因为根本不会抛出任何异常
}
看看运行结果:
Test public static void douvril.effect.tip34.Sample2.m3() failed: no exception.
Test public static void douvril.effect.tip34.Sample2.m2() failed: expected java.lang.ArithmeticException , got java.lang.ArrayIndexOutOfBoundsException: 1
Passed:1 , Failed:2
代码工作正常。只有抛出@ExceptionTest(ArithmeticException.class)
指定的这个ArithmeticException
异常, 才算测试成功。
总结。
请仔细回味,最好亲自上手运行本条中的所有代码。它们清楚的展示了注解之余命名模式的优越性。
既然有了注解,就完全没有必要使用命名模式了。
除非是工具铁匠(toolsmiths – 特定的程序员) , 否则大多数程序员都不必定义注解类型。但是所有的程序员都应该使用Java平台所提供的预定义注解类型(参考TIP 36 和 TIP 24)。
还要考虑使用IDE或静态分析工具提供的任何注解。这种注解可以提升由这些工具所提供的诊断信息的质量。