前言
对于一个Java开发者来说,Lombok应该是使用最多的插件之一了,他提供了一系列注解来帮助我们减轻对重复代码的编写,例如实体类中大量的setter,getter方法,各种IO流等资源的关闭、try…catch…finally模版等,虽然可以通过IDE的快捷帮我们生成这些方法,但这些冗长的代码仍会影响代码的简洁性与可阅读性。
如今,随着使用者数量越来越多,Lombok甚至成为IDEA的内置插件了(2020.3 版本+),可见其影响力。
但不知道使用Lombok的你,是否思考过这种自动生成代码究竟是什么原理呢?这些代码又是怎么产生的呢?而这就是本文要展开介绍的内容了。
Lombok的安装与使用网络上相关的介绍已经很多了,这里就不多说了,自行查阅相关资料即可。
注解解析的两种方式
关于注解,我在之前的文章里有过详细的介绍,在解释Lombok的原理之前,推荐你先阅读《给编译器看的注释——「注解」》
这里主要回顾一下Lombok注解的解析方式。
运行时解析
这是最常见的注解解析的方式,运行时能够解析的注解,必须将@Retention
设置为RUNTIME
,这样就可以通过反射拿到该注解。
例如使用最多的@RequestMapping
注解就是一个运行时注解。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
String name() default "";
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
RequestMethod[] method() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}
运行时注解的解析原理也很简单,在java.lang,reflect反射包中提供了一个接口AnnotatedElement,该接口定义了获取注解信息的几个方法,Class、Constructor、Field、Method、Package等都实现了该接口,对反射熟悉的朋友应该都会很熟悉这种解析方式。相关的例子在之前的文章中有介绍过,这里不赘述了。
那Lombok的注解也是这种原理吗?
翻开源码,我们可以看到@Data
这个接口RetentionPolicy是SOURCE级别的,也就是说,在代码编译的时候,相关的注解信息就已经丢掉了,并不会被加载进JVM里,那么为什么我们又会在Compile代码的时候看见那些get/set方法呢?这就不得不说另一种注解解析方式了——编译时解析
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
String staticConstructor() default "";
}
编译时解析
Javac 编译器的编译过程大致可分为 1个准备过程3个处理过程 :
- 初始化插入式注解处理器
- 解析与填充符号表;
- 插入式注解处理器的注解处理;
- 分析与字节码生成。
这 3 个步骤之间的关系如下图所示:
而其中编译期的注解处理有两种机制,分别简单描述下:
Annotation Processing Tool(APT)
APT自JDK5产生,JDK7已标记为过期,不推荐使用,JDK8中已彻底删除,自JDK6开始,可以使用Pluggable Annotation Processing API来替换它,APT被替换主要有2点原因:
- 相关API都在com.sun.mirror非标准包下
- 没有集成到javac中,需要额外运行
Pluggable Annotation Processing API(插件式注解处理器)
Java6开始纳入了JSR-269规范:Pluggable Annotation Processing API(插件式注解处理器)。作为APT的替代方案,JSR-269提供一套标准API来处理Annotations,并解决了刚才提到的APT的两个问题。
在使用javac的过程中,它产生作用的具体流程如下所示
- javac对源代码进行分析,生成了一棵抽象语法树(AST)
- 运行过程中调用实现了「JSR 269 API」的Lombok程序
- 此时Lombok就对第一步骤得到的AST进行处理,找到@Data注解所在类对应的语法树(AST),然后修改该语法树(AST),增加getter和setter方法定义的相应树节点
- javac使用修改后的抽象语法树(AST)生成字节码文件,即给class增加新的节点(代码块)
AST是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。
想要实现一个基于「JSR 269 API」的程序也很容易,具体来说,我们只需要继承AbstractProcessor
类,重写process()
方法实现自己的注解处理逻辑,并且在META-INF/services
目录下创建javax.annotation.processing.Processor
文件注册自己实现的Annotation Processor,在javac编译过程中编译器便会调用我们实现的Annotation Processor,从而使得我们有机会对java编译过程中生产的抽象语法树进行修改。
对于Java编译的一些补充
对于Java的编译期,其实是一个相对模糊的概念,需要针对具体的情况具体分析。
- 将
*.java
文件转为*.class
的过程称为编译器的前端(前端编译)。例如:JDK的javac编译器。 - 把字节码(
*.class
文件) 转变为 本地机器码 的过程称为Java虚拟机的即时编译运行期(JIT编译器,Just In Time)。例如:HotSpot虚拟机的C1、C2编译器。 - 使用静态的提前编译器(AOT编译器,Ahead Of Time Compiler)直接把程序变异成与目标及其指令集相关的二进制代码的过程。例如:JDK的Jaotc。
他们之间的关系大约是
javac把 *.java
文件编译成*.class
文件,*.class
文件进入JVM后,通过JIT编译器将*.class
文件解释为对应的机器码。而AOT则是直接把*.class
文件编译系统的库文件,不再依靠JIT去做这个事情。
Javac 这类编译器对代码的 运行效率几乎没有任何优化措施,但由于该阶段离程序员编码是最近的(相较于JIT而言),所以对于程序员编码来说,前端编译器在编译期的优化更加密切,许多新生的 Java 语法特性,都是靠编译器的 「语法糖」(自动装箱、拆箱与遍历循环)来实现的,这是因为 Javac 做了很多针对 Java 语言编码过程的优化措施来改善程序员的编码风格、提升编码效率,而不是依赖虚拟机的底层改进来支持。