从微服务构建开始的初期,就在团队内部推行使用Lombok,在开发效率上确实有所提升,简化了代码。在开始本篇内容前,先简单介绍下,lombok是一个可以通过简单的注解的形式来帮助我们简化消除一些必须有但显得很臃肿的 Java 代码的工具,简单来说,比如我们新建了一个类,然后在其中写了几个字段,然后通常情况下我们需要手动去建立getter和setter方法啊,构造函数啊之类的,lombok的作用就是为了省去我们手动创建这些代码的麻烦,它能够在我们编译源码的时候自动帮我们生成这些方法。
那么你有好奇过Lombok到底是如何做到这些的呢?其实底层就是用到了编译时注解的功能。先带大家一睹它的风采,还是以我之前写插件化动态加载设计的demo为例,源码如下:
编译后的class文件为:
只需要一个简单的@Data注解即可生成getter和setter等方法,当然Lombok的功能不止如此,还有很多其他的注解帮助我们简便开发,网上有许多的关于Lombok的使用方法,这里就不再啰嗦了。正常情况下我们在项目中自定义注解,或者使用Spring框架中@Controller、@Service等等这类注解都是运行时注解,运行时注解大部分都是通过反射来实现的。而Lombok是使用编译时注解实现的。那么编译时注解是什么呢?
核心之处就是对于注解的解析上。JDK5引入了注解的同时,也提供了两种解析方式。
运行时解析
运行时能够解析的注解,必须将@Retention设置为RUNTIME,这样就可以通过反射拿到该注解。java.lang,reflect反射包中提供了一个接口AnnotatedElement,该接口定义了获取注解信息的几个方法,Class、Constructor、Field、Method、Package等都实现了该接口,对反射熟悉的朋友应该都会很熟悉这种解析方式。
编译时解析
编译时解析有两种机制,分别简单描述下:
1)Annotation Processing Tool
apt自JDK5产生,JDK7已标记为过期,不推荐使用,JDK8中已彻底删除,自JDK6开始,可以使用Pluggable Annotation Processing API来替换它,apt被替换主要有2点原因:
api都在com.sun.mirror非标准包下
没有集成到javac中,需要额外运行
2)Pluggable Annotation Processing API
JSR 269自JDK6加入,作为apt的替代方案,它解决了apt的两个问题,javac在执行的时候会调用实现了该API的程序,这样我们就可以对编译器做一些增强,这时javac执行的过程如下:
对于这个 Java 编译器的工作流程,这里就直接引用官网的总结:
Parse and Enter:所有在命令行中指定的源文件都被读取,解析成语法树,然后所有外部可见的定义都被输入到编译器的符号表中。
Annotation Processing:调用所有适当的注释处理器。如果任何注释处理程序生成任何新的源文件或类文件,则重新开始编译,直到没有创建任何新文件为止。
Analyse and Generate:最后,解析器创建的语法树将被分析并转换为类文件。在分析过程中,可能会发现对其他类的引用。编译器将检查这些类的源和类路径,如果在源路径上找到它们,也会编译这些文件,尽管它们不需要进行注释处理。
以上补充了那么多的知识,就是为了接下来对Lombok的源码做分析用。通过上面的分析我们可以大致确定 Lombok 底层的实现应该是通过修改已有的 Java 源文件(准确来说修改是 AST)来完成的,那他到底是怎么实现的呢?接下来会通过@Setter 进行剖析。以下是对源码分析过程Lombok实现的流程图:
接下来就从源码角度对其实现的过程做分析,我们都知道当我们在自定义一个 APT 的时候需要继承 AbstractProcessor ,并实现其最核心的 process 方法来对当前轮编译的结果进行处理,在 Lombok 中也不例外,Lombok 也是通过一个顶层的 Processor 来接收当前轮的编译结果,而这个 Processor 就是 LombokProcessor ,所以我们第一步直接进入到这个类的 process 方法中。
在 process 方法中最重要的一段代码就是上面这段,继续跟踪源码:
JavacTransformer.java
HandlerLibrary.java
JavacAST.java
JavacNode.java
整体流程下来,比较关键的其实就是最后的 traverse 方法,通过该方法完成了对整棵 AST 树的深度优先遍历,对每个节点调用其对应的 visit 方法。到这里我们是不是已经大概能猜到 Lombok 的操作方式了,通过上面的代码 Lombok 已经获取到了 javac 的 AST ,并对其进行了遍历,可是也就是在这里 Lombok 已经超出正常的API操作权限,因为正常来说 javac 中的 JavacAST 和 JavacNode 等这些 Javac 的类是属于内部类,对外部不可见的,Lombok 在这里依赖这些内部的实现。
到这里,接下来的流程相对容易了些,继续看源码:
标准JavacASTVisitor适配器实现,接口上的每个方法以空方法体实现,仅需覆盖需要使用的方法
JavacTransformer.java
内部类AnnotationVisitor继承了适配器:
重点来了,对 handlers 变量调用了 handleAnnotation 方法,这里需要注意的是变量 handlers 的类型就是 HandlerLibrary 。
AnnotationHandlerContainer 为 HandlerLibrary 内部类
JavacAnnotationHandler.java
JavacAnnotationHandler 为所有 Handler 的基类,所有 Handler 应在 handle 方法中处理 AST。当我们对每个 JavacNode 节点调用 visit* 方法时,其实就已经将 AST 的相关信息传递到了 Handler ,在 HandlerLibrary 类中会接着进行一连串的验证和调用,但归根到底最终调用了抽象类 JavacAnnotationHandler 的 handle 方法。
接下来下面代码的逻辑就是根据不同的注解选择不同的 Handler 来进行处理,父类 JavacAnnotationHandler 中的泛型 T 就表示当前 Handler 所关注的注解类型,如 HandleSetter 所关注的就是 Setter 注解。而通过下面的代码我们也可以看到,在 HandleSetter 类中会首先通过 handle 方法来接收相关的 JavacNode 和 AST 信息,然后会根据不同的逻辑来使用 TreeMaker 来构建方法体,并最终组装成 JCMethodDecl ,最后通过 JavacNode 的 add 方法将新组装好的方法添加到 AST 中,这样就完成了对 AST 的修改。
至此,整个流程已经分析结束,更细节的代码可以参考源码。最后再用一个流程图来总结:
通过本篇的学习带你了解了Lombok的底层实现,巧用了javac未公开的API来动态修改AST的目的,从而实现向java源码中添加代码的效果。但也正是因为这个原因,导致Lombok过份的依赖JDK的实现,这也是其弊端吧。