23-Annotations
第二十三章 注解
注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方式,使我们可以在稍后的某个时刻更容易的使用这些数据。
注解在一定程度上是把元数据和源代码文件结合在一起的趋势所激发的,而不是保存在外部文档。这同样是对像
注解是
注解的语法十分简单,主要是在现有语法中添加
- @Override:表示当前的方法定义将覆盖基类的方法。如果你不小心拼写错误,或者方法签名被错误拼写的时候,编译器就会发出错误提示。
- @Deprecated:如果使用该注解的元素被调用,编译器就会发出警告信息。
- @SuppressWarnings:关闭不当的编译器警告信息。
- @SafeVarargs:在
Java 7 中加入用于禁止对具有泛型varargs 参数的方法或构造函数的调用方发出警告。 - @FunctionalInterface:
Java 8 中加入用于表示类型声明为函数式接口。
还有
每当创建涉及重复工作的类或接口时,你通常可以使用注解来自动化和简化流程。例如在
注解的出现可以替代一些现有的系统,例如
基本语法
在下面的例子中,使用 @Test
对 testExecute()
进行注解。该注解本身不做任何事情,但是编译器要保证其类路径上有 @Test
注解的定义。你将在本章看到,我们通过注解创建了一个工具用于运行这个方法:
// annotations/Testable.java
package annotations;
import onjava.atunit.*;
public class Testable {
public void execute() {
System.out.println("Executing..");
}
@Test
void testExecute() { execute(); }
}
被注解标注的方法和其他方法没有任何区别。在这个例子中,注解 @Test
可以和任何修饰符共同用于方法,诸如
定义注解
如下是一个注解的定义。注解的定义看起来很像接口的定义。事实上,它们和其他
// onjava/atunit/Test.java
// The @Test tag
package onjava.atunit;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {}
除了@Test
的定义看起来更像一个空接口。注解的定义也需要一些元注解(meta-annotation@Target
和 @Retention
。@Target
定义你的注解可以应用在哪里(例如是方法还是字段@Retention
定义了注解在哪里可用,在源代码中(SOURCE
注解通常会包含一些表示特定值的元素。当分析处理注解的时候,程序或工具可以利用这些值。注解的元素看起来就像接口的方法,但是可以为其指定默认值。
不包含任何元素的注解称为标记注解(marker annotation@Test
就是标记注解。
下面是一个简单的注解,我们可以用它来追踪项目中的用例。程序员可以使用该注解来标注满足特定用例的一个方法或者一组方法。于是,项目经理可以通过统计已经实现的用例来掌控项目的进展,而开发者在维护项目时可以轻松的找到用例用于更新,或者他们可以调试系统中业务逻辑。
// annotations/UseCase.java
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
int id();
String description() default "no description";
}
注意
在下面的类中,有三个方法被注解为用例:
// annotations/PasswordUtils.java
import java.util.*;
public class PasswordUtils {
@UseCase(id = 47, description =
"Passwords must contain at least one numeric")
public boolean validatePassword(String passwd) {
return (passwd.matches("\\w*\\d\\w*"));
}
@UseCase(id = 48)
public String encryptPassword(String passwd) {
return new StringBuilder(passwd)
.reverse().toString();
}
@UseCase(id = 49, description =
"New passwords can't equal previously used ones")
public boolean checkForNewPassword(
List<String> prevPasswords, String passwd) {
return !prevPasswords.contains(passwd);
}
}
注解的元素在使用时表现为 名@UseCase
声明之后的括号内。在 encryptPassword()
方法的注解中,并没有给出
你应该能够想象到如何使用这套工具来“勾勒”出将要建造的系统,然后在建造的过程中逐渐实现系统的各项功能。
元注解
注解 | 解释 |
---|---|
@Target | 表示注解可以用于哪些地方。可能的 CONSTRUCTOR:构造器的声明; FIELD:字段声明(包括 LOCAL_VARIABLE:局部变量声明; METHOD:方法声明; PACKAGE:包声明; PARAMETER:参数声明; TYPE:类、接口(包括注解类型)或者 |
@Retention | 表示注解信息保存的时长。可选的 SOURCE:注解将被编译器丢弃; CLASS:注解在 RUNTIME: |
@Documented | 将此注解保存在 |
@Inherited | 允许子类继承父类的注解 |
@Repeatable | 允许一个注解可以被使用一次或者多次(Java 8 |
大多数时候,程序员定义自己的注解,并编写自己的处理器来处理他们。
编写注解处理器
如果没有用于读取注解的工具,那么注解不会比注释更有用。使用注解中一个很重要的部分就是,创建与使用注解处理器。
下面是一个非常简单的注解处理器,我们用它来读取被注解的
// annotations/UseCaseTracker.java
import java.util.*;
import java.util.stream.*;
import java.lang.reflect.*;
public class UseCaseTracker {
public static void
trackUseCases(List<Integer> useCases, Class<?> cl) {
for(Method m : cl.getDeclaredMethods()) {
UseCase uc = m.getAnnotation(UseCase.class);
if(uc != null) {
System.out.println("Found Use Case " +
uc.id() + "\n " + uc.description());
useCases.remove(Integer.valueOf(uc.id()));
}
}
useCases.forEach(i ->
System.out.println("Missing use case " + i));
}
public static void main(String[] args) {
List<Integer> useCases = IntStream.range(47, 51)
.boxed().collect(Collectors.toList());
trackUseCases(useCases, PasswordUtils.class);
}
}
输出为:
Found Use Case 48
no description
Found Use Case 47
Passwords must contain at least one numeric
Found Use Case 49
New passwords can't equal previously used ones
Missing use case 50
这个程序用了两个反射的方法:getDeclaredMethods()
和 getAnnotation()
,它们都属于getAnnotation()
方法返回指定类型的注解对象,在本例中就是 “UseCase”。如果被注解的方法上没有该类型的注解,返回值就为id()
和 description()
方法来提取元素值。注意 encryptPassword()
方法在注解的时候没有指定description()
取得的是默认值 “no description”。
注解元素
在
- 所有基本类型(int、float、
boolean 等) - String
- Class
- enum
- Annotation
- 以上类型的数组
如果你使用了其他类型,编译器就会报错。注意,也不允许使用任何包装类型,但是由于自动装箱的存在,这不算是什么限制。注解也可以作为元素的类型。稍后你会看到,注解嵌套是一个非常有用的技巧。
默认值限制
编译器对于元素的默认值有些过于挑剔。首先,元素不能有不确定的值。也就是说,元素要么有默认值,要么就在使用注解时提供元素的值。
这里有另外一个限制:任何非基本类型的元素, 无论是在源代码声明时还是在注解接口中定义默认值时,都不能使用
// annotations/SimulatingNull.java
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimulatingNull {
int id() default -1;
String description() default "";
}
这是一个在定义注解的习惯用法。
生成外部文件
当有些框架需要一些额外的信息才能与你的源代码协同工作,这种情况下注解就会变得十分有用。像
假设你想提供一些基本的对象
以下是一个注解的定义,它告诉注解处理器应该创建一个数据库表:
// annotations/database/DBTable.java
package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.TYPE) // Applies to classes only
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
String name() default "";
}
在 @Target
注解中指定的每一个@Target
注解,但是这并不常见。
注意name()
元素,该注解通过这个元素为处理器创建数据库时提供表的名字。
如下是修饰字段的注解:
// annotations/database/Constraints.java
package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
boolean primaryKey() default false;
boolean allowNull() default true;
boolean unique() default false;
}
// annotations/database/SQLString.java
package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
int value() default 0;
String name() default "";
Constraints constraints() default @Constraints;
}
// annotations/database/SQLInteger.java
package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
String name() default "";
Constraints constraints() default @Constraints;
}
primaryKey()
,allowNull()
和 unique()
元素明显的提供了默认值,从而使得在大多数情况下,该注解的使用者不需要输入太多东西。
另外两个
这些name()
元素和 constraints()
元素。后者利用了嵌套注解的功能,将数据库列的类型约束信息嵌入其中。注意 constraints()
元素的默认值是unique()
元素为constraints()
元素的默认值,你可以像如下定义:
// annotations/database/Uniqueness.java
// Sample of nested annotations
package annotations.database;
public @interface Uniqueness {
Constraints constraints()
default @Constraints(unique = true);
}
下面是一个简单的,使用了如上注解的类:
// annotations/database/Member.java
package annotations.database;
@DBTable(name = "MEMBER")
public class Member {
@SQLString(30) String firstName;
@SQLString(50) String lastName;
@SQLInteger Integer age;
@SQLString(value = 30,
constraints = @Constraints(primaryKey = true))
String reference;
static int memberCount;
public String getReference() { return reference; }
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
@Override
public String toString() { return reference; }
public Integer getAge() { return age; }
}
类注解
@SQLString(30)
处理器将在创建表的时候使用该值设置
默认值的语法虽然很灵巧,但是它很快就变的复杂起来。以
替代方案
可以使用多种不同的方式来定义自己的注解用于上述任务。例如,你可以使用一个单一的注解类
你也可以使用一个
第三种可行的方案是一起使用两个注解,
注解不支持继承
你不能使用
实现处理器
下面是一个注解处理器的例子,他将读取一个类文件,检查上面的数据库注解,并生成用于创建数据库的
// annotations/database/TableCreator.java
// Reflection-based annotation processor
// {java annotations.database.TableCreator
// annotations.database.Member}
package annotations.database;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class TableCreator {
public static void
main(String[] args) throws Exception {
if (args.length < 1) {
System.out.println(
"arguments: annotated classes");
System.exit(0);
}
for (String className : args) {
Class<?> cl = Class.forName(className);
DBTable dbTable = cl.getAnnotation(DBTable.class);
if (dbTable == null) {
System.out.println(
"No DBTable annotations in class " +
className);
continue;
}
String tableName = dbTable.name();
// If the name is empty, use the Class name:
if (tableName.length() < 1)
tableName = cl.getName().toUpperCase();
List<String> columnDefs = new ArrayList<>();
for (Field field : cl.getDeclaredFields()) {
String columnName = null;
Annotation[] anns =
field.getDeclaredAnnotations();
if (anns.length < 1)
continue; // Not a db table column
if (anns[0] instanceof SQLInteger) {
SQLInteger sInt = (SQLInteger) anns[0];
// Use field name if name not specified
if (sInt.name().length() < 1)
columnName = field.getName().toUpperCase();
else
columnName = sInt.name();
columnDefs.add(columnName + " INT" +
getConstraints(sInt.constraints()));
}
if (anns[0] instanceof SQLString) {
SQLString sString = (SQLString) anns[0];
// Use field name if name not specified.
if (sString.name().length() < 1)
columnName = field.getName().toUpperCase();
else
columnName = sString.name();
columnDefs.add(columnName + " VARCHAR(" +
sString.value() + ")" +
getConstraints(sString.constraints()));
}
StringBuilder createCommand = new StringBuilder(
"CREATE TABLE " + tableName + "(");
for (String columnDef : columnDefs)
createCommand.append(
"\n " + columnDef + ",");
// Remove trailing comma
String tableCreate = createCommand.substring(
0, createCommand.length() - 1) + ");";
System.out.println("Table Creation SQL for " +
className + " is:\n" + tableCreate);
}
}
}
private static String getConstraints(Constraints con) {
String constraints = "";
if (!con.allowNull())
constraints += " NOT NULL";
if (con.primaryKey())
constraints += " PRIMARY KEY";
if (con.unique())
constraints += " UNIQUE";
return constraints;
}
}
输出为:
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30));
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50));
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50),
AGE INT);
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50),
AGE INT,
REFERENCE VARCHAR(30) PRIMARY KEY);
主方法会循环处理命令行传入的每一个类名。每一个类都是用 forName()
方法进行加载,并使用 getAnnotation(DBTable.class)
来检查该类是否带有getDeclaredAnnotations()
进行检查。这个方法返回一个包含特定字段上所有注解的数组。然后使用getDeclaredAnnotations()
似乎是唯一的方式。
嵌套的getConstraints()
方法,并用它来构造一个包含
需要提醒的是,上面演示的技巧对于真实的对象
使用javac 处理注解
通过
如果你的注解处理器创建了新的源文件,在新一轮处理中注解会检查源文件本身。工具在检测一轮之后持续循环,直到不再有新的源文件产生。然后它编译所有的源文件。
每一个你编写的注解都需要处理器,但是
本节中的示例将帮助你开始学习,但如果你必须深入学习,请做好反复学习,大量访问
最简单的处理器
让我们开始定义我们能想到的最简单的处理器,只是为了编译和测试。如下是注解的定义:
// annotations/simplest/Simple.java
// A bare-bones annotation
package annotations.simplest;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE, ElementType.METHOD,
ElementType.CONSTRUCTOR,
ElementType.ANNOTATION_TYPE,
ElementType.PACKAGE, ElementType.FIELD,
ElementType.LOCAL_VARIABLE})
public @interface Simple {
String value() default "-default-";
}
// annotations/simplest/SimpleTest.java
// Test the "Simple" annotation
// {java annotations.simplest.SimpleTest}
package annotations.simplest;
@Simple
public class SimpleTest {
@Simple
int i;
@Simple
public SimpleTest() {}
@Simple
public void foo() {
System.out.println("SimpleTest.foo()");
}
@Simple
public void bar(String s, int i, float f) {
System.out.println("SimpleTest.bar()");
}
@Simple
public static void main(String[] args) {
@Simple
SimpleTest st = new SimpleTest();
st.foo();
}
}
输出为:
SimpleTest.foo()
在这里我们使用
如下是一个十分简单的处理器,其所作的事情就是把注解相关的信息打印出来:
// annotations/simplest/SimpleProcessor.java
// A bare-bones annotation processor
package annotations.simplest;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import java.util.*;
@SupportedAnnotationTypes(
"annotations.simplest.Simple")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class SimpleProcessor
extends AbstractProcessor {
@Override
public boolean process(
Set<? extends TypeElement> annotations,
RoundEnvironment env) {
for(TypeElement t : annotations)
System.out.println(t);
for(Element el :
env.getElementsAnnotatedWith(Simple.class))
display(el);
return false;
}
private void display(Element el) {
System.out.println("==== " + el + " ====");
System.out.println(el.getKind() +
" : " + el.getModifiers() +
" : " + el.getSimpleName() +
" : " + el.asType());
if(el.getKind().equals(ElementKind.CLASS)) {
TypeElement te = (TypeElement)el;
System.out.println(te.getQualifiedName());
System.out.println(te.getSuperclass());
System.out.println(te.getEnclosedElements());
}
if(el.getKind().equals(ElementKind.METHOD)) {
ExecutableElement ex = (ExecutableElement)el;
System.out.print(ex.getReturnType() + " ");
System.out.print(ex.getSimpleName() + "(");
System.out.println(ex.getParameters() + ")");
}
}
}
(旧的,失效的)
你唯一需要实现的方法就是 process()
,这里是所有行为发生的地方。第一个参数告诉你哪个注解是存在的,第二个参数保留了剩余信息。我们所做的事情只是打印了注解(这里只存在一个process()
的第二个操作,我们循环所有被display()
方法。所有getModifiers()
告诉你它是否为
动态向下转型(在编译期不进行检查)并不像是
如果只是通过平常的方式来编译
javac -processor annotations.simplest.SimpleProcessor SimpleTest.java
现在编译器有了输出
annotations.simplest.Simple
==== annotations.simplest.SimpleTest ====
CLASS : [public] : SimpleTest : annotations.simplest.SimpleTest
annotations.simplest.SimpleTest
java.lang.Object
i,SimpleTest(),foo(),bar(java.lang.String,int,float),main(java.lang.String[])
==== i ====
FIELD : [] : i : int
==== SimpleTest() ====
CONSTRUCTOR : [public] : <init> : ()void
==== foo() ====
METHOD : [public] : foo : ()void
void foo()
==== bar(java.lang.String,int,float) ====
METHOD : [public] : bar : (java.lang.String,int,float)void
void bar(s,i,f)
==== main(java.lang.String[]) ====
METHOD : [public, static] : main : (java.lang.String[])void
void main(args)
这给了你一些可以发现的东西,包括参数名和类型、返回值等。
更复杂的处理器
当你创建用于
如下是一个用于提取类中方法的注解,所以它可以被抽取成为一个接口:
// annotations/ifx/ExtractInterface.java
// javac-based annotation processing
package annotations.ifx;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface ExtractInterface {
String interfaceName() default "-!!-";
}
// annotations/ifx/Multiplier.java
// javac-based annotation processing
// {java annotations.ifx.Multiplier}
package annotations.ifx;
@ExtractInterface(interfaceName="IMultiplier")
public class Multiplier {
public boolean flag = false;
private int n = 0;
public int multiply(int x, int y) {
int total = 0;
for(int i = 0; i < x; i++)
total = add(total, y);
return total;
}
public int fortySeven() { return 47; }
private int add(int x, int y) {
return x + y;
}
public double timesTen(double arg) {
return arg * 10;
}
public static void main(String[] args) {
Multiplier m = new Multiplier();
System.out.println(
"11 * 16 = " + m.multiply(11, 16));
}
}
输出为:
11 * 16 = 176
multiply()
方法,这个方法会多次调用私有方法 add()
来模拟乘法操作。 add()
是私有方法,因此不能成为接口的一部分。其他的方法提供了语法多样性。注解被赋予
这里有一个编译时处理器用于提取有趣的方法,并创建一个新的
// annotations/ifx/IfaceExtractorProcessor.java
// javac-based annotation processing
package annotations.ifx;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.util.*;
import java.util.*;
import java.util.stream.*;
import java.io.*;
@SupportedAnnotationTypes(
"annotations.ifx.ExtractInterface")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class IfaceExtractorProcessor
extends AbstractProcessor {
private ArrayList<Element>
interfaceMethods = new ArrayList<>();
Elements elementUtils;
private ProcessingEnvironment processingEnv;
@Override
public void init(
ProcessingEnvironment processingEnv) {
this.processingEnv = processingEnv;
elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(
Set<? extends TypeElement> annotations,
RoundEnvironment env) {
for(Element elem:env.getElementsAnnotatedWith(
ExtractInterface.class)) {
String interfaceName = elem.getAnnotation(
ExtractInterface.class).interfaceName();
for(Element enclosed :
elem.getEnclosedElements()) {
if(enclosed.getKind()
.equals(ElementKind.METHOD) &&
enclosed.getModifiers()
.contains(Modifier.PUBLIC) &&
!enclosed.getModifiers()
.contains(Modifier.STATIC)) {
interfaceMethods.add(enclosed);
}
}
if(interfaceMethods.size() > 0)
writeInterfaceFile(interfaceName);
}
return false;
}
private void
writeInterfaceFile(String interfaceName) {
try(
Writer writer = processingEnv.getFiler()
.createSourceFile(interfaceName)
.openWriter()
) {
String packageName = elementUtils
.getPackageOf(interfaceMethods
.get(0)).toString();
writer.write(
"package " + packageName + ";\n");
writer.write("public interface " +
interfaceName + " {\n");
for(Element elem : interfaceMethods) {
ExecutableElement method =
(ExecutableElement)elem;
String signature = " public ";
signature += method.getReturnType() + " ";
signature += method.getSimpleName();
signature += createArgList(
method.getParameters());
System.out.println(signature);
writer.write(signature + ";\n");
}
writer.write("}");
} catch(Exception e) {
throw new RuntimeException(e);
}
}
private String createArgList(
List<? extends VariableElement> parameters) {
String args = parameters.stream()
.map(p -> p.asType() + " " + p.getSimpleName())
.collect(Collectors.joining(", "));
return "(" + args + ")";
}
}
getEnclosedElements()
方法会通过指定的元素生成所有的“闭包”元素。在这里,这个类闭包了它的所有元素。通过使用 getKind()
我们会找到所有的writeInterfaceFile()
使用writeInterfaceFile()
使用了向下转型到
getFiler()
生成的,并且是
如下是一个命令行,可以在编译的时候使用处理器:
javac -processor annotations.ifx.IfaceExtractorProcessor Multiplier.java
新生成的println()
语句所猜测的那样,如下所示:
package annotations.ifx;
public interface IMultiplier {
public int multiply(int x, int y);
public int fortySeven();
public double timesTen(double arg);
}
这个类同样会被
基于注解的单元测试
单元测试是对类中每个方法提供一个或者多个测试的一种事件,其目的是为了有规律的测试一个类中每个部分是否具备正确的行为。在
在注解版本之前的
这个基于注解的测试框架叫做
要使用
// annotations/AtUnitExample1.java
// {java onjava.atunit.AtUnit
// build/classes/main/annotations/AtUnitExample1.class}
package annotations;
import onjava.atunit.*;
import onjava.*;
public class AtUnitExample1 {
public String methodOne() {
return "This is methodOne";
}
public int methodTwo() {
System.out.println("This is methodTwo");
return 2;
}
@Test
boolean methodOneTest() {
return methodOne().equals("This is methodOne");
}
@Test
boolean m2() { return methodTwo() == 2; }
@Test
private boolean m3() { return true; }
// Shows output for failure:
@Test
boolean failureTest() { return false; }
@Test
boolean anotherDisappointment() {
return false;
}
}
输出为:
annotations.AtUnitExample1
. m3
. methodOneTest
. m2 This is methodTwo
. failureTest (failed)
. anotherDisappointment (failed)
(5 tests)
>>> 2 FAILURES <<<
annotations.AtUnitExample1: failureTest
annotations.AtUnitExample1: anotherDisappointment
使用
methodOneTest()
、 m2()
、m3()
、failureTest()
以及 anotherDisappointment()
方法之前,它们告诉
如果你熟悉
你并非必须将测试方法嵌入到原来的类中,有时候这种事情根本做不到。要生产一个非嵌入式的测试,最简单的方式就是继承:
// annotations/AUExternalTest.java
// Creating non-embedded tests
// {java onjava.atunit.AtUnit
// build/classes/main/annotations/AUExternalTest.class}
package annotations;
import onjava.atunit.*;
import onjava.*;
public class AUExternalTest extends AtUnitExample1 {
@Test
boolean _MethodOne() {
return methodOne().equals("This is methodOne");
}
@Test
boolean _MethodTwo() {
return methodTwo() == 2;
}
}
输出为:
annotations.AUExternalTest
. tMethodOne
. tMethodTwo This is methodTwo
OK (2 tests)
这个示例还表现出灵活命名的价值。在这里,
你也可以使用组合来创建非嵌入式的测试:
// annotations/AUComposition.java
// Creating non-embedded tests
// {java onjava.atunit.AtUnit
// build/classes/main/annotations/AUComposition.class}
package annotations;
import onjava.atunit.*;
import onjava.*;
public class AUComposition {
AtUnitExample1 testObject = new AtUnitExample1();
@Test
boolean tMethodOne() {
return testObject.methodOne()
.equals("This is methodOne");
}
@Test
boolean tMethodTwo() {
return testObject.methodTwo() == 2;
}
}
输出为:
annotations.AUComposition
. tMethodTwo This is methodTwo
. tMethodOne
OK (2 tests)
因为在每一个测试里面都会创建
因为
// annotations/AtUnitExample2.java
// Assertions and exceptions can be used in @Tests
// {java onjava.atunit.AtUnit
// build/classes/main/annotations/AtUnitExample2.class}
package annotations;
import java.io.*;
import onjava.atunit.*;
import onjava.*;
public class AtUnitExample2 {
public String methodOne() {
return "This is methodOne";
}
public int methodTwo() {
System.out.println("This is methodTwo");
return 2;
}
@Test
void assertExample() {
assert methodOne().equals("This is methodOne");
}
@Test
void assertFailureExample() {
assert 1 == 2: "What a surprise!";
}
@Test
void exceptionExample() throws IOException {
try(FileInputStream fis =
new FileInputStream("nofile.txt")) {} // Throws
}
@Test
boolean assertAndReturn() {
// Assertion with message:
assert methodTwo() == 2: "methodTwo must equal 2";
return methodOne().equals("This is methodOne");
}
}
输出为:
annotations.AtUnitExample2
. exceptionExample java.io.FileNotFoundException:
nofile.txt (The system cannot find the file specified)
(failed)
. assertExample
. assertAndReturn This is methodTwo
. assertFailureExample java.lang.AssertionError: What
a surprise!
(failed)
(4 tests)
>>> 2 FAILURES <<<
annotations.AtUnitExample2: exceptionExample
annotations.AtUnitExample2: assertFailureExample
如下是一个使用非嵌入式测试的例子,并且使用了断言,它将会对
// annotations/HashSetTest.java
// {java onjava.atunit.AtUnit
// build/classes/main/annotations/HashSetTest.class}
package annotations;
import java.util.*;
import onjava.atunit.*;
import onjava.*;
public class HashSetTest {
HashSet<String> testObject = new HashSet<>();
@Test
void initialization() {
assert testObject.isEmpty();
}
@Test
void _Contains() {
testObject.add("one");
assert testObject.contains("one");
}
@Test
void _Remove() {
testObject.add("one");
testObject.remove("one");
assert testObject.isEmpty();
}
}
采用继承的方式可能会更简单,也没有一些其他的约束。
对每一个单元测试而言,
// annotations/AtUnitExample3.java
// {java onjava.atunit.AtUnit
// build/classes/main/annotations/AtUnitExample3.class}
package annotations;
import onjava.atunit.*;
import onjava.*;
public class AtUnitExample3 {
private int n;
public AtUnitExample3(int n) { this.n = n; }
public int getN() { return n; }
public String methodOne() {
return "This is methodOne";
}
public int methodTwo() {
System.out.println("This is methodTwo");
return 2;
}
@TestObjectCreate
static AtUnitExample3 create() {
return new AtUnitExample3(47);
}
@Test
boolean initialization() { return n == 47; }
@Test
boolean methodOneTest() {
return methodOne().equals("This is methodOne");
}
@Test
boolean m2() { return methodTwo() == 2; }
}
输出为:
annotations.AtUnitExample3
. initialization
. m2 This is methodTwo
. methodOneTest
OK (3 tests)
有的时候,你需要向单元测试中增加一些字段。这时候可以使用String.split()
方法进行分割,从其中读取一个值,这个值将会被生成测试对象:
// annotations/AtUnitExample4.java
// {java onjava.atunit.AtUnit
// build/classes/main/annotations/AtUnitExample4.class}
// {VisuallyInspectOutput}
package annotations;
import java.util.*;
import onjava.atunit.*;
import onjava.*;
public class AtUnitExample4 {
static String theory = "All brontosauruses " +
"are thin at one end, much MUCH thicker in the " +
"middle, and then thin again at the far end.";
private String word;
private Random rand = new Random(); // Time-based seed
public AtUnitExample4(String word) {
this.word = word;
}
public String getWord() { return word; }
public String scrambleWord() {
List<Character> chars = Arrays.asList(
ConvertTo.boxed(word.toCharArray()));
Collections.shuffle(chars, rand);
StringBuilder result = new StringBuilder();
for(char ch : chars)
result.append(ch);
return result.toString();
}
@TestProperty
static List<String> input =
Arrays.asList(theory.split(" "));
@TestProperty
static Iterator<String> words = input.iterator();
@TestObjectCreate
static AtUnitExample4 create() {
if(words.hasNext())
return new AtUnitExample4(words.next());
else
return null;
}
@Test
boolean words() {
System.out.println("'" + getWord() + "'");
return getWord().equals("are");
}
@Test
boolean scramble1() {
// Use specific seed to get verifiable results:
rand = new Random(47);
System.out.println("'" + getWord() + "'");
String scrambled = scrambleWord();
System.out.println(scrambled);
return scrambled.equals("lAl");
}
@Test
boolean scramble2() {
rand = new Random(74);
System.out.println("'" + getWord() + "'");
String scrambled = scrambleWord();
System.out.println(scrambled);
return scrambled.equals("tsaeborornussu");
}
}
输出为:
annotations.AtUnitExample4
. words 'All'
(failed)
. scramble1 'brontosauruses'
ntsaueorosurbs
(failed)
. scramble2 'are'
are
(failed)
(3 tests)
>>> 3 FAILURES <<<
annotations.AtUnitExample4: words
annotations.AtUnitExample4: scramble1
annotations.AtUnitExample4: scramble2
如果你的测试对象需要执行某些初始化工作,并且使用完成之后还需要执行清理工作,那么可以选择使用
// annotations/AtUnitExample5.java
// {java onjava.atunit.AtUnit
// build/classes/main/annotations/AtUnitExample5.class}
package annotations;
import java.io.*;
import onjava.atunit.*;
import onjava.*;
public class AtUnitExample5 {
private String text;
public AtUnitExample5(String text) {
this.text = text;
}
@Override
public String toString() { return text; }
@TestProperty
static PrintWriter output;
@TestProperty
static int counter;
@TestObjectCreate
static AtUnitExample5 create() {
String id = Integer.toString(counter++);
try {
output = new PrintWriter("Test" + id + ".txt");
} catch(IOException e) {
throw new RuntimeException(e);
}
return new AtUnitExample5(id);
}
@TestObjectCleanup
static void cleanup(AtUnitExample5 tobj) {
System.out.println("Running cleanup");
output.close();
}
@Test
boolean test1() {
output.print("test1");
return true;
}
@Test
boolean test2() {
output.print("test2");
return true;
}
@Test
boolean test3() {
output.print("test3");
return true;
}
}
输出为:
annotations.AtUnitExample5
. test1
Running cleanup
. test3
Running cleanup
. test2
Running cleanup
OK (3 tests)
在输出中我们可以看到,清理方法会在每个测试方法结束之后自动运行。
在@Unit 中使用泛型
泛型为
下面是一个
// annotations/StackL.java
// A stack built on a LinkedList
package annotations;
import java.util.*;
public class StackL<T> {
private LinkedList<T> list = new LinkedList<>();
public void push(T v) { list.addFirst(v); }
public T top() { return list.getFirst(); }
public T pop() { return list.removeFirst(); }
}
为了测试
// annotations/StackLStringTst.java
// Applying @Unit to generics
// {java onjava.atunit.AtUnit
// build/classes/main/annotations/StackLStringTst.class}
package annotations;
import onjava.atunit.*;
import onjava.*;
public class
StackLStringTst extends StackL<String> {
@Test
void tPush() {
push("one");
assert top().equals("one");
push("two");
assert top().equals("two");
}
@Test
void tPop() {
push("one");
push("two");
assert pop().equals("two");
assert pop().equals("one");
}
@Test
void tTop() {
push("A");
push("B");
assert top().equals("B");
assert top().equals("B");
}
}
输出为:
annotations.StackLStringTst
. tTop
. tPush
. tPop
OK (3 tests)
这种方法存在的唯一缺点是,继承使我们失去了访问被测试的类中
实现@Unit
首先我们需要定义所有的注解类型。这些都是简单的标签,并且没有任何字段。
// onjava/atunit/TestObjectCreate.java
// The @Unit @TestObjectCreate tag
package onjava.atunit;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestObjectCreate {}
// onjava/atunit/TestObjectCleanup.java
// The @Unit @TestObjectCleanup tag
package onjava.atunit;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestObjectCleanup {}
// onjava/atunit/TestProperty.java
// The @Unit @TestProperty tag
package onjava.atunit;
import java.lang.annotation.*;
// Both fields and methods can be tagged as properties:
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestProperty {}
所有测试的保留属性都为
要实现系统并运行测试,我们还需要反射机制来提取注解。下面这个程序通过注解中的信息,决定如何构造测试对象,并在测试对象上运行测试。正是由于注解帮助,这个程序才会如此短小而直接:
// onjava/atunit/AtUnit.java
// An annotation-based unit-test framework
// {java onjava.atunit.AtUnit}
package onjava.atunit;
import java.lang.reflect.*;
import java.io.*;
import java.util.*;
import java.nio.file.*;
import java.util.stream.*;
import onjava.*;
public class AtUnit implements ProcessFiles.Strategy {
static Class<?> testClass;
static List<String> failedTests= new ArrayList<>();
static long testsRun = 0;
static long failures = 0;
public static void
main(String[] args) throws Exception {
ClassLoader.getSystemClassLoader()
.setDefaultAssertionStatus(true); // Enable assert
new ProcessFiles(new AtUnit(), "class").start(args);
if(failures == 0)
System.out.println("OK (" + testsRun + " tests)");
else {
System.out.println("(" + testsRun + " tests)");
System.out.println(
"\n>>> " + failures + " FAILURE" +
(failures > 1 ? "S" : "") + " <<<");
for(String failed : failedTests)
System.out.println(" " + failed);
}
}
@Override
public void process(File cFile) {
try {
String cName = ClassNameFinder.thisClass(
Files.readAllBytes(cFile.toPath()));
if(!cName.startsWith("public:"))
return;
cName = cName.split(":")[1];
if(!cName.contains("."))
return; // Ignore unpackaged classes
testClass = Class.forName(cName);
} catch(IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
TestMethods testMethods = new TestMethods();
Method creator = null;
Method cleanup = null;
for(Method m : testClass.getDeclaredMethods()) {
testMethods.addIfTestMethod(m);
if(creator == null)
creator = checkForCreatorMethod(m);
if(cleanup == null)
cleanup = checkForCleanupMethod(m);
}
if(testMethods.size() > 0) {
if(creator == null)
try {
if(!Modifier.isPublic(testClass
.getDeclaredConstructor()
.getModifiers())) {
System.out.println("Error: " + testClass +
" no-arg constructor must be public");
System.exit(1);
}
} catch(NoSuchMethodException e) {
// Synthesized no-arg constructor; OK
}
System.out.println(testClass.getName());
}
for(Method m : testMethods) {
System.out.print(" . " + m.getName() + " ");
try {
Object testObject = createTestObject(creator);
boolean success = false;
try {
if(m.getReturnType().equals(boolean.class))
success = (Boolean)m.invoke(testObject);
else {
m.invoke(testObject);
success = true; // If no assert fails
}
} catch(InvocationTargetException e) {
// Actual exception is inside e:
System.out.println(e.getCause());
}
System.out.println(success ? "" : "(failed)");
testsRun++;
if(!success) {
failures++;
failedTests.add(testClass.getName() +
": " + m.getName());
}
if(cleanup != null)
cleanup.invoke(testObject, testObject);
} catch(IllegalAccessException |
IllegalArgumentException |
InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
public static
class TestMethods extends ArrayList<Method> {
void addIfTestMethod(Method m) {
if(m.getAnnotation(Test.class) == null)
return;
if(!(m.getReturnType().equals(boolean.class) ||
m.getReturnType().equals(void.class)))
throw new RuntimeException("@Test method" +
" must return boolean or void");
m.setAccessible(true); // If it's private, etc.
add(m);
}
}
private static
Method checkForCreatorMethod(Method m) {
if(m.getAnnotation(TestObjectCreate.class) == null)
return null;
if(!m.getReturnType().equals(testClass))
throw new RuntimeException("@TestObjectCreate " +
"must return instance of Class to be tested");
if((m.getModifiers() &
java.lang.reflect.Modifier.STATIC) < 1)
throw new RuntimeException("@TestObjectCreate " +
"must be static.");
m.setAccessible(true);
return m;
}
private static
Method checkForCleanupMethod(Method m) {
if(m.getAnnotation(TestObjectCleanup.class) == null)
return null;
if(!m.getReturnType().equals(void.class))
throw new RuntimeException("@TestObjectCleanup " +
"must return void");
if((m.getModifiers() &
java.lang.reflect.Modifier.STATIC) < 1)
throw new RuntimeException("@TestObjectCleanup " +
"must be static.");
if(m.getParameterTypes().length == 0 ||
m.getParameterTypes()[0] != testClass)
throw new RuntimeException("@TestObjectCleanup " +
"must take an argument of the tested type.");
m.setAccessible(true);
return m;
}
private static Object
createTestObject(Method creator) {
if(creator != null) {
try {
return creator.invoke(testClass);
} catch(IllegalAccessException |
IllegalArgumentException |
InvocationTargetException e) {
throw new RuntimeException("Couldn't run " +
"@TestObject (creator) method.");
}
} else { // Use the no-arg constructor:
try {
return testClass.newInstance();
} catch(InstantiationException |
IllegalAccessException e) {
throw new RuntimeException(
"Couldn't create a test object. " +
"Try using a @TestObject method.");
}
}
}
}
虽然它可能是“过早的重构”(因为它只在书中使用过一次
// onjava/ProcessFiles.java
package onjava;
import java.io.*;
import java.nio.file.*;
public class ProcessFiles {
public interface Strategy {
void process(File file);
}
private Strategy strategy;
private String ext;
public ProcessFiles(Strategy strategy, String ext) {
this.strategy = strategy;
this.ext = ext;
}
public void start(String[] args) {
try {
if(args.length == 0)
processDirectoryTree(new File("."));
else
for(String arg : args) {
File fileArg = new File(arg);
if(fileArg.isDirectory())
processDirectoryTree(fileArg);
else {
// Allow user to leave off extension:
if(!arg.endsWith("." + ext))
arg += "." + ext;
strategy.process(
new File(arg).getCanonicalFile());
}
}
} catch(IOException e) {
throw new RuntimeException(e);
}
}
public void processDirectoryTree(File root) throws IOException {
PathMatcher matcher = FileSystems.getDefault()
.getPathMatcher("glob:**/*.{" + ext + "}");
Files.walk(root.toPath())
.filter(matcher::matches)
.forEach(p -> strategy.process(p.toFile()));
}
}
process()
方法。在这种方式下,
如下是一个简单的使用示例:
// annotations/DemoProcessFiles.java
import onjava.ProcessFiles;
public class DemoProcessFiles {
public static void main(String[] args) {
new ProcessFiles(file -> System.out.println(file),
"java").start(args);
}
}
输出为:
.\AtUnitExample1.java
.\AtUnitExample2.java
.\AtUnitExample3.java
.\AtUnitExample4.java
.\AtUnitExample5.java
.\AUComposition.java
.\AUExternalTest.java
.\database\Constraints.java
.\database\DBTable.java
.\database\Member.java
.\database\SQLInteger.java
.\database\SQLString.java
.\database\TableCreator.java
.\database\Uniqueness.java
.\DemoProcessFiles.java
.\HashSetTest.java
.\ifx\ExtractInterface.java
.\ifx\IfaceExtractorProcessor.java
.\ifx\Multiplier.java
.\PasswordUtils.java
.\simplest\Simple.java
.\simplest\SimpleProcessor.java
.\simplest\SimpleTest.java
.\SimulatingNull.java
.\StackL.java
.\StackLStringTst.java
.\Testable.java
.\UseCase.java
.\UseCaseTracker.java
如果没有命令行参数,这个程序会遍历当前的目录树。你还可以提供多个参数,这些参数可以是类文件(带或不带
回到我们对
ClassNameFinder.thisClass()
。 在这里,我们正在进入“字节码工程”领域,因为我们实际上正在分析类文件的内容:
// onjava/atunit/ClassNameFinder.java
// {java onjava.atunit.ClassNameFinder}
package onjava.atunit;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import onjava.*;
public class ClassNameFinder {
public static String thisClass(byte[] classBytes) {
Map<Integer,Integer> offsetTable = new HashMap<>();
Map<Integer,String> classNameTable = new HashMap<>();
try {
DataInputStream data = new DataInputStream(
new ByteArrayInputStream(classBytes));
int magic = data.readInt(); // 0xcafebabe
int minorVersion = data.readShort();
int majorVersion = data.readShort();
int constantPoolCount = data.readShort();
int[] constantPool = new int[constantPoolCount];
for(int i = 1; i < constantPoolCount; i++) {
int tag = data.read();
// int tableSize;
switch(tag) {
case 1: // UTF
int length = data.readShort();
char[] bytes = new char[length];
for(int k = 0; k < bytes.length; k++)
bytes[k] = (char)data.read();
String className = new String(bytes);
classNameTable.put(i, className);
break;
case 5: // LONG
case 6: // DOUBLE
data.readLong(); // discard 8 bytes
i++; // Special skip necessary
break;
case 7: // CLASS
int offset = data.readShort();
offsetTable.put(i, offset);
break;
case 8: // STRING
data.readShort(); // discard 2 bytes
break;
case 3: // INTEGER
case 4: // FLOAT
case 9: // FIELD_REF
case 10: // METHOD_REF
case 11: // INTERFACE_METHOD_REF
case 12: // NAME_AND_TYPE
case 18: // Invoke Dynamic
data.readInt(); // discard 4 bytes
break;
case 15: // Method Handle
data.readByte();
data.readShort();
break;
case 16: // Method Type
data.readShort();
break;
default:
throw
new RuntimeException("Bad tag " + tag);
}
}
short accessFlags = data.readShort();
String access = (accessFlags & 0x0001) == 0 ?
"nonpublic:" : "public:";
int thisClass = data.readShort();
int superClass = data.readShort();
return access + classNameTable.get(
offsetTable.get(thisClass)).replace('/', '.');
} catch(IOException | RuntimeException e) {
throw new RuntimeException(e);
}
}
// Demonstration:
public static void main(String[] args) throws Exception {
PathMatcher matcher = FileSystems.getDefault()
.getPathMatcher("glob:**/*.class");
// Walk the entire tree:
Files.walk(Paths.get("."))
.filter(matcher::matches)
.map(p -> {
try {
return thisClass(Files.readAllBytes(p));
} catch(Exception e) {
throw new RuntimeException(e);
}
})
.filter(s -> s.startsWith("public:"))
// .filter(s -> s.indexOf('$') >= 0)
.map(s -> s.split(":")[1])
.filter(s -> !s.startsWith("enums."))
.filter(s -> s.contains("."))
.forEach(System.out::println);
}
}
输出为:
onjava.ArrayShow
onjava.atunit.AtUnit$TestMethods
onjava.atunit.AtUnit
onjava.atunit.ClassNameFinder
onjava.atunit.Test
onjava.atunit.TestObjectCleanup
onjava.atunit.TestObjectCreate
onjava.atunit.TestProperty
onjava.BasicSupplier
onjava.CollectionMethodDifferences
onjava.ConvertTo
onjava.Count$Boolean
onjava.Count$Byte
onjava.Count$Character
onjava.Count$Double
onjava.Count$Float
onjava.Count$Integer
onjava.Count$Long
onjava.Count$Pboolean
onjava.Count$Pbyte
onjava.Count$Pchar
onjava.Count$Pdouble
onjava.Count$Pfloat
onjava.Count$Pint
onjava.Count$Plong
onjava.Count$Pshort
onjava.Count$Short
onjava.Count
onjava.CountingIntegerList
onjava.CountMap
onjava.Countries
onjava.Enums
onjava.FillMap
onjava.HTMLColors
onjava.MouseClick
onjava.Nap
onjava.Null
onjava.Operations
onjava.OSExecute
onjava.OSExecuteException
onjava.Pair
onjava.ProcessFiles$Strategy
onjava.ProcessFiles
onjava.Rand$Boolean
onjava.Rand$Byte
onjava.Rand$Character
onjava.Rand$Double
onjava.Rand$Float
onjava.Rand$Integer
onjava.Rand$Long
onjava.Rand$Pboolean
onjava.Rand$Pbyte
onjava.Rand$Pchar
onjava.Rand$Pdouble
onjava.Rand$Pfloat
onjava.Rand$Pint
onjava.Rand$Plong
onjava.Rand$Pshort
onjava.Rand$Short
onjava.Rand$String
onjava.Rand
onjava.Range
onjava.Repeat
onjava.RmDir
onjava.Sets
onjava.Stack
onjava.Suppliers
onjava.TimedAbort
onjava.Timer
onjava.Tuple
onjava.Tuple2
onjava.Tuple3
onjava.Tuple4
onjava.Tuple5
onjava.TypeCounter
虽然无法在这里介绍其中所有的细节,但是每个类文件都必须遵循一定的格式,而我已经尽力用有意义的字段来表示这些从
现在让我们回到Class.forName()
将其加载进来。现在我们可以对这个类进行
我们只需要关注三件事:首先是
每找到一个
本章小结
注解是
-
The Java designers coyly suggest that a mirror is where you find a reflection. ↩︎