Java中的注解处理器

1. 简介

本文是Java源代码级别注解处理的简介,并提供了使用此技术在编译期间生成其他源文件的示例。

2. 注解处理的应用

源级注解处理首先出现在Java 5中。它是一种在编译阶段生成其他源文件的便捷技术。

源文件不必是Java文件 - 您可以根据源代码中的注解生成任何类型的描述,元数据,文档,资源或任何其他类型的文件。

注解处理在许多无处不在的Java库中被广泛使用,例如,在QueryDSL和JPA中生成元类,以使用Lombok库中的样板代码来扩充类。

需要注意的一件重要事情是注解处理API的局限性 - 它只能用于生成新文件,而不能用于更改现有文件

值得注意的例外是Lombok库,它使用注解处理作为引导机制,将自身包含在编译过程中,并通过一些内部编译器API修改AST。这种hacky技术与注解处理的预期目的无关,因此本文不讨论。

3. 注解处理API

注解处理在多轮中完成。每一轮都从编译器搜索源文件中的注解并选择适合这些注解的注解处理器开始。反过来,每个注解处理器在相应的源上被调用。

如果在此过程中生成了任何文件,则会以生成的文件作为输入启动另一轮。此过程将继续,直到在处理阶段没有生成新文件。

反过来,每个注解处理器在相应的源上被调用。如果在此过程中生成了任何文件,则会以生成的文件作为输入启动另一轮。此过程将继续,直到在处理阶段没有生成新文件。

注解处理API位于javax.annotation.processing包中。您必须实现的主要接口是Processor接口,它具有AbstractProcessor类形式的部分实现。这个类是我们要扩展的类,以创建我们自己的注解处理器。

4. 设置项目

为了演示注解处理的可能性,我们将开发一个简单的处理器,用于为带注解的类生成流畅的对象构建器。

我们将把项目分成两个Maven模块。其中一个注解处理器模块将包含处理器本身和注解,另一个注解用户模块将包含注解类。这是注解处理的典型用例。

annotation-processor模块的设置如下。我们将使用Google的auto-service库来生成稍后将讨论的处理器元数据文件,以及针对Java 8源代码调整的maven-compiler-plugin。这些依赖项的版本将提取到属性部分。

可以在Maven Central存储库中找到最新版本的auto-service库和maven-compiler-plugin

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
<properties>
<auto-service.version>1.0-rc2</auto-service.version>
<maven-compiler-plugin.version>
3.5.1
</maven-compiler-plugin.version>
</properties>

<dependencies>

<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>${auto-service.version}</version>
<scope>provided</scope>
</dependency>

</dependencies>

<build>
<plugins>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

</plugins>
</build>

带有注解源的注解用户 模型不需要任何特殊调整,除了在依赖项部分中添加对注解处理器模块的依赖:

1
2
3
4
5
<dependency>
<groupId>com.doleje</groupId>
<artifactId>annotation-processing</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>

5. 定义注解

假设我们的注解用户模块中有一个简单的POJO类,它包含几个字段:

1
2
3
4
5
6
7
8
9
public class Person {

private int age;

private String name;

// getters and setters …

}

我们想要创建一个构建器帮助程序类,以更流畅地实例化Person类:

1
2
3
4
Person person = new PersonBuilder()
.setAge(25)
.setName("John")
.build();

这个PersonBuilder类 Person对象的构建器,因为它的结构完全由Person setter方法定义。

让我们在注解处理器模块中为setter方法创建一个@BuilderProperty注解。它将允许我们为每个具有其setter方法注解的类生成Builder类:

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface BuilderProperty {
}

带有ElementType.METHOD参数的@Target注解确保此注解只能放在方法上。

SOURCE保留策略意味着该注解只能用于源文件处理期间,而不是在运行时可用。

具有使用@BuilderProperty注解注解的属性的Person类将如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Person {

private int age;

private String name;

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

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

// getters …

}

6. 实现 Processor

6.1 创建AbstractProcessor子类

我们将首先在注解处理器 Maven模块中扩展AbstractProcessor类。

首先,我们应该指定该处理器能够处理的注解,以及支持的源代码版本。这可以通过实施方法进行getSupportedAnnotationTypesgetSupportedSourceVersion的的处理器接口或通过注解你的类@SupportedAnnotationTypes@SupportedSourceVersion注解。

所述@AutoService注解是的一部分auto-service库,并允许生成,这将在下面的章节进行说明处理器的元数据。

1
2
3
4
5
6
7
8
9
10
11
12
@SupportedAnnotationTypes(
"com.doleje.annotation.processor.BuilderProperty")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class BuilderProcessor extends AbstractProcessor {

@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
return false;
}
}

您不仅可以指定具体的注解类名称,还可以指定通配符,例如“com.doleje.annotation。\来处理com.doleje.annotation包及其所有子包内的注解,甚至“*”*来处理所有注解。

我们必须实现的单一方法是进行process方法。编译器会为包含匹配注解的每个源文件调用它。

注解作为第一个Set <? extends TypeElement> annotations参数,并将有关当前处理轮次的信息作为RoundEnviroment roundEnv参数传递。

如果注解处理器已处理了所有传递的注解,并且您不希望它们传递到列表中的其他注解处理器,则返回 true

6.2 收集数据

我们的处理器还没有真正做任何有用的事情,所以让我们用代码完成它。

首先,我们需要遍历在类中找到的所有注解类型。 在我们的示例中,annotation 集将具有与@BuilderProperty注解相对应的单个元素,即使此注解在源文件中多次出现也是如此。

尽管如此,为了完整起见,最好将 process 方法实现为迭代循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {

for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedElements
= roundEnv.getElementsAnnotatedWith(annotation);

// …
}

return true;
}

在此代码中,我们使用RoundEnvironment实例接收使用@BuilderProperty批注注解的所有元素。对于Person类,这些元素对应于setNamesetAge方法。

@BuilderProperty注解的用户可能会错误地注解实际上不是setter的方法。setter方法名称应以set开头,方法应该接收一个参数。

在下面的代码中,我们使用Collectors.partitioningBy()收集器将带注解的方法拆分为两个集合:正确注解的 setter 和其他错误注解的方法:

1
2
3
4
5
6
7
Map<Boolean, List<Element>> annotatedMethods = annotatedElements.stream().collect(
Collectors.partitioningBy(element ->
((ExecutableType) element.asType()).getParameterTypes().size() == 1
&& element.getSimpleName().toString().startsWith("set")));

List<Element> setters = annotatedMethods.get(true);
List<Element> otherMethods = annotatedMethods.get(false);

在这里,我们使用Element.asType()方法接收TypeMirror类的实例,这使我们能够探测类型,即使我们只处于源处理阶段。

另外,我们应该警告用户注解错误的方法,所以让我们使用可从AbstractProcessor.processingEnv protected字段访问的Messager实例。以下行将在源处理阶段为每个错误注解的元素输出错误:

1
2
3
4
otherMethods.forEach(element ->
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@BuilderProperty must be applied to a setXxx method "
+ "with a single argument", element));

当然,如果正确的setter集合为空,则没有必要继续当前的类型元素集迭代:

1
2
3
if (setters.isEmpty()) {
continue;
}

如果setter集合至少有一个元素,我们将使用它从封闭元素中获取完全限定的类名:

1
2
String className = ((TypeElement) setters.get(0)
.getEnclosingElement()).getQualifiedName().toString();

生成构建器类所需的最后一点信息是setter名称和参数类型名称之间的映射:

1
2
3
4
5
Map<String, String> setterMap = setters.stream().collect(Collectors.toMap(
setter -> setter.getSimpleName().toString(),
setter -> ((ExecutableType) setter.asType())
.getParameterTypes().get(0).toString()
));

6.3 生成输出文件

现在我们拥有生成构建器类所需的所有信息:源类的名称,所有setter名称及其参数类型。

要生成输出文件,我们将使用AbstractProcessor.processingEnv protected属性中的对象再次提供的Filer实例:

1
2
3
4
5
JavaFileObject builderFile = processingEnv.getFiler()
.createSourceFile(builderClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
// writing generated file to out …
}

下面提供了writeBuilderFile方法的完整代码。我们只需要为源类和构建器类计算包名,完全限定的构建器类名和简单类名。其余的代码非常简单。

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
private void writeBuilderFile(
String className, Map<String, String> setterMap)
throws IOException {

String packageName = null;
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0, lastDot);
}

String simpleClassName = className.substring(lastDot + 1);
String builderClassName = className + "Builder";
String builderSimpleClassName = builderClassName
.substring(lastDot + 1);

JavaFileObject builderFile = processingEnv.getFiler()
.createSourceFile(builderClassName);

try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {

if (packageName != null) {
out.print("package ");
out.print(packageName);
out.println(";");
out.println();
}

out.print("public class ");
out.print(builderSimpleClassName);
out.println(" {");
out.println();

out.print(" private ");
out.print(simpleClassName);
out.print(" object = new ");
out.print(simpleClassName);
out.println("();");
out.println();

out.print(" public ");
out.print(simpleClassName);
out.println(" build() {");
out.println(" return object;");
out.println(" }");
out.println();

setterMap.entrySet().forEach(setter -> {
String methodName = setter.getKey();
String argumentType = setter.getValue();

out.print(" public ");
out.print(builderSimpleClassName);
out.print(" ");
out.print(methodName);

out.print("(");

out.print(argumentType);
out.println(" value) {");
out.print(" object.");
out.print(methodName);
out.println("(value);");
out.println(" return this;");
out.println(" }");
out.println();
});

out.println("}");
}
}

7. 运行示例

要查看代码生成的实际操作,您应该从公共父根编译两个模块,或者首先编译annotation-processor模块,然后编译annotation-user模块。

生成的PersonBuilder类可以在annotation-user / target / generated-sources / annotations / com / doleje / annotation / PersonBuilder.java文件中找到,应该如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.doleje.annotation;

public class PersonBuilder {

private Person object = new Person();

public Person build() {
return object;
}

public PersonBuilder setName(java.lang.String value) {
object.setName(value);
return this;
}

public PersonBuilder setAge(int value) {
object.setAge(value);
return this;
}
}

8. 注册处理器的其他方法

要在编译阶段使用注解处理器,您还有其他几个选项,具体取决于您的使用案例和您使用的工具。

8.1 使用注解处理器工具

apt工具是用于处理源文件一个特殊的命令行实用程序。它是Java 5的一部分,但是从Java 7开始,它被弃用以支持其他选项并在Java 8中完全删除。本文不讨论它。

8.2 使用编译器密钥

-processor编译器关键是一个标准的JDK设施,以增加编译器的源处理阶段,自己的注解处理器。

请注意,处理器本身和注解必须已在单独的编译中编译为类,并出现在类路径中,因此您应该做的第一件事是:

1
2
javac com/doleje/annotation/processor/BuilderProcessor
javac com/doleje/annotation/processor/BuilderProperty

然后使用-processor键指定您刚刚编译的注解处理器类,对源进行实际编译:

1
javac -processor com.doleje.annotation.processor.MyProcessor Person.java

要一次指定多个注解处理器,可以用逗号分隔它们的类名,如下所示:

1
javac -processor package1.Processor1,package2.Processor2 SourceFile.java

8.3 使用Maven

maven-compiler-plugin允许指定注解处理器作为其配置的一部分。

这是为编译器插件添加注解处理器的示例。您还可以使用generatedSourcesDirectory配置参数指定要将生成的源放入的目录。

请注意,BuilderProcessor类应该已经编译,例如,从构建依赖项中的另一个jar导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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>
<generatedSourcesDirectory>${project.build.directory}
/generated-sources/</generatedSourcesDirectory>
<annotationProcessors>
<annotationProcessor>
com.doleje.annotation.processor.BuilderProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>

</plugins>
</build>

8.4 将处理器Jar添加到Classpath

您可以简单地将具有处理器类的特殊结构化jar添加到编译器的类路径中,而不是在编译器选项中指定注解处理器。

要自动获取它,编译器必须知道处理器类的名称。因此,您必须在META-INF / services / javax.annotation.processing.Processor文件中将其指定为处理器的完全限定类名:

1
com.doleje.annotation.processor.BuilderProcessor

您还可以指定此jar中的多个处理器,通过用新行分隔它们来自动拾取:

1
package1.Processor1``package2.Processor2``package3.Processor3

如果您使用Maven构建此jar并尝试将此文件直接放入src/main/resources/META-INF/services目录中,您将遇到以下错误:

1
2
3
[ERROR] Bad service configuration file, or exception thrown while
constructing Processor object: javax.annotation.processing.Processor:
Provider com.doleje.annotation.processor.BuilderProcessor not found

这是因为当尚未编译BuilderProcessor文件时,编译器尝试在模块本身的source-processing阶段使用此文件。该文件必须放在另一个资源目录中,并在Maven构建的资源复制阶段复制到META-INF / services目录,或者在构建期间生成(甚至更好)。

以下部分中讨论的Google auto-service库允许使用简单的注解生成此文件。

8.5 使用Google auto-service库

要自动生成注册文件,您可以使用Google auto-service库中的@AutoService注解,如下所示:

1
2
3
4
@AutoService(Processor.class)
public BuilderProcessor extends AbstractProcessor {
// …
}

该注解本身由注解处理器从auto-service库处理。此处理器生成包含BuilderProcessor类名的META-INF / services / javax.annotation.processing.Processor文件。

9. 结论

在本文中,我们使用为POJO生成Builder类的示例演示了源级注释处理。我们还提供了几种在项目中注册注释处理器的替代方法。