Effective Java

第35条:注解优先于命名模式

2017-06-19  本文已影响0人  郭_4d5f

命名模式的缺点:
1.文字拼写错误导致失败,测试方法没有执行,也没有报错 (JUNIT测试框架测试的方法要用test开头)
2.无法确保它们只用于相应的程序元素上,如希望一个类的所有方法被测试,把类命名为test开头,但JUnit不支持类级的测试,只在test开头的方法中生效
3.没有提供将参数值与程序元素关联起来的好方法。想要支持一种测试类别,它只在抛出特殊异常时才会成功。异常类型本质是测试的一个参数,如果命名类不存在,或者不是一个异常,你只有通过运行后才能发现。
注解能解决命名模式存在的问题,下面定义一个注解类型指定简单的测试,它们自动运行,并在抛出异常时失败(注意,下面的Test注解是自定义的,不是JUnit的实现)

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;


@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface Test {
   
}

像test使用了Retention和Target 这两种注解,这种注解被称为元注解
@Retention(RetentionPolicy.RUNTIME)表明Test注解在运行时保留,如果没有保留,测试工具无法知道Test注解
@Target(ElementType.METHOD)表明只有在方法声明中Test注解才是合法的,它不能运用到类声明,域声明或者其他程序元素上。
use only on parameterless static method (只用于无参的静态方法),但是编译器并不能做到对参数进行限制,如果将Test注解放在实例方法中,或者放在带有一个或者多个的方法中,测试程序还是不会编译错误,只能让测试工具运行的时候进行处理

下面的Sample类使用Test注解,如果拼错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() {
 thrownew  RuntimeException("Crash");
}
public  static  void  m8() {
}
}

在Sample 中有八个方法(其中m5不是静态方法),四个被注解为测试的方法中,有两个抛出异常:m3和m7,另外两个没有:m1和m5,被注解方法m5是一个实例方法,因此不属于注解的有效使用。没有进行标记的方法则会被测试工具忽略
test注解对Sample类的语义没有直接影响,只负责提供信息供相关程序使用。也就是注解不会改变被注解代码的语义,但是它可以通过工具进行特殊的处理。比如咱们用注解对方法进行简单的测试。
测试Sample的测试运行类:

public class RunTests {

    public static void main(String[] args) throws Exception {

        int tests = 0;

        int passed = 0;

        Class testClass = Class.forName("service.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.printf("Passed: %d, Failed: %d%n", passed, tests - passed);

    }

}

测试运行工具在命令行上使用完全匹配的类名,并通过调用Method.invoke反射式的运行类中所有标注了test的方法,isAnnotationPresent 方法告知工具要运行哪些方法。如果测试方法抛出异常反射机制就会将错误信息封装到InvocationTargetException中,该工具捕捉到了这个异常,并且打印失败报告,包含测试方法抛出的原始异常,这些信息是通过getCasuse方法从InvocationTargetException中提取出来的
如果尝试通过反射调用测试方法时抛出InvocationTargetException之外的任何异常,表面编译的时候没有捕捉到Test注解的无效用法,这种用法包括实例方法的注解,或者带一个或者多个参数的方法的注解,并且打印相应的错误信息
运行结果:

public   static   void   Sample.m3()
 failed: java.lang.RuntimeException: Boom
INVALID @Test:public void  Sample.m5()
public  static   void  Sample.m7()  
failed: java.lang.RuntimeException: Crash
Passed:1, Failed: 3

针对只有在抛出特殊异常才成功的注解:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class <? extends Exception>value();
}
Sample --
public class Sample1 {

    @ExceptionTest(ArithmeticException.class)
    public static  void  m1() {
    
    }

    public static void m2() {
    
    }
    
    @ExceptionTest(ArithmeticException.class)
    public static void  m3() {
    
        throw new   RuntimeException("Boom");
    
    }

    public static void   m4() {
    
    }
    @ExceptionTest(ArithmeticException.class)
    public  void  m5() {
    
    }
    
    public  static  void  m6() {
    
    }
    @ExceptionTest(ArithmeticException.class)
    public  static  void  m7() {
    
        throw new  RuntimeException("Crash");
    
    }
    
    public  static  void  m8() {

}

这段代码类似于用来处理Test注解的代码,但有一处不同:这段代码提取了注解参数的值,并用它检验该测试抛出的异常是否是正确的类型。没有显示的转换,因此没有出现类型转换异常的危险,编译过的测试程序确保它的注解参数表示的是有效的异常类型,需要提醒一点:有可能注解参数参数在编译时是有效的,但是表示特定异常类型的类文件在运行时却不再存在,在这种希望很少出现的情况下,测试运行类会抛出TypeNotPresentException异常。
将上面的异常测试示例再深入一点,测试可以抛出任何一种指定异常时都得到通过。我们将exceptionTest注解的参数类型改为Class对象的一个数组:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)
public @interface ExceptionTest1 {
    Class <? extends Exception> [] value();
}

注解中数组参数的语法十分灵活。它是进行过优化的单元数组。使用了ExceptionTest新版的数组参数之后,之前的所有的ExceptionTest注解依然有效,并产生单元素包围起来,为了指定多元素的数组,需要用({})将元素保卫起来,并且用{,}隔开

public class Sample2 {

    @ExceptionTest1({ArithmeticException.class,NullPointerException.class})
    public static  void  m1() {
    
    }

    public static void m2() {
    
    }
    
    @ExceptionTest1({ArithmeticException.class,NullPointerException.class})
    public static void  m3() {
    
        throw new   RuntimeException("Boom");
    
    }

    public static void   m4() {
    
    }
    @ExceptionTest1({ArithmeticException.class,NullPointerException.class})
    public  void  m5() {
    
    }
    
    public  static  void  m6() {
    
    }
    @ExceptionTest1({ArithmeticException.class,NullPointerException.class})
    public  static  void  m7() {
    
        throw new  RuntimeException("Crash");
    
    }
    
    public  static  void  m8() {

}

}
public class RunTests2 {

    public static void main(String[] args) throws Exception {

        int tests = 0;

        int passed = 0;

        Class testClass = Class.forName("service.Sample2");

        for (Method m : testClass.getDeclaredMethods()) {

            if (m.isAnnotationPresent(ExceptionTest1.class)) {
                tests++;

                try { //反射式的运行所有标注了Test的方法

                    m.invoke(null);

                } catch (InvocationTargetException e) {
                    //InvocationTargetException异常由Method.invoke(obj, args...)方法抛出。当被调用的方法的内部抛出了异常而没有被捕获时,将由此异常接收。
                    Throwable exc = e.getCause();
                    Class[] excTypes = m.getAnnotation(ExceptionTest1.class).value();
                    int oldPassed = passed;
                    for (Class excType : excTypes) {
                        if (excType.isInstance(exc)) {
                            passed++;
                            break;
                        }
                    }
                    if (passed == oldPassed) {
                        System.out.printf("测试%s失败:%s %n", m, exc);
                    }

                } catch (Exception e) {

                    System.out.println("Invalid @Test: " + m);

                }

            }

        }

        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);

    }

}

以上的例子不揭露了注解的冰山一角 , 但它鲜明了表达了一个观点 , 既然有了注解 , 就不必再用命名模式了
总结:除了特定的程序员之外 , 大多数程序员都不必定义注解类型 . 但是所有的程序员都应该使用Java平台所提供的预定义的注解类型 . 还要考虑 IDE(集成开发环境(IDE,Integrated Development Environment )是用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面等工具) 或者静态分析工具所提供的任何注解 . 这种注解可以提升由这些工具所提供的诊断信息的质量 . 但是要注意这些注解还没有标准化 , 因此如果变换工具或者形成标准 , 就需要做更多地工作 .

上一篇下一篇

猜你喜欢

热点阅读