美文网首页Effective Java
第35条:注解优先于命名模式

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

作者: 郭_4d5f | 来源:发表于2017-06-19 14:31 被阅读0次

    命名模式的缺点:
    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 )是用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面等工具) 或者静态分析工具所提供的任何注解 . 这种注解可以提升由这些工具所提供的诊断信息的质量 . 但是要注意这些注解还没有标准化 , 因此如果变换工具或者形成标准 , 就需要做更多地工作 .

    相关文章

      网友评论

        本文标题:第35条:注解优先于命名模式

        本文链接:https://www.haomeiwen.com/subject/yibgqxtx.html