14.4 编译时处理注解

14.4 编译时处理注解

APT(Annotation Processing Tool)是一种注解处理工具,它对源代码文件进行检测,并找出源文件所包含的注解信息,然后针对注解信息进行额外的处理。
使用APT工具处理注解时可以根据源文件中的注解生成额外的源文件和其他的文件(文件的具体内容由注解处理器的编写者决定),APT还会将生成的源代码文件和原来的源文件一起编译生成class文件。
使用APT的主要目的是简化开发者的工作量,因为APT可以在编译程序源代码的同时生成一些附属文件(比如源文件、类文件、程序发布描述文件等),这些附属文件的内容也都与源代码相关。换句话说,使用APT可以代替传统的对代码信息和附属文件的维护工作。
了解过Hibernate早期版本的读者都知道:每写一个Java类文件,还必须额外地维护一个Hibernate映射文件(名为*.hbm.xml的文件,也有一些工具可以自动生成)。下面将使用注解来简化这步操作。
不了解Hibernate的读者也无须担心,你只需要明白此处要做什么即可:
通过注解可以在Java源文件中放置一些注解,然后使用APT工具就可以根据该注解生成另一份XML文件,这就是注解的作用

通过javac命令的-processor选项 指定注解管理器

Java提供的javac.exe工具有一个-processor选项,该选项可指定一个注解处理器,如果在编译Java源文件时通过该选项指定了注解处理器,那么这个注解处理器将会在编译时提取并处理Java源文件中的注解。

如何实现注解管理器

每个注解处理器都需要实现Javax.annotation.processing包下的Processor接口。不过实现该接口必须实现它里面所有的方法,因此通常会采用继承AbstractProcessor的方式来实现注解处理器。一个注解处理器可以处理一种或者多种注解类型。

程序

为了示范使用APT根据源文件中的注解来生成额外的文件,下面将定义3种注解类型,分别用于修饰持久化类、标识属性和普通成员属性。

定义@Persistent注解

1
2
3
4
5
6
7
8
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Persistent {
String table();
}

这是一个非常简单的注解,它能修饰类、接口等类型声明,这个注解使用了@Retention元注解指定它仅在Java源文件中保留,运行时不能通过反射来读取该注解信息。

定义 @Id注解

下面是修饰标识属性的@Id注解。

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Id {
String column();

String type();

String generator();
}

这个@Id与前一个@Persistent的结构基本相似,只是多了两个成员变量而已。

定义 @Property注解

下面还有一个用于修饰普通成员属性的注解。

1
2
3
4
5
6
7
8
9
10
import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Property {
String column();

String type();
}

使用上面的注解

定义了这三个注解之后,下面提供一个简单的Java类文件,这个Java类文件使用这三个注解来修饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Persistent(table = "person_inf")
public class Person {
@Id(column = "person_id", type = "integer", generator = "identity")
private int id;
@Property(column = "person_name", type = "string")
private String name;
@Property(column = "person_age", type = "integer")
private int age;

// 无参数的构造器
public Person() {
}

// 初始化全部成员变量的构造器
public Person(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
// 下面省略所有成员变量的setter和getter方法
}

上面的Person类是一个非常普通的Java类,但这个普通的Java类中使用了@Persistent@Id@Property三个注解进行修饰。

处理注解的APT工具

下面为这三个注解提供一个APT工具,该工具的功能是根据注解来生成一个Hibernate映射文件(不懂Hibernate也没有关系,读者只需要明白可以根据这些注解来生成另一份XML文件即可)。

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
import javax.annotation.processing.*;
import javax.lang.model.element.*;
import javax.lang.model.*;

import java.io.*;
import java.util.*;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
// 指定可处理@Persistent、@Id、@Property三个注解
@SupportedAnnotationTypes({ "Persistent", "Id", "Property" })
public class HibernateAnnotationProcessor extends AbstractProcessor {
// 循环处理每个需要处理的程序对象
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 定义一个文件输出流,用于生成额外的文件
PrintStream ps = null;
try {
// 遍历每个被@Persistent修饰的class文件
for (Element t : roundEnv.getElementsAnnotatedWith(Persistent.class)) {
// 获取正在处理的类名
Name clazzName = t.getSimpleName();
// 获取类定义前的@Persistent注解
Persistent per = t.getAnnotation(Persistent.class);
// 创建文件输出流
ps = new PrintStream(new FileOutputStream(clazzName + ".hbm.xml"));
// 执行输出
ps.println("<?xml version=\"1.0\"?>");
ps.println("<!DOCTYPE hibernate-mapping PUBLIC");
ps.println(" \"-//Hibernate/Hibernate " + "Mapping DTD 3.0//EN\"");
ps.println(" \"http://www.hibernate.org/dtd/" + "hibernate-mapping-3.0.dtd\">");
ps.println("<hibernate-mapping>");
ps.print(" <class name=\"" + t);
// 输出per的table()的值
ps.println("\" table=\"" + per.table() + "\">");
for (Element f : t.getEnclosedElements()) {
// 只处理成员变量上的注解
if (f.getKind() == ElementKind.FIELD) // ①
{
// 获取成员变量定义前的@Id注解
Id id = f.getAnnotation(Id.class); // ②
// 当@Id注解存在时输出<id.../>元素
if (id != null) {
ps.println(" <id name=\"" + f.getSimpleName() + "\" column=\"" + id.column()
+ "\" type=\"" + id.type() + "\">");
ps.println(" <generator class=\"" + id.generator() + "\"/>");
ps.println(" </id>");
}
// 获取成员变量定义前的@Property注解
Property p = f.getAnnotation(Property.class); // ③
// 当@Property注解存在时输出<property.../>元素
if (p != null) {
ps.println(" <property name=\"" + f.getSimpleName() + "\" column=\"" + p.column()
+ "\" type=\"" + p.type() + "\"/>");
}
}
}
ps.println(" </class>");
ps.println("</hibernate-mapping>");
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (ps != null) {
try {
ps.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
return true;
}
}

上面的注解处理器其实非常简单,与前面通过反射来获取注解信息不同的是,这个注解处理器使用RoundEnvironment来获取注解信息。

获取需要处理的程序单元

RoundEnvironment里包含了一个getElementsAnnotatedWith()方法,可根据注解获取需要处理的程序单元,这个程序单元由Element代表。

程序处理单元Element介绍

Element里包含一个getKind()方法,该方法返回Element所代表的程序单元,返回值可以是ElementKind.CLASS(类)、ElementKind.FIELD(成员变量)……
除此之外,Element还包含一个getEnclosedElements()方法,该方法可用于获取该Element里定义的所有程序单元,包括成员变量、方法、构造器、内部类等。

接下来程序只处理成员变量前面的注解,因此程序先判断这个Element必须是ElementKind.FIELD如上程序中①号粗体字代码所示)
再接下来程序调用了Element提供的getAnnotation(Class clazz)方法来获取修饰该Element的注解,如上程序中②③号粗体字部分就是获取成员变量上注解对象的代码。获取到成员变量上的@Id@Property注解之后,接下来就根据它们提供的信息执行输出。

编译时指定APT

提供了上面的注解处理器类之后,接下来就可使用带-processor选项的javac.exe命令来编译Person.java了。
例如如下命令,

1
2
3
rem 使用HibernateAnnotationProcessor作为APT处理Person.java中的Annotation
javac *.java
javac -processor HibernateAnnotationProcessor Person.java

使用HibernateAnnotationProcessor作为APT处理Person.java中的Annotation

通过上面的命令编译Person.java后,将可以看到在相同路径下生成了一个Person.hbm.xml文件该文件就是根据Person.java里的注解生成的。该文件的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="Person" table="person_inf">
<id name="id" column="person_id" type="integer">
<generator class="identity"/>
</id>
<property name="name" column="person_name" type="string"/>
<property name="age" column="person_age" type="integer"/>
</class>
</hibernate-mapping>

对比上面XML文件中的粗体字部分与Person.Java中的注解部分,它们是完全对应的,这即表明这份XML文件是根据Person.Java中的注解生成的。从生成的这份XML文件可以看出,通过使用APT工具确实可以简化程序开发,程序员只需把一些关键信息通过注解写在程序中,然后使用APT工具就可生成额外的文件