java annotation processor

注解(annotation)是Java1.5引入的新功能,可以用来进行代码检查、代码生成等各种实用的功能。例如@Override、@Nonnull等注解可以给编译器、代码风格检查器提供代码检查,Spring框架中的@Autowire、@Component等可以进行Spring中Bean的创建、注入等功能,开发人员从繁琐的配置文件中解脱出来,注解已经成为当前Java各种类库中非常常见的功能了。
但是注解的这些功能并不是单单提供一个注解就能实现的了,注解只是一个标记、一个元数据,实现还是依靠内部的注解处理器来完成,处理的时机又可以分为编译前、编译时、虚拟机启动时、虚拟机启动后运行时等。以Java程序员非常熟悉的SpringAop注解为例,Spring在虚拟机启动后Spring初始化时扫描到某个Bean的类上声明了@Aspect注解后,就会从该类中获取AOP切面等信息,然后通过字节码技术(JDKProxyGenerator或cglib)创建符合Aop拦截条件的bean的代理的类的bean,这样就实现了非常方便的AOP功能。通过SpringAOP创建的代理也有一些小问题,例如prinvate方法不能被代理、只能增强方法等,这些字节码层面的问题之后会讨论,今天要讨论的是Java源代码层的处理。
Java程序给人的第一印象往往是类很多、类很长,例如实现同一个功能的不同的project, go语言可能两三个文件、几百行代码就完成了,但是Java的话可能会搞出几十个文件、每个里面大量的gettersetter等,当然这也和个人的编程风格有关,这里要表达的意思是Java严谨工程规范降低一定错误的同时也会造成开发效率的降低。同样以GetterSetter为例,虽然现在IDE基本都提供了生成代码的快捷键,但是每次修改字段后都要删除旧代码生成新代码也是一种开销。同样情况也发生在Java的类和数据库表定义之间的同步问题,那边大家有没有相关自动生成代码的方式呢,没错,业界已经有lombokmybatis-generator等解决方案了。lombok中定义@Data、@Getter、@ToString等方法就能在原有类上加入GetterSetter方法、toString方法等,很是方便。今天,我们要就是利用JSR269(Pluggable Annotation Processing API)实现类似lombok的功能。
JSR269的结果是JDK加入了javax.annotation.processing包,其中核心的类是AbstractProcessor类,我们的注解处理就是声明在其中实现的,其中的核心方法是process方法。JSR269的本意是通过注解实现代码检查、代码生成等功能(不是替换原有代码,lombok使用了一些黑科技来实现)。
compile-process

1
2
3
public abstract boolean process​(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv)
Processes a set of annotation types on type elements originating from the prior round and returns whether or not these annotation types are claimed by this processor. If true is returned, the annotation types are claimed and subsequent processors will not be asked to process them; if false is returned, the annotation types are unclaimed and subsequent processors may be asked to process them.

roundEnv可以理解为当前上下文的各种信息,process返回true表示这个注解被我处理之后其他处理器就不用处理了。
另外要注意的是AbstractProcessor的实现需要在编译目标代码前已经编译好,并且通过ServiceProvider的方式注册,通常是通过jar包的方式引入。
下面我们就开始实践起来。
我们的目标是,给一个POJO类创建一个Builder类, 例如一个Order类,会在编译后自动创建创建OrderBuilder类,来提供fluent的创建Order对象的方法。其中会使用[javapoet]来创建Java文件。
首先创建一个maven项目,创建两个子module,一个是processor模块包含注解、注解处理器,一个worker模块来使用processor中的功能。
首先我们编写processor模块。
创建一个名为Builder的注解类。

1
2
3
4
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Builder {
}

然后创建它的处理器BuilderProcessor
我们的思路是获取所有标注Builder的类,然后创建它的Builder类,Builder类提供目标类的所有字段的builder方法以及最终一个build方法来生成目标类对象。
SupportedAnnotationTypes表示当前处理器支持的注解,SupportedSourceVersion表示支持的Java语言的版本。
init方法可以进行一些初始化,例如保存Filer对象。
在process中,我们先找出所有标注的类,过滤出只有标注在类上的注解,然后中间部分就是使用javapoet拼出一个对应的Builder出来。

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
@SupportedAnnotationTypes({
"com.lzy.blog.Builder"
})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BuilderProcessor extends AbstractProcessor {
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("Processing " + annotations + roundEnv);
Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Builder.class);
elementsAnnotatedWith.forEach(element -> {
if (element.getKind() != ElementKind.CLASS) {
System.out.println("not class");
return;
}
TypeMirror elementTypeMirror = element.asType();
String builderClassName = element.getSimpleName() + "Builder";
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(builderClassName)
.addModifiers(Modifier.FINAL, Modifier.PUBLIC);
MethodSpec.Builder builderMethod = MethodSpec.methodBuilder("build")
.returns(TypeName.get(element.asType()))
.addModifiers(Modifier.PUBLIC)
.addStatement("$T instance = new $T()", TypeName.get(elementTypeMirror), TypeName.get(elementTypeMirror));
element.getEnclosedElements().forEach(field -> {
if (field.getKind() == ElementKind.FIELD) {
boolean isStatic = field.getModifiers().contains(STATIC);
if (isStatic) {
System.out.println(field.getSimpleName() + " is static");
return;
}
String fieldName = field.getSimpleName().toString();
System.out.println(fieldName);
String transformedName = upperFirstChar(fieldName);
MethodSpec methodSpec = MethodSpec.methodBuilder("build" + transformedName)
.returns(TypeName.get(element.asType()))
.returns(ClassName.get("com.lzy.gen", builderClassName))
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeName.get(field.asType()), field.getSimpleName().toString())
.addStatement("this.$L = $L;return this", field.getSimpleName().toString()
, field.getSimpleName().toString())
.build();
FieldSpec fieldSpec = FieldSpec.builder(TypeName.get(field.asType()),
fieldName, Modifier.PRIVATE).build();
String fieldSetName = upperFirstChar(fieldName);
builderMethod.addStatement("instance.set$L(this.$L)", fieldSetName, fieldName);
typeBuilder.addField(fieldSpec);
typeBuilder.addMethod(methodSpec);
}
});
builderMethod.addStatement("return instance");
typeBuilder.addMethod(builderMethod.build());
TypeSpec typeSpec = typeBuilder.build();
try {
JavaFile.builder("com.lzy.gen", typeSpec).build().writeTo(System.out);
JavaFile.builder("com.lzy.gen", typeSpec).build().writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
});
return true;
}
private static String upperFirstChar(String name) {
if (name.length() < 1) {
return name;
}
String firstChar = name.substring(0, 1).toUpperCase();
if (name.length() > 1) {
return firstChar + name.substring(1);
}
return firstChar;
}
}

创建好注解和注解处理器后,另外要注意的就是在resources文件夹下创建一个META-INF/services/javax.annotation.processing.Processor文件,其中以行为单位填写要注册的annotationProcessor,当前处理器就填写
com.lzy.blog.processor.BuilderProcessor即可。
修改pom中的compiler配置,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</plugin>
</plugins>
</build>

然后在processor模块下执行 mvn clean install打包。

然后我们在worker工程中使用注解,首先引入processor的依赖,这样我们就能在编译时进行代码处理。
创建一个平凡的Order类, 类上标注@Builder

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
@Builder
public class Order {
private long id;
private long addTime;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public long getAddTime() {
return addTime;
}
public void setAddTime(long addTime) {
this.addTime = addTime;
}
@Override
public String toString() {
return "Order{" +
"id=" + id +
", addTime=" + addTime +
'}';
}
}

然后我们在另一个类中就可以使用OrderBuilder来创建Order对象了。

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
Order build = new OrderBuilder().buildId(2).buildAddTime(System.currentTimeMillis()).build();
System.out.println(build);
}
}

在worker目录下执行mvn clean compile
然后再执行main方法,就会发现我们成功动态创建了Builder类。并且在target目录中也能找到对应的Builder的class文件。

整个项目的代码放在了github

更多参考

感觉有收获的话,请我吃个早饭吧!