JSR269插件化注解API

背景

一直在使用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运行机制

javac/Mainmain/Mainmain/JavaCompilerprocessing/JavaProcessingEnvironmentprocessing/Roundprocessing/DiscoveredProcessorsnew main.Main("javac)编译参数传递Javac上下文创建JavaFileManager.preRegister预注册执行compile,并传递编译参数以及上下文参数有误显示帮助信息参数解析(获取class以及files并初始化注解处理器路径)无source.files.classesJavaCompiler实例创建传递源文件列表以及类列表不初始化初始化注解设置Processors从参数获取processorsprocessors初始化并赋值给discoveredProcsalt[ PROC为none值 ][ PROC非none值 ]处理注解将需要的package注解以及class注解加入待处理列表传递上下文、待处理的包类注解列表构造Round构造JavacRoundEnvironment,传递包类通过顶层的包类找到所有支持的注解通过变量annotationsPresent判断这个注解是否支持加入待处理列表;

matchedNames添加支持的注解名不做处理,继续下一步alt[ 注解支持 ][ 注解不支持 ]调用注解处理器的process方法标记注解处理器已被执行unmatchedAnnotations移除matchedNames内所有匹配的项不做处理,继续下一步alt[ 执行结果是为true ][ 执行结果为false ]不做处理,继续下一步

  • alt[ matchedNames是否不为空且处理器是否已被执行 ]
  • loop[ 循环discoveredProcs ]

一次round循环完成构建下一个roundloop[ 循环直到没有新文件产生(moreToDo方法) ]执行最后一次run方法所有执行过的processor会带上空注解集合参数再次执行一遍完成执行完成最后一次round判断本次是否存在错误,存在则标记errorStatus清理所有包类构建为最终编译使用的JavaCompiler错误数不为0返回编译器错误数为0日志记录无错误+1enterTreesalt[ errorStatus状态为true ][ erroStatus状态为false ]返回编译器执行编译完成执行错误执行正确alt[ 错误数大于0 ][ 为空 ]alt[ 解析的文件为空且含有注解处理参数 ][ 存在文件或者无注解处理参数 ]javac/Mainmain/Mainmain/JavaCompilerprocessing/JavaProcessingEnvironmentprocessing/Roundprocessing/DiscoveredProcessors

样例

构建三个模块:

  1. 注解处理类以及目标注解类
  2. 注解注册为服务
  3. 调用方
注解类以及处理类

注解类:

1
2
3
4
5
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Test {
String value();
}

注解处理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SupportedAnnotationTypes(value = {"com.whatakitty.learn.jsr269.Test"})
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
public class AnnotationProcessor extends AbstractProcessor {

private Messager messager;
private AtomicInteger atomicInteger;

@Override
public void init(ProcessingEnvironment env) {
messager = env.getMessager();
atomicInteger = new AtomicInteger(0);
super.init(env);
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
messager.printMessage(Diagnostic.Kind.NOTE, "Hello World!" + atomicInteger.incrementAndGet());
return true;
}
}
注册服务

在文件夹resources/META-INF/services下创建文件javax.annotation.processing.Processor,内容如下:

1
com.whatakitty.learn.jsr269.AnnotationProcessor
调用方
1
2
3
4
5
6
7
8
9
10
11
12
public class Test1 {

public static void main(String[] args) throws Exception {
System.out.println("success");
test();
}

@Test("method is test")
public static void test() throws Exception {
}

}
运行结果

http://static.cyblogs.com/191455.jpg

可以看到,上图中已经输出两次Hello World!。至于为什么会输出两次,是由于第一次是本身注解的处理调用;最后一次是,jdk会在所有注解处理完成后,将所有处理过的注解全部传入空注解再次执行一遍,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Run all remaining processors on the procStateList that
* have not already run this round with an empty set of
* annotations.
*/
public void runContributingProcs(RoundEnvironment re) {
if (!onProcInterator) {
// 构造空的注解元素集合
Set<TypeElement> emptyTypeElements = Collections.emptySet();
// 遍历所有注册的注解处理器
while(innerIter.hasNext()) {
ProcessorState ps = innerIter.next();
// 判断该注解处理器是否在之前处理过注解,未参与过的不会调用
if (ps.contributed)
// 传入空的注解元素集合,重新调用一次注解处理器
callProcessor(ps.processor, emptyTypeElements, re);
}
}
}

Lombok原理

Lombok基于JSR269 API实现了通过特定注解生成对应代码的功能。

Lombok主要在类LombokProcessor处理了自己的注解通过AST生成代码。如下,主要看两个重写方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// 初始化本次处理的一些变量等
@Override public void init(ProcessingEnvironment procEnv) {
super.init(procEnv);
// 判断lombok是否被禁用
if (System.getProperty("lombok.disable") != null) {
lombokDisabled = true;
return;
}

this.processingEnv = procEnv;
this.javacProcessingEnv = getJavacProcessingEnvironment(procEnv);
this.javacFiler = getJavacFiler(procEnv.getFiler());

// 替换类加载器、对netbeans IDE相关hook处理、替换JavaFileManager
placePostCompileAndDontMakeForceRoundDummiesHook();
trees = Trees.instance(javacProcessingEnv);
transformer = new JavacTransformer(procEnv.getMessager(), trees);
// 获取标记HandlerPriority注解的所有优先级
SortedSet<Long> p = transformer.getPriorities();
if (p.isEmpty()) {
this.priorityLevels = new long[] {0L};
this.priorityLevelsRequiringResolutionReset = new HashSet<Long>();
} else {
this.priorityLevels = new long[p.size()];
int i = 0;
// 循环所有获取到的注释或者visit处理优先级
for (Long prio : p) this.priorityLevels[i++] = prio;
this.priorityLevelsRequiringResolutionReset = transformer.getPrioritiesRequiringResolutionReset();
}
}

/** {@inheritDoc} */
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 如果禁用,则不处理lombok注解
if (lombokDisabled) return false;
// 是否已经处理结束
if (roundEnv.processingOver()) {
cleanup.run();
return false;
}

// We have: A sorted set of all priority levels: 'priorityLevels'

// Step 1: Take all CUs which aren't already in the map. Give them the first priority level.

String randomModuleName = null;

// 标记所有编译单元的优先级
// 如果是第二次循环,因为roots已经包含了这个编译单元,所以会忽略
for (Element element : roundEnv.getRootElements()) {
if (randomModuleName == null) randomModuleName = getModuleNameFor(element);
JCCompilationUnit unit = toUnit(element);
if (unit == null) continue;
if (roots.containsKey(unit)) continue;
roots.put(unit, priorityLevels[0]);
}

while (true) {
// Step 2: For all CUs (in the map, not the roundEnv!), run them across all handlers at their current prio level.

// 循环优先级列表
for (long prio : priorityLevels) {
List<JCCompilationUnit> cusForThisRound = new ArrayList<JCCompilationUnit>();
// 获取在该优先级下的所有编译单元并加入该优先级下需要处理的编译单元列表内
for (Map.Entry<JCCompilationUnit, Long> entry : roots.entrySet()) {
Long prioOfCu = entry.getValue();
if (prioOfCu == null || prioOfCu != prio) continue;
cusForThisRound.add(entry.getKey());
}
// 按照优先级顺序执行编译单元
// 访问AST树并编译目标注解
transformer.transform(prio, javacProcessingEnv.getContext(), cusForThisRound, cleanup);
}

// Step 3: Push up all CUs to the next level. Set level to null if there is no next level.

// 排除掉列表第一个优先级准备执行下一次循环
Set<Long> newLevels = new HashSet<Long>();
for (int i = priorityLevels.length - 1; i >= 0; i--) {
Long curLevel = priorityLevels[i];
Long nextLevel = (i == priorityLevels.length - 1) ? null : priorityLevels[i + 1];
List<JCCompilationUnit> cusToAdvance = new ArrayList<JCCompilationUnit>();
for (Map.Entry<JCCompilationUnit, Long> entry : roots.entrySet()) {
if (curLevel.equals(entry.getValue())) {
cusToAdvance.add(entry.getKey());
newLevels.add(nextLevel);
}
}
for (JCCompilationUnit unit : cusToAdvance) {
roots.put(unit, nextLevel);
}
}
newLevels.remove(null);

// Step 4: If ALL values are null, quit. Else, either do another loop right now or force a resolution reset by forcing a new round in the annotation processor.

// 判断是否将所有优先级排除,优先级列表为空,则结束
if (newLevels.isEmpty()) return false;
newLevels.retainAll(priorityLevelsRequiringResolutionReset);
if (!newLevels.isEmpty()) {
// Force a new round to reset resolution. The next round will cause this method (process) to be called again.
forceNewRound(randomModuleName, javacFiler);
return false;
}
// None of the new levels need resolution, so just keep going.
}
}

init方法主要是做一些初始化;

process方法内主要是将注解以及visitor处理器的按照优先级划分,然后每次执行完成后,排除最开始的一个优先级后,重新开始下一轮编译。知道所有优先级排除完毕。这么做的原因,应该是为了在高优先级处理器处理完成生成文件后,能够让低优先级处理器根据高优先级处理器生成的文件重新执行一遍防止遗漏生成的新的代码

总结

  • 详细了解了JSR269内部的执行逻辑
  • 了解了JAVAC的编译过程
  • 了解了Lombok内部的执行原理,可以依托现有Lombok处理器,自定义注解

参考地址

如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员简栈文化-小助手(lastpass4u),他会拉你们进群。

简栈文化服务订阅号