其前身是来自 java 6 的 APT (abstract processor tool),自 java 8 之后被插件式注解 API (pluggable annotation processing api) 取代。其本质上是提供用户在编译器访问注解元数据,处理和自定义编译输出,并能够创建新的源文件等等。

目的

  • 用于生成模板代码,减少工作量,并保证了实际源码的简洁(如:lombok);
  • 取代大量通过反射处理注解的方式,提高代码的运行性能(如:对象的序列化);

应用

  • MapStruct:java bean 映射注解处理器;

代码实现

插件式注解是通过继承虚注解处理器(AbstractProcessor)来实现的,通过在其核心方法  (process)中匹配相应注解,可以实现在编译时向项目中输出 java 源文件。

01.注解

创建注解 “@Hello”。

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

02.处理器

创建名为 “HelloProcessor”的处理器,并指定其支持的源码为 java 8,处理上述注解( “@Hello”)。

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
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.annotation.Hello")
public class HelloProcessor extends AbstractProcessor {

private static final String HELLO_TEMPLATE =
"package %1$s;\n\npublic class %2$sHello {\n public static void sayHello() {\n System.out.println(\"Hello %3$s\");\n }\n}\n";

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
roundEnvironment.getElementsAnnotatedWith(Hello.class).forEach(element -> {
TypeElement typeElem = (TypeElement) element;
String typeName = typeElem.getQualifiedName().toString();
Filer filer = processingEnv.getFiler();
try (Writer sw = filer.createSourceFile(typeName + "Hello").openWriter()) {
log("生成 " + typeName + "Hello 源码");
int lastIndex = typeName.lastIndexOf('.');
sw.write(String.format(HELLO_TEMPLATE, typeName.substring(0, lastIndex), typeName.substring(lastIndex + 1), typeName));
} catch (IOException e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage());
}
});
return true;
}

private void log(String msg) {
if (processingEnv.getOptions().containsKey("debug")) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg);
}
}
}

03.作用

凡是被 “@Hello” 标记的类,都会生成以其类名 + Hello 形式的类,该类仅有一个类方法(sayHello)。在 Person 类上标记注解,如下:

1
2
@Hello
public class Person

04.效果

最终在 【模块名】\target\generated-sources\annotations 下生成如下类:

1
2
3
4
5
public class PersonHello {
public static void sayHello() {
System.out.println("Hello com.example.bean.Person");
}
}

配置

01.Maven 编译插件

通过在  maven-compiler-plugin 插件中指定注解处理器。在多模块中,可以仅仅在需要提供注解处理器支持的模块中添加配置,亦可在父模块中添加(为所有子模块支持)。但实际上,注解处理器的代码在编译完成(生成源文件)之后,就已经完成其使命,因此在其他模块中,使用 provided 引用,在编译时会排除该依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependency>
<groupId>com.example</groupId>
<artifactId>processor</artifactId>
<scope>provided</scope>
</dependency>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<!--指定源文件生成路径,默认为当前模块下 -->
<!-- <generatedSourcesDirectory>${project.build.directory}/generated-sources/</generatedSourcesDirectory>-->
<annotationProcessors>
<annotationProcessor>
com.example.processor.HelloProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>

问题

无法支持 lombok 插件。与其一起使用,会出现如下报错:

1
java: 找不到符号

02.Java 服务注册

通过 spi 服务注册的方式。
当然需要添加(注解处理器的模块)依赖。
在注解处理器所在模块(或项目中)的 resources,创建 javax.annotation.processing.Processor 文件,目录结构如下:

1
2
3
4
5
resources
└── META-INF
└── services
├── java.lang.Process
└── javax.annotation.processing.Processor

在此文件中,填写自定义的注解处理器(如上面的):com.example.processor.HelloProcessor。
(实测)该方法兼容 lombok 插件。

额外的配置

直接注册会产生注解处理器无法找到的错误:

1
2
Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project processor: Compilation failure
服务配置文件不正确, 或构造处理程序对象javax.annotation.processing.Processor: Provider com.example.processor.HelloProcessor not found时抛出异常错误

此时需要在该模块下配置  maven-compiler-plugin 插件:

1
2
3
4
5
6
7
8
9
10
11
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<!-- 不加这一句编译会报找不到processor的异常-->
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</plugin>

问题

在其他博客中提到,可以使用 google 的 AutoService 注解,为注解处理器自动生成服务注册文件。但是在实际测试时,发现其所生成的文件名为:java.lang.process,与上述不符,且并不能为注解提供注解处理器的支持。

注意

注解处理器在编译器生成代码,因此在使用(生成的代码)时需要先编译,使用 maven 插件 compiler(或 javac 命令)进行编译,最终可以得到此效果

其他

下面是用于生成 builder 类的注解及处理器:

1
2
3
4
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface 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
@SupportedAnnotationTypes("com.example.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.example", 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.example", typeSpec).build().writeTo(System.out);
JavaFile.builder("com.example", 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;
}
}

问题

一些在测试中遇到但还没有解决的问题:

  • 测试过程中发现在 idea 的配置中 lombok 的注解处理器被自定义的替换掉了,导致无法使用(见 Java 服务注册);

apt  jdk 8   注解   代码生成

评论