Java 注解处理器

工作流程

注解处理器是一种应用于编译期间的模块,在编译完源文件后,编译器会解析类信息,转换成抽象语法树,接着执行注册的注解处理器,解析语法树是否发生了变化并重新生成源文件,接着调用下一个注解处理器。

编译器具体处理过程可查看 OpenJDK 官方文档: https://openjdk.java.net/groups/compiler/doc/compilation-overview/index.html

  1. 定义注解 创建注解处理器的第一步就是需要定义相关注解,并且在注解上定义 @Retention,当指定为 RetentionPolicy.SOURCE 时,该注解即在编译结束后会被擦除。

  2. 创建处理器 接着就需要创建处理器了,一般可通过继承 javax.annotation.processing.AbstractProcessor 定义处理器,由于注解处理器是通过反射获取的,所以需要提供无参构造函数。

  3. 设置解析注解 重写 getSupportedAnnotationTypes 或定义 @SupportedAnnotationTypes 将先前定义的注解类名加入解析目标,也可以使用 * 通配符。

  4. 指定支持版本 对于存在源文件版本需求的处理器,则可以通过重写 SupportedSourceVersion 或定义 @SupportedSourceVersion 来指定版本。

  5. 初始化处理器 注解处理器拥有许多可供使用的工具类,但是这些工具类需要通过 init 方法的 ProcessingEnvironment 才可获取,一般做法也是重写此方法,提取所需工具对象保存至处理器中。

  6. 实现处理流程 注解处理器的核心流程为 process 方法,可通过参数 RoundEnvironment 获取被注解标记的元素,实现想提供的功能,一般为 动态创建源文件修改语法树

  7. 注册处理器 注册注解处理器的方式可以在编译时通过 javac -processor 指定。 也可以配置自动加载,依照 ServiceLoader 形式,在 META-INF/services 下创建名为 javax.annotation.processing.Processor 的文件,将创建的注解处理器全类名填入,由于注解处理器是作用于编译期的,在编译时需要增加参数 -proc:none 以不使用注解处理器。

创建文件

文件的创建需要通过 javax.annotation.processing.Filer 来实现,可通过 init 方法获取。 通过 Filer 可以新建源文件并获取 JavaFileObject,以 Java 代码的方法将内容写入源文件。

MapStructJavaPoet 就是利用这个功能开发的工具。

修改语法树

对比创建文件,修改语法树则是十分复杂且麻烦的工作,同样要通过 init 获取语法树构造器 com.sun.source.util.Trees。 通过 Trees 可以将元素转换为语法树,并接受 Visitor 以进行语法树节点扫描和修改,一般监视器的实现可以通过继承 TreeTranslatorTreeScanner,对需要的方法进行重写。

而语法树操作的难点在于其他方面:

  1. 文档的缺少,这是无疑是对开发人员不友好的。

  2. 不稳定 api,有可能这个版本还能用的,在下一个版本就无效了。

  3. 未知的异常,可能由于操作失误而引发的,而且通过异常信息无法准确定位问题。

  4. 无法很好的控制注解处理器之间的顺序。

Lombok 就是一种修改语法树的工具。

这是我开发的一款通过修改语法树增加方法校验功能的工具: https://moyada.github.io/medivh/