Effective Java(3rd)-Item39 注解比命名
从历史上讲,通常使用 命名模式来指示某些程序元素需要工具或框架的特殊处理。比如在release4之前,Junit 测试框架需要它的使用者通过它们的test命名来代理测试 [Beck04]。这个技术工厂了,但是它有一些大缺点。首先排字错误会导致无声的失败。比如,假设你不小心将一个测试方法命名为tsetSafetyOverride而不是testSafetyOverride。Junit3不会编译,但特不会执行这个测试,这会导致安全上的错误。
命名模式的第二个缺点是没有办法来确保它们只用于合适的程序元素。比如,假设你叫一个类TestSafetyMechanisms希望JUnit3可以自动测试它的所有方法,无视它们的名字。还是那样,JUnit3将不会编译,也不会执行测试。
命名模式的第三个错误是它们没有提供将参数值与程序元素关联的好方法。比如,假设你想要支持一个测试的种类,只在抛出一个特殊的异常时测试才算成功。异常类型本质上是测试的一个参数。可以使用一些精细的命名模式将异常类型名称编码到测试方法名称中,但是这是丑陋的也是脆弱的(item62 )。编译器没有办法知道用于命名异常的字符串是否确实存在。如果命名类不存在或者不是异常,则在尝试运行测试之前不会被发现。
注解很好地解决了所有的这些问题,JUnit从第四版开始采用了它们。在这个item,我们将编写我们自己的玩具测试框架来显示注解是怎么工作的。假设你想要定义一个注解类型来代理简单的测试,并自动测试如果它们抛出异常则运行失败。这是一个注解类型的样子,叫作Test,可能长这个样子:
image.png
Test注解类型的声明本身带有Retention和Target注解。在注解类型声明的注解被叫作 元注解. @Retention(RetentionPolicy.RUNTIME)元注解指示了注解应该在运行时保留Test注解。没有它,Test注解将对测试工具不可见。@Target.get(ElementType.METHOD)元注解指示了Test注解只在方法声明中合法:它不能被用于类声明,字段声明或其他程序元素中。
Test注解声明之前的注释说了,“只在无参静态方法中使用”.如果编译器可以强制这一点,那就太好了,但是它不能,除非你编写一个 注释处理器来这么做。有关此主题的更多信息,可以查阅javax.annotation.processing的文档。在缺少这样的注解处理器的情况下,如果你将一个Test注解放在一个实例方法的声明上或在一个或多个参数的方法上,测试程序仍然会编译,留下测试工具在运行时间解决这个问题。
这是Test注解在实际上是长什么样子。它叫作 标记注释因为它没有参数只是简单地“标记”了被注解元素。如果程序员拼错了Test或将Test注解应用于一个不是方法声明的程序元素中,程序将编译失败。
Sample类有7个静态方法,四个被测试加上注解了。它们中的两个,m3和m7,抛出异常,两个,m1和m5不是。但是这些被注解的方法中的其中一个方法没有抛出异常,m5,因为它是一个实例方法啊,它在注解中不是有效的。总结,Sample包含了四个测试:一个成功,两个失败,一个是无效的。四个没有被Test注解注解的方法会被测试工具忽略。
Test注解对Sample类的语义没有直接的影响。它们只提供给感兴趣的项目使用的信息。更一般的是,注解没有改变被注解代码的语义但是可以被工具特殊对待,比如如下简单的测试runner:
image.png
测试运行器工具在命令行上使用一个完全限定的类名,并通过调用Method.Invode反射运行类的所有带有Test注解的方法。isAnnotationPresent方法告诉工具哪些方法可以运行。如果一个测试方法抛出异常,反射设施包装它为InvocationTargetException。工具捕获这个异常并打印失败报告包含测试方法的原始异常,提取自InvocationTargetException的getCause方法。
如果试图通过反射调用测试方法会抛出InvocationTargetException以外的异常,它表示在编译时未捕获的Test注解的无效使用。这些使用包括一个实例方法注解,或一个方法带上了一个或多个参数,或者一个不可访问的方法。测试runner的第二个捕获块捕获了这些测试使用错误并打印一个合适的错误消息。这是在Sample上运行RunTests会打印的错误:
image.png
现在,让我们添加如果抛出一个特定的错误测试就成功的功能。我们将需要一个新的注解类型,如下:
image.png
这个注解的参数类型是Class<? extends Throable>。 诚然,这个通配符类型是妙语。在英语中,这代表了“一些继承Throwable的类对象”,它允许注解的用户确保任何异常(或错误)类型。这个用法是 有界类型令牌的例子(item33)。如下是在实际上注解的样子。注意到类文本用作注释参数的值:
现在,让我们修改测试运行工具以处理新的注解把。这样做包括向主方法添加以下代码:
image.png
&emsp这串代码和我们处理Test注解的代码类似,有一个区别:这个代码提取注解参数的值,并使用它检查测试引发的异常是否为正确类型。没有显式强制类型转换,因此不会有ClassCastException的危险。所编译的测试程序确保其注解参数代表有效的异常类型,但有一个警告:如果注解参数在编译时是有效的但是代表这个确切的异常类型的类文件在运行时不再出现了,测试运行将抛出TypeNotPresentException。
把我们的异常测试例子走的更远一步,如果一个测试抛出几个指定的异常中的任何一个,就可以想象它是否通过了测试。注解机制提供了一种工具,可以方便地支持这种使用。假设我们将ExceptionTest注解的参数类型更改为Class对象数组:
image.png
注解中数组参数的语法是灵活的。它为单元素数组是有优化的。新的ExceptionTest版本上,所有先前的ExceptionTest注解仍然是有效的,并结果为单元素数组。若要指定多元素数组,请用大括号包为元素并用逗号分隔它们:
image.png
修改测试运行工具以处理ExceptionTest的新版本是相当简单的,此代码替换了原始版本:
image.png
在Java8中,有另一种方法来操作多值注解。与使用数组参数声明注解类型不同,你可以用@Repeable元注解对注解的声明进行注解,以指示注解可以重复应用于单个元素。这个元注解传递单个参数,是 包含注释类型的类对象,其唯一参数是注解类型的数组e[JLS, 9.6.3]。这是注解声明的样子如果我们使用这个方法传递给ExceptionTest注解。请注意,必须用适当的保留策略和目标对包含的注解类型进行 注解,否则声明不会编译:
如下是我们的doublyBad测试的样子,有着重复注解代替了数组值得注解:
image.png
处理可重复的注释需要小心。重复注解生成包含注解类型的合成注解,getAnnotationsByType方法掩饰了这个事实,并可用于访问可重复批注类型的重复注释和非重复注释。但是isAnnotationPresent明确指出,重复注解不是注解类型,而是包含注解类型的注解类型。如果一个元素有某种重复的注解,并且使用isAnnotationPresent方法检查该元素是否有该类型的注解,那么你会发现它没有。因此,使用此方法检查注解类型是否存在将导致你的程序忽略重复的注解。类似地,使用此方法检查包含的注解类型将导致程序忽略不重复的注解。要用isAnnotationPresent检测重复和非重复的注解,你需要检查注解类型及其包含的注解类型。这是我们的RunTests程序的样子的相关部分,修改为使用ExceptionTest注解的可重复的版本:
image.png
添加可重复的注解提高了源代码的可读性,该源代码在逻辑上将同一批注解类型的多个实例应用于给定的程序元素。如果你认为他们增强了源代码的可读性,那么请使用他们,但是请记住,在声明和处理可重复注释时有更多的样板,而且处理可重复注解容易出错。
这个项目中的测试框架指示一个玩具,但它清楚地展示了注释比命名模式的优越性,它只触及了你可以用它们做什么的表面。如果你编写的工具要求程序员向源代码中添加信息,请定义适当的注解类型。当你可以使用注解的时候,根本没有理由使用命名模式。
也就是说,除了工具匠之外,大多数程序员没有必要定义注解类型。但是所有程序员应该使用Java提供的预定义的注解类型(item40,27)。同时,考虑使用IDE或静态分析工具提供的注解,这些注解会因为这些工具提高诊断信息的质量。然而,注意到,这些注解还没有标准化,因此如果你却换工具或出现标准,你可能有一些工作要做。
本文写于2019.7.9,历时1天