注解

注解(Annotations)是一种元数据(metadata),可以附加到代码的某些部分(如类、方法、字段、参数等)以提供额外的信息或指示。

注解是 Java 5 的一项重要语言更新。它提供了用 Java 无法表达,却是完整表述程序所需的信息。因此,注解使你可以用某种格式来保存和程序有关的额外信息,编译器会验证该格式的正确性。注解可以生成描述符文件,甚至还可以生成新的类定义,并帮助你减轻编写“样板”代码的负担。通过注解,可以将这些元数据保存在 Java 源代码中,并拥有以下优势:更整洁的代码,编译时的类型检查,为注解构建处理工具的注解 API。

注解的语法十分简单,主要是在现有语法中添加 @ 符号。Java 5 引入了第一批定义在 java.lang 中的3个通用内建注解:

  • @Override:用来声明该方法的定义会重载基类中的某个方法

  • @Deprecated:如果使用该注解的元素被调用,编译器就会发出警告信息

  • @SuppressWarnings:关闭不当的编译器警告信息

下面是 Java 7 和 Java 8 新增的注解:

  • @SafeVarargs:Java 7 中引入,用于在使用泛型作为可变参数的方法或构造器中关闭对调用者的警告

  • @FunctionalInterface:Java 8 引入,用于表明类型声明是函数式接口

每当你创建涉及重复工作的类或接口时,通常都可以用注解来自动化及简化该过程。例如 Enterprise JavaBeans(EJB)中的许多额外工作已被 EJB3 中的注解替代。

注解可以替代一些已有系统,如 XDoclet(一个创建注解风格文档的独立文档工具)。对比来看,注解是真正的语言组件,因此是结构化的,并可接受编译时类型检查。将所有信息都保存在真正的代码而不是注释中,会使代码更整洁,且更便于维护。通过直接使用或扩展注解 API 和工具,或者使用外部的字节码处理库(如本章后面所述),可以对源代码以及字节码执行强大的检查和操作。

基本语法

在下面的示例中,testExecute() 方法添加了 Test 注解。该注解本身并不会做任何事,只是编译器会确保在 CLASSPATH 中存在 @Test 注解的定义。本章稍后会创建一个通过反射来运行该方法的工具。

import onjava.atunit.Test;

public class Testable {
    public void execute() {
        System.out.println("Executing..");
    }
    @Test
    void testExecute() { execute();}
}

增加了注解的方法和其他方法并无区别,本例中的 @Test 注解可以和任何修饰符一起配合使用,如 public, static 和 void。从语法角度看,注解的使用方式和修饰符基本相同。

定义注解

下面是对上面注解的定义。它看起来很像接口的定义。事实上,它们和其他 Java 接口一样,也会被编译成 class 文件。

除了 @ 符号之外,@Test 的定义看起来更像一个空接口。注解的定义也要求必须有元注解 @Target 和 @Retention。 @Target 定义了你可以在何处应用该注解(例如方法或字段)。 @Retention 定义了该注解在源代码(SOURCE),类文件(CLASS)或运行时(RUNTIME)中是否可用。

注解通常包含一些可以设定值的元素。程序或工具在处理注解时可以使用这些参数。元素看起来比较像接口方法,只不过你可以为其指定默认值。

不包含任何元素的注解(如上面的 @Test)称为标记注解(marker annotation)。

下面是一个用于跟踪某项目中用例的简单注解,程序员会给某个特定用例所需的所有方法或一组方法都加上注解。项目经理可以通过计算已实现的用例数来了解项目的进度,维护项目的开发人员可以轻松地找到需要更新的用例,或者在系统内调试业务规则。

注意,id 和 description 与方法声明很相似。因为 id 会受到编译器的类型检查,所以可以放心地用它将跟踪数据库链接到用例文档和源代码。description 元素有个默认值,如果在方法被注解时未指定该元素的值,则注解处理器会使用该默认值。

在下面的类中,有三个方法被注解为用例:

注解元素定义值的方式是,在 @UseCase 声明后的圆括号中,用"名-值"对形式来表示。

你可以先用这种方法来“勾勒”出你的系统,然后在构建时逐渐完善其功能。

元注解

Java 语言中目前只定义了 5 个标准注解(前面已介绍)和 5 个元注解(如下表)。元注解是为了对注解进行注解。

注解
解释

@Target

表示注解可以用于哪些地方。可能的 ElementType 参数包括: - CONSTRUCTOR:构造器的声明 - FIELD:字段声明(包括 enum 常量) - LOCAL_VARIABLE:本地变量声明 - METHOD:方法声明 - PACKAGE:包声明 - PARAMETER:参数声明 - TYPE:类、接口(包括注解类型)或者 enum 声明

@Retention

表示注解信息保存的时长。可选的 RetentionPolicy 参数包括: - SOURCE:注解会被编译器丢弃 - CLASS:注解在类文件中可被编译器使用,但会被虚拟机丢弃 - RUNTIME:注解在运行时仍被虚拟机保留,因此可以通过反射读取到注解信息

@Documented

将此注解保存在 Javadoc 中

@Inherited

允许子类继承父类的注解

@Repeatable

可以多次应用于同一个声明(Java 8)

大多数时候,你可以定义自己的注解,然后自行编写处理器来处理它们。

编写注解处理器

如果没有用于读取注解的工具,那么注解不会比注释更有用。使用注解很重要的一点就是创建并使用注解处理器。Java 拓展了反射机制的 API 用于帮助你创造这类工具,同时还提供了一个 javac 编译器钩子,用来在编译时使用注解。

以下示例是一个简单的注解处理器,它读取被注解的 PasswordUtils(密码工具)类,然后利用反射来查找 @UseCase 标签。通过给定的 id 值列表,该注解列出了它找到的所有用例,并报告所有丢失的用例。

此处同时使用了反射方法 getDeclaredMethods() 和从 AnnotatedElement 接口(诸如 Class,Method 以及 Field 等这样的类都会实现该接口)中继承实现的 getAnnotation() 方法,该方法返回指定类型的注解对象,在本例中即 UseCase。如果在此注解方法上没有该指定类型的注解,将会返回 null。元素的值通过调用 id()description() 方法提取出来。

注解元素

前面定义的 @UseCase 标签包含了 int 元素 id 和 String 元素 description。下面是注解所允许的所有元素类型:

  • 所有的基本类型(int, float, boolean 等)

  • String

  • Class

  • enum

  • Annotation

  • 以上类型的数组

注意,也不允许使用任何包装类型,但是由于自动装箱的存在,这不算是什么限制。注解也可以作为元素的类型。稍后会看到,注解嵌套是一个非常有用的技巧。

默认值限制

编译器对元素的默认值要求很苛刻,所有元素都需要有确定的值,即要么有默认值,要么由使用该注解的类设定值。

还有另一个限制:不论是在源代码中声明时,还是在注解中定义默认值时,任何非基本类型元素都不能赋值为 null。这使得处理器很难表现一个元素的存在或者缺失的状态,因为在每个注解的声明中,所有的元素都存在,并且具有相应的值。但是可以自定义一些特殊的值,比如空字符串或者负数用于表达某个元素不存在:

生成外部文件

当有些框架需要一些额外的信息才能与你的源代码协同工作,这种情况下注解就会变得十分有用。诸如 Enterprise JavaBeans(即EJB)这样的技术(在EJB3 出现之前)需要大量的接口和部署描述文件作为“样板”代码,它们以相同的方式为每个 bean 进行定义。Web 服务、自定义标签库,以及 Toplink, Hibernate 等对象/关系映射工具(ORM)通常也需要代码之外的 XML 描述文件。每定义一个 Java 类,程序员都必须经过一个乏味的配置信息的过程,比如配置类名、包名等这些都是类中本来就有的信息。无论你什么时候使用外部描述符文件,最终都会得到关于一个类的两个独立的信息源,这常常导致代码的信息同步问题。同时这也要求该项目的程序员除了写 Java 程序外,还必须知道如何编写这些描述符文件。

假如你想实现一套基本的 ORM 功能来自动化数据库表的创建,你便可以通过 XML 描述符文件来指定类名、类中的每一个成员,以及数据库映射信息。而如果使用注解,你可以将所有的信息都维护在单个源代码文件中。要实现此功能,你需要注解来定义数据库表名、字段信息,以及要映射到属性的 SQL 类型。

以下示例是一个注解,它会让注解处理器创建一个数据库表:

在 @Target 注解中指定的每个 ElementType(元素类型)都在告诉编译器只能应用于该特定类型。可以指定一个单值的 enum 元素类型,也可以指定一个用逗号分隔的任意值组成的列表。如果想要将注解应用于所有的 ElementType,那么可以去掉 @Target 注解,但是这并不常见。

注意 @DBTable 中有一个 name() 元素,该注解通过这个元素为处理器创建数据库时提供表的名字。

下面是表字段的注解:

@Constraints 注解允许处理器提供数据库表的元数据。这相当于数据库提供的一个小的约束子集,不过它可以帮助你形成一个整体的概念。通过为 primaryKey(), allowNull()unique() 元素设置合理的默认值,可以帮使用者减少很多编码工作。

另外两个注解定义的是 SQL 类型,为了使框架更好用,可以为每个 SQL 类型都定义相应的注解。不过作为示例,两个元素足够了。

这里利用了嵌套注解特性,嵌入字段类型的数据库约束信息。注意 constraints() 元素的默认值是 @Constraints。由于在 @Constraints 注解类型之后,没有在括号中指明 @Constraints 元素的值,因此,constraints() 的默认值为所有元素都为默认值的 @Constraints 注解。假设要使得嵌入的 @Constraints 注解中的 unique() 元素为 true,并作为 constraints() 元素的默认值,你可以像如下定义:

下面是使用了该注解的简单类:

类注解 @DBTable 被赋值为 MEMBER,以用作表名。属性 firstName 和 lastName 都被注解为 @SOLString,并且分别被赋值为 30 和 50。这两个注解都用到了嵌入的 @Constraints 注解的默认值,而且使用了快捷方式的特性。如果你在注解中定义了名为 value 的元素,并且在使用该注解时,value 为唯一一个需要赋值的元素,你就不需要使用名—值对的语法,你只需要在括号中给出 value 元素的值即可。这可以应用于任何合法类型的元素。这也限制了你必须将元素命名为 value。

但是,再看 reference 上的注解,其中有一个 @SQLString 注解,但它同时又必须是数据库的主键,因此在内嵌注解 @Constraint 必须设置元素类型 primaryKey。此时就必须使用名值对的形式,使其不再优雅。

对于该问题,可以使用多种不同的方式来定义自己的注解。例如,你可以使用一个单一的注解类 @TableColumn,它拥有一个 enum 元素,元素值定义了 STRING,INTEGER,FLOAT 等类型。这消除了每个 SQL 类型都需要定义一个 @interface 的负担,不过也使得用额外信息修饰 SQL 类型变的不可能,这些额外的信息例如长度或精度等,都可能是非常有用的。

也可以使用 String 元素来描述实际的 SQL 类型(比如 VARCHAR(30) 或 INTEGER)。这使得你可以修饰类型,却将 Java 类型和 SQL 类型的映射关系在代码中绑定了,使得每当数据库有变化时就重新编译一遍代码。更优雅的方法是,告诉注解处理器你需要什么“口味”(flavor)的 SQL,然后处理器在执行时再来处理这些细节。

第三种可行的方法是同时使用两个注解类型来注解目标字段一@Constraints 和相应的 SQL 类型(比如 @SQLInteger)。这不太优雅,但是只要你需要,编译器就允许对目标增加任意个注解。在 Java 8 中使用多注解时,同一个注解可以重复使用。

注解不支持继承

无法对 @interface 使用 extends 关键字,这很遗憾,在当前情况下,上例中的解决方案可能已经是最佳方案了。

实现处理器

下面示例演示了注解处理器如何读取类文件,检查其数据库注解,并生成创建数据库的 SQL 命令:

主方法会遍历命令行中的所有类名,forName() 方法负责加载所有类,而 getAnnotation(DBTable.class) 则检查类上是否有 DBTable 注解。如果有,将表名存储起来。然后读取这个类的所有字段,并使用 getDeclaredAnnotations() 进行检查。该方法返回定义在某个方法上的所有注解。instanceof 操作符用来确定这些注解是否是 @SOLInteger 和 @SQLString 类型,不论是哪种,都会用表字段名来创建相关的 String 片段。注意,因为无法继承注解接口,所以使用 getDeclaredAnnotations() 是唯一能实现近似多态行为的方式。

嵌套的 @Constraint 注解被传递给 getConstraints() 方法,并用它来构造一个包含 SQL 约束的 String 对象。

然而使用上述方式定义一套 ORM 是较差的方案,如果使用将表名作为参数的 @DBTable 类型,那么只要表名有变更,就得重新编译 Java 代码。现在已经有了很多可用的框架,用于将对象映射到数据库中,并且越来越多的框架开始使用注解了。

用 javac 处理注解

通过 javac,可以通过创建编译时(compile-time)注解处理器在 Java 源文件上使用注解,而不是编译之后的 class 文件。但是这里有一个重要限制:不能通过处理器来改变源代码。唯一影响输出的方式就是创建新的文件。

如果你的注解处理器创建了新的源文件,在新一轮处理中注解会检查源文件本身。工具在检测一轮之后持续循环,直到不再有新的源文件产生。然后它编译所有的源文件。

每一个你编写的注解都需要处理器,但是 javac 可以非常容易的将多个注解处理器合并在一起。你可以指定多个需要处理的类,并且你可以添加监听器用于监听注解处理完成后接到通知。

最简单的处理器

先从最简单的注解开始:

@Retention 的参数现在为 SOURCE,这意味着注解不会再存留在编译后的代码。这在编译时处理注解是没有必要的,它只是指出,在这里,javac 是唯一有机会处理注解的代理。

@Target 声明了几乎所有的目标类型(除了 PACKAGE) ,同样是为了演示。

下面是测试的示例:

这里使用 @Simple 注解了所有 @Target 声明允许的地方。

javac 允许使用 @Simple 注解(只要它还存在),但是并不会对它做任何事,直到我们创建了一个注解处理器,并将其绑定到编译器中。下面是一个只是打印注解信息的简单处理器:

已失效的旧 apt(Annotation Processing Tool,编译时注解处理器) 版本的注解处理器需要额外的方法来确定哪些注解和 Java 版本可以被支持。但现在可以简单的使用 @SupportedAnnotationTypes 和 @SupportedSourceVersion 注解来达到目的(这是一个很好的示例关于注解如何简化你的代码)。

此处唯一需要实现的方法是 process(),其中包含了所有的逻辑。第一个参数会告诉你有哪些注解,第二个参数则包含余下的所有信息。这里只是把注解(只有一个)都打印了出来,要了解其他功能,请参考 TypeElement 文档。

通过 process() 方法的第二个参数,我们遍历所有被 @Simple 注解的元素,并目对每个元素都调用了 display() 方法。每个 Element 都可以携带自身的基本信息,例如 getModifiers() 能够告诉我们它是否是 public 和 static 的。

Element 只能执行那些编译器解析的所有基本对象共有的操作,而类和方法之类的东西有额外的信息需要提取。因此需要先检查它是哪种 ElementKind,然后向下转型为更具体的元素类型(这里指对 CLASS 的 TypeElement 和对 METHOD 的 ExecutableElement),然后为这些元素调用其他方法。

这里的动态向下转型(不会在编译期被检查)是一种“很不 Java”的处理方式。

想得到注解的输出,需要加上 -processor 标识和注解处理器类(注意 idea 中要先对其进行编译,再进行下面的步骤,并且还需要加上 -cp target/classes,否则会出现“找不到类”错误):

输出结果如下:

这给出一些可以发现的东西,包括参数名和类型、返回值等。

复杂的处理器

当你创建了配合 javac 使用的注解处理器时,就不能使用传统的 Java 反射,因为你操作的是源代码而非编译后的类文件。为了解决这个问题,你可以使用 Java 的镜像(mirror)机制在源代码级别查看和操作类、方法和字段的信息。

以下示例是一个注解,它从一个类中提取 public 方法,以将它们转换为接口:

RetentionPolicy 的值为 SOURCE,这是为了在提取类中的接口之后不再将注解信息保留在 class 文件中。下面测试类提供了可组成接口的一些 public 方法:

Mutiplier 类中的 multipy() 方法会多次调用私有方法 add() 来模拟乘法操作。add() 是私有方法,将不被包含进接口。该注解的 interfaceName 被赋值为 IMultiplier,以作为要创建的接口名。

下面是一个编译时处理器,它会提取出感兴趣的方法,并创建新接口的源代码文件。(该源文件之后会作为“编译阶段”的一部分,被自动编译)

Elements 对象 elementutils 是个 static 工具的集合,通过它可以在 writeInterfaceFile() 中找到包名。

getEnclosedElements() 方法会通过指定的元素生成所有的“围住”的元素,通过 getKind() 可以找到所有 public 和 static 方法,并将其添加到 interfaceMethods 列表中。然后 writeInterfaceFile() 通过该列表来生成新的接口定义。注意在 writeInterfaceFile() 中对 ExecutableElement 的向下转型使得我们可以提取所有的方法信息。createArgList() 则是一个生成参数列表的辅助方法。

Filer(由 getFiler() 生成)是一种创建新文件的 Printwriter。之所以使用 Filer 对象而非某个普通的 Printwriter,是因为 Filer 对象允许 javac 持续跟踪你创建的所有新文件,从而可以检查它们的注解,并在额外的“编译阶段”中编译它们。

如前面那样对其编译后,他所生成的 IMultiplier.java 文件如下:

该文件同样也会被 javac 所编译(作为“编译阶段”的一部分),因此你可以在同一个目录中看到 IMultiplier.class 文件。

基于注释的单元测试

单元测试,即通过为类中的每个方法都创建一个或多个测试,以定期检测类中各部分的行为是否正确。目前在 Java 中最流行的单元测试工具是 JUnit,JUnit 4 引入了注解。

在 JUnit 4 之前的版本中,你需要创建一个单独的类来持有你的单元测试代码。而在 JUnit 4 中,你可以将单元测试集成到要测试的类中,从而将各种耗时和故障降到最低值。这种方法还有额外的好处,它测试 private 方法就和测试 public 方法一样简单。

下面这个测试框架的示例是基于注解的,叫作 @Unit。只用一个 @Test 注解来标识出需要测试的方法,是最基础也可能是你最常用的测试形式。也可以选择让测试方法不接收参数,仅返回一个 boolean 值来表示测试是成功还是失败。@Unit 注解的测试方法可以支持任何访问权限,包括 private。

引入 onjava.atunit 以使用 @Unit,使用 @Unit 的测试标记为合适的方法和字段打上标签(在后面的例子中会学到),然后让你的构建系统对编译后的类运行 @Unit。

要用 @Unit 测试的类必须放在包中。

这些方法前面的 @Test 注解告诉 @Unit 要将这些方法作为单元测试来运行,同时 @Test 确保这些方法没有任何参数并且返回值为 boolean 或者 void。编写单元测试时,只需要确定测试是否成功,分别返回 true 或 false。

如果不想将测试方法嵌入类中,那么创建一个非嵌入式的测试,最简单的方式就是继承:

也可以用组合的方式创建非嵌入的测试:

@Unit 中没有 JUnit 中特殊的 assert 方法,不过另一种形式的 @Test 方法仍然允许返回值为 void。为了表示测试成功,可以使用 Java 的 assert 语句。Java 断言机制需要你在 java 命令行行加上 -ea 标志来开启,但是 @Unit 已经自动开启了该功能。要表示测试失败的话,甚至可以使用异常。@Unit 的设计目标之一就是尽可能减少添加额外的语法,而 Java 的 assert 和异常则是报告错误所必须的。如果测试方法引发了失败断言或者异常,则会被视为测试失败,但是 @Unit 并不会因此阳塞,它会持续运行,直到所有的测试都运行完毕。如下例所示:

下面是用断言实现的非嵌入式测试,它对 java.util.HashSet 做了一些简单的测试:

若没有其他约束,采用继承的方式会看起来简单一些。

@Unit 依赖无参数的构造方法为每个要测试的类创建测试对象,然后该对象会被丢弃,以防止各种副作用渗透到其他单元测试中。如果没有无参数的构造函数,或者需要更复杂的构造函数,需要创建一个静态方法来构建对象,并添加 @TestObjectCreate 注解,如下:

@TestObjectCreate 方法必须是静态的,并且必须返回你测试的类型的对象。@Unit 程序会确保这些。

有时可能需要添加额外的字段来支持单元测试,@TestProperty 注解可以标识仅用于单元测试的字段(在交付前便可以移除这些字段)。下面示例读取一个被 String.split() 方法切割后的字符串的值,将其作为输人来生成测试对象:

@TestProperty 也可以用来标记那些只在测试中使用的方法,但是本身不是测试方法。

如果测试对象的创建过程需要执行初始化,而且需要在稍后清理对象,你可以选择添加一个静态的 @TestObjectCleanup 方法,以在使用完测试对象后执行清理工作。在下一个示例中,@TestobjectCreate 通过打开一个文件来创建各个测试对象,因此必须在丢弃测试对象前关闭该文件:

从输出可以看到,每项测试后清理工作都自动执行了。

在 @Unit 中使用泛型

泛型会带来一个问题,因为无法进行“通用测试”,只能针对某个特定类型的参数或者参数集合进行测试。解决方法十分简单,让测试类继承自泛型类的一个特定版本即可:

下例用 LinkedList 实现了一个栈:

假设测试字符串版本,则从 StackL<String> 继承一个测试类:

继承的唯一潜在缺点是,会失去访问被测试类中 Private 方法的能力。如果不希望这样,则可以将该方法设为 protected,或者添加一个非 private 的 @TestProperty 方法来调用该私有方法(稍后介绍的 @AtUnitRemover 工具会从产品代码中剥离 @TestProperty 方法)。

@Unit 会搜索那些包含合适注解的类文件,然后运行 @Test 方法。

实现 @Unit

首先,需要定义所有的注解类型,这些都是简单的标签,并且没有任何字段。@Test 标签已经定义过,下面是其他注解:

所有测试的生命周期都限定在 RUNTIME 内,这是因为 @Unit 必须在编译后的代码中发现这些注解。

要实现系统并运行测试,需要使用反射机制提取注解。程序通过注解中的信息,决定如何构造测试对象,并在测试对象上运行测试。注解使得结果出乎意料地简单易懂:

这里使用了另一个叫作 Processfiles 的工具来单步遍历命令行中的每个参数,以及确定它是目录还是文件,并进行相应的处理。其中包含了一个可定制的 Strategy(策略)接口,因此可应用于多种方案实现。

AtUnit 类实现了 ProcessFiles.Strategy,其中包含 process() 方法。由此,AtUnit 的实例可以传递给 ProcessFiles 构造器。构造器的第二个参数告诉 ProcessFiles 去查找所有文件名后缀为 .class 的文件。下面是简单的用法示例:

在没有命令行参数的情况下,程序会遍历当前的目录树。还可以提供多个参数,不论是类文件还是目录。

因为 @Unit 会自动找到可测试的类和方法,所以不需要“套件”机制。

AtUnit 中寻找类文件时有个问题:从类文件名无法确切地得知限定的类名(包括包名)。要获取这个信息就必须分析类文件。找到 .class 文件时,会打开它并读取其二进制数据并将其传递给 ClassNameFinder.thisClass()。 在这里,我们正在进入“字节码工程”领域,因为我们实际上正在分析类文件的内容:

虽然无法介绍所有细节,但是每个类文件必须遵循一定的格式,而这里已经尽力用有意义的字段来表示这些从 ByteArrayInputStream 中提取出来的数据片段。通过施加在输入流上的读操作,你能看出每个信息片的大小。例如每一个类的头 32 个 bit 总是一个 “魔法数字” 0xcafebabe,而接下来的两个 short 值是版本信息。常量池包含了程序的常量,所以这是一个可变的值。接下来的 short 告诉我们这个常量池有多大,然后我们为其创建一个尺寸合适的数组。常量池中的每一个元素,其长度可能是固定式,也可能是可变的值,因此我们必须检查每一个常量的起始标记,然后才能知道该怎么做,这就是 switch 语句的工作。我们并不打算精确的分析类中所有的数据,仅仅是从文件的起始一步一步的走,直到取得我们所需的信息,因此你会发现,在这个过程中我们丢弃了大量的数据。关于类的信息都保存在 classNameTable 和 offsetTable 中。在读取常量池之后,就找到了 this_class 信息,这是 offsetTable 的一个坐标,通过它可以找到进入 classNameTable 的坐标,然后就可以得到我们所需的类的名字了。

然后回到 AtUnit 中,process() 方法中拥有了类的名字,然后检查它是否包含“.”,如果有就表示该类定义于一个包中。没有包的类会被忽略。如果一个类在包中,那么我们就可以使用标准的类加载器通过 Class.forName() 将其加载进来。紧接着可以分析该类中的 @Unit 注释了。

我们需要找的三个方法: @Test 方法(被保存在 TestMehtods 列表中),检查其是否具有 @TestObjectCreate 和 @TestObjectCleanup 方法。它们都是通过调用相关方法找到的。

每找到一个 @Test 方法,就打印出来当前类的名字,于是观察者立刻就可以知道发生了什么。接下来开始执行测试,也就是打印出方法名,然后调用 createTestObject() (如果存在一个加了 @TestObjectCreate 注解的方法),或者调用默认构造器。一旦创建出来测试对象,如果调用其上的测试方法。如果测试的返回值为 boolean,就捕获该结果。如果测试方法没有返回值,那么就没有异常发生,我们就假设测试成功,反之,如果当 assert 失败或者有任何异常抛出的时候,就说明测试失败,这时将异常信息打印出来以显示错误的原因。如果有失败的测试发生,那么还要统计失败的次数,并将失败所属的类和方法加入到 failedTests 中,以便最后报告给用户。

总结

注解是 Java 引入的一项非常受欢迎的补充,它提供了一种结构化,并且具有类型检查能力的新途径,从而使得你能够为代码中加入元数据,而且不会导致代码杂乱并难以阅读。使用注解能够帮助我们避免编写累赘的部署描述性文件,以及其他的生成文件。而 Javadoc 中的 @deprecated 被 @Deprecated 注解所替代的事实也说明,与注释性文字相比,注解绝对更适用于描述类相关的信息。