第20章 注解

注解(元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻方便使用这些数据


从JDK 1.6开始提供了一组插入式注解处理器的标准API在编译期间对注解进行处理,有了这些API之后,我们的代码就可能干涉编译器的行为,对于语法树中的任意元素,甚至包括代码注释都可以在插件中访问到

注解使得我们能

  • 以将由编译器来测试和验证的格式,存储有关程序的额外信息
  • 可以生成描述符文件,甚至或是新的类定义,有助于减轻编写样板的负担
  • 将元数据保存在Java源代码中,并构造处理工具
  • 简单易读

Java内置了三种注解

  • @Override:定义覆盖超类的方法,如果签名对不上,就会出错
  • @Deprecated:如果使用了注解为它的元素,就会发出警告
  • @SuppressWarnings:关闭不当的编译器警告信息

基本语法

与其他任何Java接口一样,注解也会编译成class文件

1
2
3
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {}

上面就是最简单的标记注解(没有元素),@Target @Retention是两个元注解,第一个定义了注解应用在什么地方(构造器、域、局部变量、方法、包、参数、类、接口等),第二个定义注解在哪个级别可用(SOURCE、CLASS、RUNTIME

1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
public int id();
public String desciption() default "no description";
}

上面的注解可以用来跟踪一个项目的用例,如果一个方法实现了某个需求,程序员就可以为该方法加上注解,如下所示

1
2
3
4
5
6
7
public class PasswordUtils {
@UseCase(id = 47,description =
"Password must contain at least one numeric")
public boolean validatePassword(String password) {
return password.matches("\\w*\\d\\w*");
}
}

编写注解处理器

如果没有读取注解的工具,那注解和注释并没与两样,因此使用注解的很大一部分工作就是创建和使用注解处理器,底层实现是反射机制的API,同时还提供apt帮助解析带有注解的源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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() +
" " + uc.description());
useCases.remove(new Integer(uc.id()));
}
}
for(int i : useCases) {
System.out.println("Warning:Missing use case-" + i);
}
}
public static void main(String[] args) {
List<Integer> useCases = new ArrayList<Integer>();
Collections.addAll(useCases, 47,48,49,50);
trackUseCases(useCases,PasswordUtils.class);
}
}

//运行结果
Found Use Case:47 Password must contain at least one numeric
Warning:Missing use case-48
Warning:Missing use case-49
Warning:Missing use case-50

上面就是简单的注解处理器,将用它来读取PasswordUtils类,先提供一组id,它会列出类中找到的用例和缺失的用例

这个程序用到了两个反射方法:getDeclaredMethods()getAnnotation(),它们都属于AnnotatedElement接口(Class、Method和Field等类都实现了该接口??),getAnnotation()就是返回指定类型的注解对象,然后通过调用注解对象的方法来提取元素的值

注解元素可用类型有所有基本类型、String、Class、enum、Annotation、以上类型数组。由此可见注解可以嵌套,这将非常有用

编译器对于注解元素的默认值有很大的限制,要么元素具有默认值,要么在使用注解时提供值,不能用null作为值,这使得处理器很难表现一个元素的存在或缺失的状态,为了绕开这个限制,只能使用空字符串或负数来表示元素不存在

生成外部文件(ORM)

有些framework需要一些额外的信息来能和源代码协同工作,如String中的ORM(Mybatis、Hibernate),这就是注解的价值所在!

假如希望提供一些基本的对象/关系映射功能,能够自动生成数据库表,用以存储Javabean对象,可以选择XML描述文件,指明类的名字,每个成员以及数据库映射的相关信息,但是如果使用注解,所有信息都保存在Javabean源文件中,为此只要一些新的注解,用来定义与Javabean关联的数据库表的名字,以及与Javabean属性关联的列的名字和SQL类型

1
2
3
4
5
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
public String name() default "";
}

该注解通过该元素为处理器创建数据库表提供表的名字

1
2
3
4
5
6
7
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
boolean primaryKey() default false;
boolean allowNULL() default true;
boolean unique() default false;
}

注解处理器通过Constraints提取出数据库表的元数据,虽然相对于数据库能提供的属性来说,只是一部分

1
2
3
4
5
6
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
String name() default "";
Constraints constraints() default @Constraints;
}
1
2
3
4
5
6
7
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
int value() default 0;
String name() default "";
Constraints constraints() default @Constraints;
}

上面两个注解定义的是SQL类型,Constraints constraints() default @Constraints;这里即采取了嵌套注解,默认值是@Constraints,当然也可以这样定义Constraints constraints() default @Constraints(unique=true);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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 handle;

static int memberCount;
public String getHandle() { return handle; }
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public String toString() { return handle; }
public Integer getAge() { return age; }
}

上述代码则应用了之前定义的注解,当然也有不同的方式来实现上面的功能,那种灵活且简洁就选那种

注解目前不支持继承

下面的代码实现了上面的定义的注解的处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import java.util.List;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;

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<?> class1 = Class.forName(className);
DBTable dbTable = class1.getAnnotation(DBTable.class);
if(dbTable == null) {
System.out.println(
"No Dbtable annotations in class " + className);
continue;
}
String tableName = dbTable.name();
if(tableName.length() < 1) {
tableName = class1.getName().toUpperCase();
}

List<String> columnDefs = new ArrayList<String>();
for(Field field : class1.getDeclaredFields()) {
String columnName = null;
Annotation[] annotations = field.getDeclaredAnnotations();
if(annotations.length < 1)
continue;
if(annotations[0] instanceof SQLInteger) {
SQLInteger sqlInteger = (SQLInteger)annotations[0];
if(sqlInteger.name().length() < 1)
columnName = field.getName().toUpperCase();
else
columnName = sqlInteger.name();
columnDefs.add(columnName + "INT" +
getConstraints(sqlInteger.constraints()));
}
if(annotations[0] instanceof SQLString) {
SQLString sqlString = (SQLString)annotations[0];
if(sqlString.name().length() < 1)
columnName = field.getName().toUpperCase();
else
columnName = sqlString.name();
columnDefs.add(columnName + " VARCHAR(" +
sqlString.value() + ")" +
getConstraints(sqlString.constraints()));
}
StringBuilder createCommand = new StringBuilder(
"CREATE TABLE " + tableName + "(");
for(String columnDef : columnDefs)
createCommand.append("\n " + columnDef + ",");
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.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30));
Table Creation SQL for annotations.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50));
Table Creation SQL for annotations.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50),
AGE INT);
Table Creation SQL for annotations.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50),
AGE INT,
HANDLE VARCHAR(30) PRIMARY KEY);

虽然上面的代码对现在的自己来说大开眼界!但是对于真正的对象/关系映射框架来说,还是太幼稚!例如使用@DBTable注解给出表的名字,如果要修改表的名字,就要重新编译Java代码,这显然不是希望看到的!

详细的对象/关系映射框架参见《Spring-数据访问》