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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,724评论 6 342
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,398评论 25 707
  • 凯文凯利的《必然》,在一年多前读了,当时很是震撼。最近“得到”分11期进行了解读。 重新来过后,里面的一些现象在现...
    不骛于虚声阅读 131评论 0 0