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; } }
|
上面的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)
@SupportedAnnotationTypes({ "Persistent", "Id", "Property" }) public class HibernateAnnotationProcessor extends AbstractProcessor { public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { PrintStream ps = null; try { for (Element t : roundEnv.getElementsAnnotatedWith(Persistent.class)) { Name clazzName = t.getSimpleName(); 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); ps.println("\" table=\"" + per.table() + "\">"); for (Element f : t.getEnclosedElements()) { if (f.getKind() == ElementKind.FIELD) { Id id = f.getAnnotation(Id.class); 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 p = f.getAnnotation(Property.class); 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
| 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
工具就可生成额外的文件