Java自定义注解之编译时注解
java 自定义注解分为两种, 运行时注解和编译期注解。本篇文章主要讲的是编译期注解。先介绍下注解处理原理,JVM编译处理流程如下:
- 在命令行上指定的所有源文件被读取,解析为语法树,然后将所有外部可见定义输入到编译器的符号表中。
- 调用所有适当的注释处理器。如果任何注释处理器生成任何新的源或类文件,则重新启动编译,直到不创建新文件。
- 最后,解析器创建的语法树被分析并转换成类文件。在分析过程中,可能会发现对其他类的引用。编译器将检查这些类的源和类路径; 如果在源路径上找到这些文件,那么这些文件也将被编译,尽管它们不会被注释处理。
本篇文章引用源码默认为Java 8
处理流程详细介绍
Parse and Enter 阶段
源文件被scanner处理成unicode字符流,parser 读取字符流,使用TreeMaker构建语法树,语法树的节点都是JCTree的子类(这些子类都实现了com.sun.source.Tree的子接口)构建的。我们先看下包com.sun.source.tree
。
显然图中可以看出com.sun.source.tree
包中的内容是java程序的语法节点。
com.sun.source.Tree 包位于 JAVA_HOME/lib/tools.jar 中,如需使用,要把tools.jar 加到项目的classpath中。
com.sun.tools.javac.Main.compile()
是 javac的主入口。
每个生成的语法树都被传递给Enter(Enter 继承了JCTree及其子类的访问者类 visitor)。
graph TD state(Enter.main)-->state0(Enter.classEnter) state0(Enter.classEnter)-->|Enter.uncompleted|state1(MemberEnter1) state1(MemberEnter1)-->|MemberEnter.halfcompleted|state2(MemberEnter2) state2(MemberEnter2)-->|To Do|state3(Attribute and Generate)
- 在第一阶段,对所有类递归下降遍历tree及其成员类的tree。每个类都有一个 MemberEnter 对象作为处理结果返回。
- 将所有类中出现的符号输入到类自身的符号表中,所有类符号、类的参数类型符号(泛型参数类型)、超类符号和继承的接口类型符号等都存储到一个未处理的列表中。
- 将这个未处理的列表中所有的类都解析到各自的类符号列表中,这个操作是在MemberEnter.complete()中完成(默认构造器也是在这里完成的)。
代码关键流程如下:
1 | // 程序调试入口 |
词法分析主要借助com.sun.tools.javac.parser
包,语法分析主要借助了com.sun.tools.javac.tree
包。代码调用栈如下:
1 | JavaCompiler.parseFiles |
注解处理
这一部分主要由com.sun.tools.javac.processing.JavacProcessingEnvironment
处理。注释处理是编译之前的一个初步步骤。该初步步骤包括一系列轮次,每个轮次用于解析和输入源文件,然后确定并调用任何适当的注释处理器。在初始回合之后,如果任何被调用的注释处理器生成任何需要作为最终编译的一部分的新源文件或类文件,则将执行后续轮次。最后,当完成所有必要的轮次后,将执行实际编译。
实际上在解析了要编译的文件且确定其包含的注解后,才知道是否需要执行注解处理。因此,为了避免在没有执行注释处理的情况下不必要地解析和输入源文件, JavacProcessingEnvironment与概念模型有细微的差异,同时仍然满足在实际编译之前整个注释处理发生的概念要求。
分析生成
一旦指定的所有文件都已被解析并输入到编译器的符号表中,并且在结束了所有的注释处理,JavaCompiler继续分析语法树以生成相应的类文件。此阶段包括类型检查、控制流分析、泛型的类型擦除、去除语法糖、字节码生成等操作。
com.sun.tools.javac.comp.Attr
:检查语义的合理性并进行逻辑判断,类型是否匹配,是否初始化,泛型是否可推导,字符串常量合并com.sun.tools.javac.comp.Check
:协助attr,变量类型是否正确com.sun.tools.javac.comp.Resolve
:协助attr,变量方法类的访问是否合法,是否是静态变量com.sun.tools.javac.comp.ConstFold
:协助attr,常量折叠com.sun.tools.javac.comp.Infer
:协助attr,推导泛型com.sun.tools.javac.comp.Flow
: 数据流分析和替换等价源代码的分析(即上面的进一步语义分析)com.sun.tools.javac.comp.TransTypes
: 涉及泛型类型的代码将转换为没有泛型类型的代码com.sun.tools.javac.comp.Lower
: 处理语法糖”,Lower通过替换等效的,更简单的树来重写语法树以消除特定类型的子树com.sun.tools.javac.jvm.Gen
:遍历语法树生成最终的java字节码com.sun.tools.javac.jvm.Items
:辅助gen,这个类表示任何可寻址的操作项,这些操作项都可以作为一个单位出现在操作栈上com.sun.tools.javac.jvm.Code
:辅助gen,存储生成的字节码,并提供一些能够影射操作码的方法
一旦类被写为类文件,其大部分语法树和生成的字节码将不再需要。为了节省内存,将清除对树的这些部分和符号的引用,以允许垃圾收集器恢复内存。
功能实现
既然是自定义注解,那么首先是定义一个注解:
1 | (ElementType.TYPE) |
@Retention(RetentionPolicy.CLASS)
是必须的,这样才能在编译期获取注解信息。
接下来我们实现自定义的注解处理器:
1 | // 注解处理器支持的注解 |
如何应用注解
annotation maven demos
annotation 可以和maven 结合使用:
1 | <build> |
如果不使用maven,也可以通过java 提供的方式使用注解处理器。
- 处理器本身和注解必须已经在单独的编译中作为类进行编译,并出现在类路径中,因此,应该做的第一件事是:
1 | javac -proc:none info/victorchu/demos/processor/ClassInfoPrinterProcessor.java |
-proc:none
选项的意思是停用所有注解处理器,可选
- 然后,使用-processor 指定刚编译的注解处理器类,对源文件进行编译:
1 | javac -processor info.victorchu.demos.processor.ClassInfoPrinterProcessor Person.java |
要一次性指定多个注解处理器,可以用逗号分隔它们的类名,如下所示:
1 | javac -processor package1.Processor1,package2.Processor2 SourceFile.java |
如果不想在编译时指定,可以集合java 的SPI 机制,在META-INF/services/javax.annotation.processing.Processor
文件中加上想要使用的注解处理器类名。
应用场景
依赖 JSR-269 开发的典型的第三方库有,代码自动生成的 Lombok 和 Google Auto,代码检查的 Checker 和 Google Error Prone,编译阶段完成依赖注入的 Google Dagger 2 等。
实际应用中,由于字节码的版本问题,创建新文件会比修改java 语法树的方式更安全一些。
抽象注解处理器
JDK 提供了易于扩展的抽象注解处理器:
1 | // javax.annotation.processing.AbstractProcessor |
要实现我们自己的注解处理器,需要继承AbstractProcessor,并实现process方法。
注解处理上下文 ProcessingEnvironment
在实现process方法前,我们先了解下JDK 提供的工具类。ProcessingEnvironment 提供了编写新文件,报告错误信息和提供一些实用工具。Hotspot的实现类是com.sun.tools.javac.processing
包中的JavacProcessingEnvironment。
1 | public interface ProcessingEnvironment { |
filer
filter是个接口,hotspot的实现类是com.sun.tools.javac.processing
包中的JavacFiler。该接口支持注解处理器创建新文件。通过这种方式创建的文件会被实现类感知,并且能够更好地管理这些文件。一共有3种文件类型:源文件,类文件和辅助资源文件。注意。这样创建的文件(源文件,类文件)在写入流(write或outputStream) 被关闭后,才会被后面的注解处理器处理。辅助资源文件不会参见之后的注解处理。
1 | public interface Filer { |
element 和type涉及JDK对Java 编程语言的建模,将在后面的blog中详细介绍。
参考资料:
- jsr-269, 注解处理器
- compilation overview