1. 前言
上一篇 主要介绍了什么是 注解 (Annotation) 以及如何读取 运行时注解 中的数据, 同时用注解实现了简单的 ORM
功能. 这次介绍另一部分: 如何读取 编译时注解 ( RetentionPolicy.SOURCE )
2. 作用
编译时注解可以用来动态生成代码. 使用 SOURCE
类型注解的代码会在编译时被解析, 生成新的 java
文件, 然后和原来的 java
文件一起编译成字节码. 由于不使用反射功能, 编译时注解不会拖累性能, 因而被许多框架使用, 比如 Butter Knife
, Dragger2
等.
3. 例子
1. 代码
还是从简单的例子开始看. 这里要做的是生成一个 java 类, 其拥有一个打印注解信息的方法.
先定义一个注解
package apt;
......
@Retention(RetentionPolicy.SOURCE) // 注解只在源码中保留
@Target(ElementType.TYPE) // 用于修饰类
public @interface Hello {
String name() default "";
}
使用注解的类
package apt;
@Hello(name = "world")
public class Player {
}
不使用注解的类, 用于对比
package apt;
public class Ignored {
}
上一篇说过, 注解没有行为, 只有数据, 需要对应的处理器才能发挥作用. javac
提供了解析编译时注解的注解处理器 ( Annotation Processor ). 对于自定义的注解, 需要手动实现它的注解处理器.下面来看一个简单的注解处理器实现.
package apt;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;
/**
* Created by away on 2017/6/12.
*/
@SupportedSourceVersion(SourceVersion.RELEASE_8) // 源码级别, 这里的环境是 jdk 1.8
@SupportedAnnotationTypes("apt.Hello") // 处理的注解类型, 这里需要处理的是 apt 包下的 Hello 注解(这里也可以不用注解, 改成重写父类中对应的两个方法)
public class HelloProcessor extends AbstractProcessor {
// 计数器, 用于计算 process() 方法运行了几次
private int count = 1;
// 用于写文件
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
}
// 处理编译时注解的方法
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("start process, count = " + count++);
// 获得所有类
Set<? extends Element> rootElements = roundEnv.getRootElements();
System.out.println("all class:");
for (Element rootElement : rootElements) {
System.out.println(" " + rootElement.getSimpleName());
}
// 获得有注解的元素, 这里 Hello 只能修饰类, 所以只有类
Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Hello.class);
System.out.println("annotated class:");
for (Element element : elementsAnnotatedWith) {
String className = element.getSimpleName().toString();
System.out.println(" " + className);
String output = element.getAnnotation(Hello.class).name();
// 产生的动态类的名字
String newClassName = className + "_New";
// 写 java 文件
createFile(newClassName, output);
}
return true;
}
private void createFile(String className, String output) {
StringBuilder cls = new StringBuilder();
cls.append("package apt;\n\npublic class ")
.append(className)
.append(" {\n public static void main(String[] args) {\n")
.append(" System.out.println(\"")
.append(output)
.append("\");\n }\n}");
try {
JavaFileObject sourceFile = filer.createSourceFile("apt." + className);
Writer writer = sourceFile.openWriter();
writer.write(cls.toString());
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
代码的逻辑很简单:
获得所有标有注解的类
取出注解中的信息
生成新的 java 文件
这里只需要知道, 自定义注解处理器要继承 AbstractProcessor
类, 并重写 process
方法.
2. 运行
此时项目目录如下, 这里 out
目录为手动创建
-
out
-
production
apt
-
-
src
apt
在命令行中进入项目根目录, 即 src
文件夹的上一层.
首先编译注解处理器: javac -encoding UTF-8 -d out\production\ src\apt\HelloProcessor.java src\apt\Hello.java
接着执行注解处理器: javac -encoding UTF-8 -cp out\production\ -processor apt.HelloProcessor -d out\production -s src\ src\apt\*.java
得到如下输出
start process, count = 1
all class:
Hello
HelloProcessor
Ignored
Player
annotated class:
Player
start process, count = 2
all class:
Player_New
annotated class:
start process, count = 3
all class:
annotated class:
这时 src/apt
目录下会出现新的 Player_New.java
文件, 内容如下
package apt;
public class Player_New {
public static void main(String[] args) {
System.out.println("world");
}
}
执行 java -cp out\production\elevator apt.Player_New
得到输出 world
.
到这里, 编译时注解便处理成功了. 我们定义了一个极其简单的注解处理器, 读取了注解信息, 并生成了新的 java
类来打印该信息.
这里可能会报一个错误
编译器 (1.8.0_131) 中出现异常错误。如果在 Bug Database (http://bugs.java.com) 中没有找到该错误, 请通过 Java Bug 报告页 (http://bugreport.java.com) 建立该 Java 编译器 Bug。请在报告中附上您的程序和以下诊断信息。谢谢。
java.lang.IllegalStateException: endPosTable already set
...
...
这时把产生的 Player_New.java
文件删去重新执行注解处理器就好了
3. javac
这里稍微解释一下 javac
命令, IDE
用多了, 写的时候都忘得差不多了 (:зゝ∠)javac
用于启动 java 编译器, 格式为 javac <options> <source files>
, 其中 <options>
的格式为 -xx xxxx
, 都是配对出现的, 用于指定一些信息.
这里 <options>
的位置并没有讲究, 只要在 javac
后面就行了, 在两个 xxx.java
之间出现也是可以的, 比如: javac -d out\production\ src\apt\HelloProcessor.java -encoding UTF-8 src\apt\Hello.java
正常执行.
一些 <option>
-
-cp <路径>
和
-classpath <路径>
一样, 用于指定查找用户类文件和注释处理程序的位置
-
-d <目录>
指定放置生成的类文件的位置
-
-s <目录>
指定放置生成的源文件的位置
-
-processorpath <路径>
指定查找注释处理程序的位置
不写的话会使用
-cp
的位置
-
-processor <class1>[,<class2>,<class3>...]
要运行的注释处理程序的名称; 绕过默认的搜索进程
4. 问题
到这里应该会有一些问题, 比如
AbstractProcessor
,Elememt
分别是什么process
为什么执行了 3 次运行注解处理器的时候会启动
jvm
吗
这里先说一下第三个问题. javac
运行注解处理器的时候, 会开一个完整的 java 虚拟机执行代码, 所以自定义的注解处理器是可以使用各种类库的.
接下来讲一下一些基本概念, 用来回答上面两个问题.
4.概念
1. AbstractProcessor
这是处理器的API,所有的处理器都是基于
AbstractProcessor
, 它实现了接口Processor
-
接口
-
void init(ProcessingEnvironment processingEnv)
;会被注解处理工具调用,
ProcessingEnvironment
提供了一些实用的工具类Elements
,Types
和Filer
.
-
boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
;相当于
main
函数, 是注解处理器的入口. 输入参数RoundEnviroment
可以查询出包含特定注解的被注解元素
-
SourceVersion getSupportedSourceVersion()
;用来指定使用的
java
版本
-
Set<String> getSupportedAnnotationTypes()
;指定这个注解处理器是注册给哪个注解的, 这里需要用注解的全称, 比如上面的
apt.Hello
最后两个也可以用注解的形式实现, 例子中的代码就是这么做的
-
2. Element
程序的元素, 例如包, 类或者方法. 每个
Element
代表一个静态的, 语言级别的构件. 可以参考下面的代码理解
package com.example; // PackageElement
public class Foo { // TypeElement
private int a; // VariableElement
private Foo other; // VariableElement
public Foo () {} // ExecuteableElement
public void setA ( // ExecuteableElement
int newA // TypeElement
) {}
}
由此可见 roundEnv.getElementsAnnotatedWith(xxx.class)
得到的并不一定是类, 也可能是方法, 成员变量等, 只是例子中用的注解只能用于修饰类.
3. 注解处理器的执行
javadoc
中对此的描述如下
Annotation processing happens in a sequence of rounds. On each round, a processor may be asked to process a subset of the annotations found on the source and class files produced by a prior round. The inputs to the first round of processing are the initial inputs to a run of the tool; these initial inputs can be regarded as the output of a virtual zeroth round of processing.
概况来说, 就是 process() 方法会被调用多次, 直到没有新的类产生为止.
因为新生成的文件中也可能包含 @Hello 注解,它们会继续被 HelloProcessor
处理.
Round | input | output |
---|---|---|
1 | Hello.java HelloProcessor.java Ignored.java Player.java |
Player_New.java |
2 | Player_New.java | - |
3 | - | - |
下一篇会开始分析 Butter Knife
的源码.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。