Pluggable Annotation Processing的讲解与实践

Pluggable Annotation Processing的讲解与实践

如何自己实现一个lombok。

项目结构

先来个简单的测试:

1
2
3
4
5
6
7
8
9
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── jimo
│ │ │ ├── Data.java
│ │ │ ├── Main.java
│ │ │ └── MyProcessor.java

代码

Data.java

1
2
3
4
5
6
7
8
9
10
11
package com.jimo;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
}

Main.java

1
2
3
4
5
6
7
8
9
10
11
@Data
public class Main {

private int age;
private String name;

public static void main(String[] args) {
new Main();
System.out.println("yes");
}
}

MyProcessor.java

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
package com.jimo;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;

@SupportedAnnotationTypes(value = {"com.jimo.Data"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyProcessor extends AbstractProcessor {

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("begin");
for (TypeElement annotation : annotations) {
if (annotation.getSimpleName().contentEquals("Data")) {
Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(annotation);
for (Element element : elementsAnnotatedWith) {
String pkg = element.getEnclosingElement().toString();
String className = element.getSimpleName().toString();

try {
rewriteClass(pkg, className);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("1:" + pkg);
System.out.println("2:" + className);
}
}
System.out.println("anno:" + annotation);
}
System.out.println(roundEnv);
return true;
}

private void rewriteClass(String pkg, String className) throws IOException {
final JavaFileObject sourceFile = processingEnv.getFiler().createClassFile(pkg + ".NewClass");
// JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(pkg + ".Test");
// JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(pkg + "." + className);
try (Writer writer = sourceFile.openWriter()) {
writer.write("package " + pkg + ";");
writer.write("class Main{");
writer.write("private int age;");
writer.write("}");
}
}
}

如何使用

javac编译法

需要先编译处理器,再编译其他代码

1
2
3
4
// 编译处理器
src\main\java>javac com\jimo\MyProcessor.java
// 带上处理器
javac -processor com.jimo.MyProcessor com\jimo\Main.java

然后会看到编译时输出:

1
2
3
4
5
6
com.jimo.Data
[errorRaised=false, rootElements=[com.jimo.Main], processingOver=false]
[errorRaised=false, rootElements=[], processingOver=true]
警告: 注释处理不适用于隐式编译的文件。
使用 -implicit 指定用于隐式编译的策略。
1 个警告

maven插件编译法

在插件中声明处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<annotationProcessors>
<annotationProcessor>
com.jimo.MyProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build>

同样,我们也需要先编译处理器:因为默认,maven编译是放在target/classes/com/jimo/xxx.class,所以如下:
(编译命令参考:javac -d ..\..\..\target\classes\ com\jimo\MyProcessor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── jimo
│ │ │ ├── Data.java
│ │ │ ├── Main.java
│ │ │ └── MyProcessor.java
│ │ └── resources
│ └── test
│ └── java
└── target
├── classes
│ └── com
│ └── jimo
│ └── MyProcessor.class

然后使用maven compile命令,或者在IDEA里调用Lifecycle-->compile按钮:

结果:

1
2
3
4
5
6
7
[INFO] Compiling 3 source files to D:\workspace\test-demos\j-lombok-demo\target\classes
com.jimo.Data
[errorRaised=false, rootElements=[com.jimo.MyProcessor, com.jimo.Data, com.jimo.Main], processingOver=false]
[errorRaised=false, rootElements=[], processingOver=true]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

jar包引入

以上2种方式都是在一个仓库里使用,仅供开发测试,真正投入使用时,还是用jar包的方式好。

实践lombok

上面的方法只能生成新的类和class文件,而有时需要修改源代码和class,这就更复杂一些,需要懂得AST(抽象语法树)和编译原理。

安装tools.jar 到本地仓库:因为需要用到里面的AST代码

1
mvn install:install-file -Dfile=JDK目录\lib\tools.jar -DgroupId=com.sun -DartifactId=tools -Dversion=1.8 -Dpackaging=jar

写处理器代码:

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package com.jimo;

import com.google.auto.service.AutoService;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.*;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

@SupportedAnnotationTypes(value = {"com.jimo.Data"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class CustomProcessor extends AbstractProcessor {
/**
* AST
*/
private JavacTrees trees;
/**
* 操作修改AST
*/
private TreeMaker treeMaker;
/**
* 符号封装类,处理名称
*/
private Names names;
/**
* 打印信息
*/
private Messager messager;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
trees = JavacTrees.instance(processingEnv);
final Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
treeMaker = TreeMaker.instance(context);
names = Names.instance(context);
messager = processingEnv.getMessager();
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("begin");

final Set<? extends Element> dataAnnotations = roundEnv.getElementsAnnotatedWith(Data.class);

dataAnnotations.stream().map(element -> trees.getTree(element)).forEach(
tree -> tree.accept(new TreeTranslator() {
@Override
public void visitMethodDef(JCTree.JCMethodDecl jcMethodDecl) {
// print method name
System.out.println("-------------2");
messager.printMessage(Diagnostic.Kind.NOTE, jcMethodDecl.toString());
super.visitMethodDef(jcMethodDecl);
}

@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {

System.out.println("-------------1");
final Map<Name, JCTree.JCVariableDecl> treeMap =
jcClassDecl.defs.stream().filter(k -> k.getKind().equals(Tree.Kind.VARIABLE))
.map(tree -> (JCTree.JCVariableDecl) tree)
.collect(Collectors.toMap(JCTree.JCVariableDecl::getName, Function.identity()));

treeMap.forEach((k, var) -> {
messager.printMessage(Diagnostic.Kind.NOTE, "var:" + k);
System.out.println("-------------3");
try {
// add getter
jcClassDecl.defs = jcClassDecl.defs.prepend(getter(var));
// add setter
jcClassDecl.defs = jcClassDecl.defs.prepend(setter(var));
// jcClassDecl.defs.prepend(setter(var));
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
}
});

super.visitClassDef(jcClassDecl);
}
})
);
return true;
}

/**
* 自定义setter
*/
private JCTree setter(JCTree.JCVariableDecl var) throws ClassNotFoundException, IllegalAccessException,
InstantiationException {
// 方法级别public
final JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);

final Name varName = var.getName();
Name methodName = methodName(varName, "set");

// 方法体
ListBuffer<JCTree.JCStatement> jcStatements = new ListBuffer<>();
jcStatements.append(treeMaker.Exec(treeMaker.Assign(
treeMaker.Select(treeMaker.Ident(names.fromString("this")), varName),
treeMaker.Ident(varName)
)));
final JCTree.JCBlock block = treeMaker.Block(0, jcStatements.toList());

// 返回值类型void
JCTree.JCExpression returnType =
treeMaker.Type((Type) (Class.forName("com.sun.tools.javac.code.Type$JCVoidType").newInstance()));

List<JCTree.JCTypeParameter> typeParameters = List.nil();

// 参数
final JCTree.JCVariableDecl paramVars = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER,
List.nil()), var.name, var.vartype, null);
final List<JCTree.JCVariableDecl> params = List.of(paramVars);

List<JCTree.JCExpression> throwClauses = List.nil();
// 重新构造一个方法, 最后一个参数是方法注解的默认值,这里没有
return treeMaker.MethodDef(modifiers, methodName, returnType, typeParameters, params, throwClauses, block,
null);
}

/**
* 构造驼峰命名
*/
private Name methodName(Name varName, String prefix) {
return names.fromString(prefix + varName.toString().substring(0, 1).toUpperCase()
+ varName.toString().substring(1));
}

/**
* 构造getter
*/
private JCTree getter(JCTree.JCVariableDecl var) {
// 方法级别
final JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);

// 方法名称
final Name methodName = methodName(var.getName(), "get");

// 方法内容
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), var.getName())));
final JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

// 返回值类型
final JCTree.JCExpression returnType = var.vartype;

// 没有参数类型
List<JCTree.JCTypeParameter> typeParameters = List.nil();

// 没有参数变量
List<JCTree.JCVariableDecl> params = List.nil();

// 没有异常
List<JCTree.JCExpression> throwClauses = List.nil();

// 构造getter
return treeMaker.MethodDef(modifiers, methodName, returnType, typeParameters, params, throwClauses, block,
null);
}
}

使用google的auto service进行配置处理器:

引入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0-rc4</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.auto</groupId>
<artifactId>auto-common</artifactId>
<version>0.10</version>
<optional>true</optional>
</dependency>

注意到上面类中的@AutoService注解就是来自这。

这样编译完后,会生成META-INF信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── jimo
│ │ │ ├── CustomProcessor.java
│ │ │ ├── Data.java
│ │ └── resources
│ └── test
│ └── java
└── target
├── classes
│ ├── META-INF
│ │ └── services
│ │ └── javax.annotation.processing.Processor
│ └── com
│ └── jimo
│ ├── CustomProcessor$1.class
│ ├── CustomProcessor.class
│ ├── Data.class

javax.annotation.processing.Processor这个文本文件里就一句话:

1
com.jimo.CustomProcessor

接着可以使用mvn install安装到本地,让其他项目引入试试。

目标类:

1
2
3
4
5
6
@Data
public class User {
private int age;
private String name;
public int height;
}

编译完后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class User {
private int age;
private String name;
public int height;

public void setAge(int age) {
this.age = age;
}

public void setName(String name) {
this.name = name;
}

public void setHeight(int height) {
this.height = height;
}

public User() {
}
}