背景
一直在使用Lombok以及MapStruct,但是对于它们能够在编译阶段直接生成实例代码却没有仔细了解过。最近刚好在部门内做了一次分享,也在这里对具体原理做一个详细阐述。
Lombok以及MapStruct实现大体思路
Lombok以及MapStruct都是通过在目标代码上标记注解,编译器能够根据注解生成对应的实现代码。比如Lombok在属性上标记@Getter
,那么在这个Java Bean
内就会生成对应属性的get
方法。
本质上来说,不管是Lombok或者MapStruct,都是通过Java的一个标准API来实现的;这个API即为Pluggable Annotation Processing API
,简称为JSR269。
JSR269
借用JSR269官方原文定义(附带原文地址:https://jcp.org/en/jsr/detail?id=269):
J2SE 1.5 added a new Java language mechanism “annotations” that allows annotation types to be used to annotate classes, fields, and methods. These annotations are typically processed either by build-time tools or by run-time libraries to achieve new semantic effects. In order to support annotation processing at build-time, this JSR will define APIs to allow annotation processors to be created using a standard pluggable API. This will simplify the task of creating annotation processors and will also allow automation of the discovery of appropriate annotation processors for a given source file.
The specification will include at least two sections, a section of API modeling the Java programming language and a distinct section for declaring annotation processors and controlling how they are run. Since annotations are placed on program elements, an annotation processing framework needs to reflect program structure. Annotation processors will be able to specify what annotations they process and multiple processors will be able to run cooperatively.
The processors and program structure api can be accessed at build-time; i.e. this functionality supplements core reflection support for reading annotations.
译文如下:
J2SE 1.5 增加了一种新的Java语言机制”annotations“,它允许注解被用于类、字段以及方法上。这些注解由
build-time
工具以及run-time
库处理,来达到新的语义效果。为了支持在build-time
时处理注解,这个JSR定义了通用的插入式API用于创建标准的注解处理器。这将简化创建注解处理器的任务,并且还能够根据源文件自动匹配响应的注解处理器。
该规范将至少包含两个部分,一部分用于建模Java编程语言的API,另一部分用于声明注解处理器以及他们的运作机制。由于注解被用于程序元素上,一个注解处理框架需要反映程序结构。注解处理器将能指定哪些注解是它们可以处理的,以及多个注解处理器如何协同工作。
注解处理器以及程序框架api可以在build-time
时访问;举个例子,此功能提供了核心反射支持用于读取注解(注:一般指从源码内读取注解或者只读取标注为source-only
的注解)。
即,在J2SE 1.6版本加入的JSR269的主要点如下:
- 专门用于支持 J2SE 1.5 无法处理的
build-time
注解处理场景,并定义通用注解处理API - 引入程序框架api
JSR269运行机制
sequenceDiagram; javac/Main->>javac/Main: new main.Main("javac) javac/Main->>main/Main: 编译参数传递 main/Main->>main/Main: Javac上下文创建 main/Main->>main/Main: JavaFileManager.preRegister预注册 main/Main->>main/Main: 执行compile并传递编译参数以及上下文 main/Main-->>javac/Main: 参数有误显示帮助信息 main/Main->>main/Main: 参数解析(获取class以及files并初始化注解处理器路径) alt 解析的文件为空且含有注解处理参数 main/Main-->>javac/Main: 无source.files.classes else 存在文件或者无注解处理参数 main/Main->>main/Main: JavaCompiler实例创建 main/Main->>main/JavaCompiler: 传递源文件列表以及类列表 alt PROC为none值 main/JavaCompiler->>main/JavaCompiler: 不初始化 else PROC非none值 main/JavaCompiler->>main/JavaCompiler: 初始化注解 main/JavaCompiler->>processing/JavaProcessingEnvironment: 设置Processors processing/JavaProcessingEnvironment->>processing/JavaProcessingEnvironment: 从参数获取processors processing/JavaProcessingEnvironment->>processing/JavaProcessingEnvironment: processors初始化并赋值给discoveredProcs end main/JavaCompiler->>main/JavaCompiler: 处理注解 main/JavaCompiler->>main/JavaCompiler: 将需要的package注解以及class注解加入待处理列表 main/JavaCompiler->>processing/JavaProcessingEnvironment: 传递上下文、待处理的包类注解列表 processing/JavaProcessingEnvironment->>processing/JavaProcessingEnvironment: 构造Round loop 循环直到没有新文件产生(moreToDo方法) processing/JavaProcessingEnvironment->>processing/Round: 构造JavacRoundEnvironment,传递包类 processing/Round->>processing/Round: 通过顶层的包类找到所有支持的注解 loop 循环discoveredProcs processing/Round->>processing/Round: 通过变量annotationsPresent判断这个注解是否支持 alt 注解支持 processing/Round->>processing/Round: 加入待处理列表;matchedNames添加支持的注解名 else 注解不支持 processing/Round->>processing/Round: 不做处理,继续下一步 end alt matchedNames是否不为空且处理器是否已被执行 processing/Round->>processing/Round: 调用注解处理器的process方法 processing/Round->>processing/Round: 标记注解处理器已被执行 alt 执行结果是为true processing/Round->>processing/Round: unmatchedAnnotations移除matchedNames内所有匹配的项 else 执行结果为false processing/Round->>processing/Round: 不做处理,继续下一步 end else processing/Round->>processing/Round: 不做处理,继续下一步 end end processing/Round-->>processing/JavaProcessingEnvironment: 一次round循环完成 processing/JavaProcessingEnvironment->>processing/JavaProcessingEnvironment: 构建下一个round end processing/JavaProcessingEnvironment->>processing/Round: 执行最后一次run方法 processing/Round->>processing/DiscoveredProcessors: 所有执行过的processor会带上空注解集合参数再次执行一遍 processing/DiscoveredProcessors-->>processing/Round: 完成执行 processing/Round-->>processing/JavaProcessingEnvironment: 完成最后一次round processing/JavaProcessingEnvironment->>processing/JavaProcessingEnvironment: 判断本次是否存在错误,存在则标记errorStatus processing/JavaProcessingEnvironment->>processing/JavaProcessingEnvironment: 清理所有包类 processing/JavaProcessingEnvironment->>processing/JavaProcessingEnvironment: 构建为最终编译使用的JavaCompiler alt errorStatus状态为true processing/JavaProcessingEnvironment->>processing/JavaProcessingEnvironment: 错误数不为0返回编译器 processing/JavaProcessingEnvironment->>processing/JavaProcessingEnvironment: 错误数为0日志记录无错误+1 else erroStatus状态为false processing/JavaProcessingEnvironment->>processing/JavaProcessingEnvironment: enterTrees end processing/JavaProcessingEnvironment-->>main/JavaCompiler: 返回编译器 main/JavaCompiler-->>main/Main: 执行编译完成 alt 错误数大于0 main/Main-->>javac/Main: 执行错误 else 为空 main/Main-->>javac/Main: 执行正确 end end
样例
构建三个模块:
- 注解处理类以及目标注解类
- 注解注册为服务
- 调用方
注解类以及处理类
注解类:
1 | ({ElementType.METHOD, ElementType.TYPE}) |
注解处理类:
1 | "com.whatakitty.learn.jsr269.Test"}) (value = { |
注册服务
在文件夹resources/META-INF/services
下创建文件javax.annotation.processing.Processor
,内容如下:
1 | com.whatakitty.learn.jsr269.AnnotationProcessor |
调用方
1 | public class Test1 { |
运行结果
可以看到,上图中已经输出两次Hello World!。至于为什么会输出两次,是由于第一次是本身注解的处理调用;最后一次是,jdk会在所有注解处理完成后,将所有处理过的注解全部传入空注解再次执行一遍,代码如下:
1 | /** |
Lombok原理
Lombok基于JSR269 API实现了通过特定注解生成对应代码的功能。
Lombok主要在类LombokProcessor
处理了自己的注解通过AST生成代码。如下,主要看两个重写方法:
1 | // 初始化本次处理的一些变量等 |
init
方法主要是做一些初始化;
process
方法内主要是将注解以及visitor处理器的按照优先级划分,然后每次执行完成后,排除最开始的一个优先级后,重新开始下一轮编译。知道所有优先级排除完毕。这么做的原因,应该是为了在高优先级处理器处理完成生成文件后,能够让低优先级处理器根据高优先级处理器生成的文件重新执行一遍防止遗漏生成的新的代码
总结
- 详细了解了JSR269内部的执行逻辑
- 了解了JAVAC的编译过程
- 了解了Lombok内部的执行原理,可以依托现有Lombok处理器,自定义注解
总之,收获满满。