java注解-网络博客

一、介绍

本文介绍了 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 的自动服务库来生成稍后将讨论的处理器元数据文件,以及针对 Java 8 源代码调整的maven-compiler-plugin。这些依赖项的版本被提取到属性部分。

最新版本的[自动服务](https://search.maven.org/classic/#search|gav|1|g%3A"com.google.auto.service" AND a%3A”auto-service”)库和[maven-compiler-plugin](https://search.maven.org/classic/#search|gav|1|g%3A"org.apache.maven.plugins" AND a%3A”maven-compiler-plugin”)可以在 Maven 中央存储库中找到:

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>

带有注释源的annotation-user Maven 模块不需要任何特殊调整,除了在依赖项部分添加对 annotation-processor 模块的依赖:

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

5. 定义注释

假设我们的annotation-user模块中有一个简单的 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 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. 实现*处理器*

6.1. 创建一个*AbstractProcessor*子类

我们将从在注释处理器Maven 模块中扩展AbstractProcessor类开始。

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

所述*@AutoService注释是的一部分自动服务*库,并允许生成,这将在下面的章节进行说明处理器的元数据。

1
2
3
4
5
6
7
8
9
10
11
12
@SupportedAnnotationTypes(
"com.baeldung.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.baeldung.annotation.*”来处理com.baeldung.annotation包及其所有子包中的注释,甚至可以“*”来处理所有注释.

我们必须实现的单一方法是处理本身的process方法。编译器为每个包含匹配注释的源文件调用它。

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

如果您的注释处理器已经处理了所有传递的注释,并且您不希望它们被传递到列表中的其他注释处理器,则返回布尔值应该为true

6.2. 收集数据

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

首先,我们需要遍历在类中找到的所有注释类型——在我们的例子中,注释集将有一个与*@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保护字段访问的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));

当然,如果正确的 setters 集合为空,则继续当前类型元素集合迭代是没有意义的:

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

如果 setter 集合至少有一个元素,我们将使用它从封闭元素中获取完全限定的类名,在 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受保护属性中的对象再次提供的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/baeldung/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.baeldung.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. 使用注释处理器工具

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

8.2. 使用编译器密钥

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

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

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

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

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

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

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

8.3. 使用 Maven

Maven的编译器插件允许指定注释处理器作为其结构的一部分。

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

请注意,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.baeldung.annotation.processor.BuilderProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>

</plugins>
</build>

8.4. 将处理器 Jar 添加到类路径

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

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

1
com.baeldung.annotation.processor.BuilderProcessor

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

1
2
3
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.baeldung.annotation.processor.BuilderProcessor not found

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

下一节中讨论的 Google自动服务库允许使用简单的注释生成此文件。

8.5. 使用 Google“auto service”库

要自动生成注册文件,您可以使用Google 的自动服务库中的*@AutoService*注释,如下所示:

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

此注释本身由来自自动服务库的注释处理器处理。该处理器生成包含BuilderProcessor类名的META-INF/services/javax.annotation.processing.Processor文件。

9. 结论

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

本文的源代码可在 GitHub 上找到

原文:

1. Introduction

This article is an intro to Java source-level annotation processing and provides examples of using this technique for generating additional source files during compilation.

2. Applications of Annotation Processing

The source-level annotation processing first appeared in Java 5. It is a handy technique for generating additional source files during the compilation stage.

The source files don’t have to be Java files — you can generate any kind of description, metadata, documentation, resources, or any other type of files, based on annotations in your source code.

Annotation processing is actively used in many ubiquitous Java libraries, for instance, to generate metaclasses in QueryDSL and JPA, to augment classes with boilerplate code in Lombok library.

An important thing to note is the limitation of the annotation processing API — it can only be used to generate new files, not to change existing ones.

The notable exception is the Lombok library which uses annotation processing as a bootstrapping mechanism to include itself into the compilation process and modify the AST via some internal compiler APIs. This hacky technique has nothing to do with the intended purpose of annotation processing and therefore is not discussed in this article.

3. Annotation Processing API

The annotation processing is done in multiple rounds. Each round starts with the compiler searching for the annotations in the source files and choosing the annotation processors suited for these annotations. Each annotation processor, in turn, is called on the corresponding sources.

If any files are generated during this process, another round is started with the generated files as its input. This process continues until no new files are generated during the processing stage.

Each annotation processor, in turn, is called on the corresponding sources. If any files are generated during this process, another round is started with the generated files as its input. This process continues until no new files are generated during the processing stage.

The annotation processing API is located in the javax.annotation.processing package. The main interface that you’ll have to implement is the Processor interface, which has a partial implementation in the form of AbstractProcessor class. This class is the one we’re going to extend to create our own annotation processor.

4. Setting Up the Project

To demonstrate the possibilities of annotation processing, we will develop a simple processor for generating fluent object builders for annotated classes.

We’re going to split our project into two Maven modules. One of them, annotation-processor module, will contain the processor itself together with the annotation, and another, the annotation-user module, will contain the annotated class. This is a typical use case of annotation processing.

The settings for the annotation-processor module are as follows. We’re going to use the Google’s auto-service library to generate processor metadata file which will be discussed later, and the maven-compiler-plugin tuned for the Java 8 source code. The versions of these dependencies are extracted to the properties section.

Latest versions of the [auto-service](https://search.maven.org/classic/#search|gav|1|g%3A"com.google.auto.service" AND a%3A”auto-service”) library and [maven-compiler-plugin](https://search.maven.org/classic/#search|gav|1|g%3A"org.apache.maven.plugins" AND a%3A”maven-compiler-plugin”) can be found in Maven Central repository:

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>

The annotation-user Maven module with the annotated sources does not need any special tuning, except adding a dependency on the annotation-processor module in the dependencies section:

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

5. Defining an Annotation

Suppose we have a simple POJO class in our annotation-user module with several fields:

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

private int age;

private String name;

// getters and setters …

}

We want to create a builder helper class to instantiate the Person class more fluently:

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

This PersonBuilder class is an obvious choice for a generation, as its structure is completely defined by the Person setter methods.

Let’s create a @BuilderProperty annotation in the annotation-processor module for the setter methods. It will allow us to generate the Builder class for each class that has its setter methods annotated:

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

The @Target annotation with the ElementType.METHOD parameter ensures that this annotation can be only put on a method.

The SOURCE retention policy means that this annotation is only available during source processing and is not available at runtime.

The Person class with properties annotated with the @BuilderProperty annotation will look as follows:

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. Implementing a *Processor*

6.1. Creating an *AbstractProcessor* Subclass

We’ll start with extending the AbstractProcessor class inside the annotation-processor Maven module.

First, we should specify annotations that this processor is capable of processing, and also the supported source code version. This can be done either by implementing the methods getSupportedAnnotationTypes and getSupportedSourceVersion of the Processor interface or by annotating your class with @SupportedAnnotationTypes and @SupportedSourceVersion annotations.

The @AutoService annotation is a part of the auto-service library and allows to generate the processor metadata which will be explained in the following sections.

1
2
3
4
5
6
7
8
9
10
11
12
@SupportedAnnotationTypes(
"com.baeldung.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;
}
}

You can specify not only the concrete annotation class names but also wildcards, like “com.baeldung.annotation.*” to process annotations inside the com.baeldung.annotation package and all its sub packages, or even “*” to process all annotations.

The single method that we’ll have to implement is the process method that does the processing itself. It is called by the compiler for every source file containing the matching annotations.

Annotations are passed as the first Set<? extends TypeElement> annotations argument, and the information about the current processing round is passed as the RoundEnviroment roundEnv argument.

The return boolean value should be true if your annotation processor has processed all the passed annotations, and you don’t want them to be passed to other annotation processors down the list.

6.2. Gathering Data

Our processor does not really do anything useful yet, so let’s fill it with code.

First, we’ll need to iterate through all annotation types that are found in the class — in our case, the annotations set will have a single element corresponding to the @BuilderProperty annotation, even if this annotation occurs multiple times in the source file.

Still, it’s better to implement the process method as an iteration cycle, for completeness sake:

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;
}

In this code, we use the RoundEnvironment instance to receive all elements annotated with the @BuilderProperty annotation. In the case of the Person class, these elements correspond to the setName and setAge methods.

@BuilderProperty annotation’s user could erroneously annotate methods that are not actually setters. The setter method name should start with set, and the method should receive a single argument. So let’s separate the wheat from the chaff.

In the following code, we use the Collectors.partitioningBy() collector to split annotated methods into two collections: correctly annotated setters and other erroneously annotated methods:

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);

Here we use the Element.asType() method to receive an instance of the TypeMirror class which gives us some ability to introspect types even though we are only at the source processing stage.

We should warn the user about incorrectly annotated methods, so let’s use the Messager instance accessible from the AbstractProcessor.processingEnv protected field. The following lines will output an error for each erroneously annotated element during the source processing stage:

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));

Of course, if the correct setters collection is empty, there is no point of continuing the current type element set iteration:

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

If the setters collection has at least one element, we’re going to use it to get the fully qualified class name from the enclosing element, which in case of the setter method appears to be the source class itself:

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

The last bit of information we need to generate a builder class is a map between the names of the setters and the names of their argument types:

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. Generating the Output File

Now we have all the information we need to generate a builder class: the name of the source class, all its setter names, and their argument types.

To generate the output file, we’ll use the Filer instance provided again by the object in the AbstractProcessor.processingEnv protected property:

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

The complete code of the writeBuilderFile method is provided below. We only need to calculate the package name, fully qualified builder class name, and simple class names for the source class and the builder class. The rest of the code is pretty straightforward.

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. Running the Example

To see the code generation in action, you should either compile both modules from the common parent root or first compile the annotation-processor module and then the annotation-user module.

The generated PersonBuilder class can be found inside the annotation-user/target/generated-sources/annotations/com/baeldung/annotation/PersonBuilder.java file and should look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.baeldung.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. Alternative Ways of Registering a Processor

To use your annotation processor during the compilation stage, you have several other options, depending on your use case and the tools you use.

8.1. Using the Annotation Processor Tool

The apt tool was a special command line utility for processing source files. It was a part of Java 5, but since Java 7 it was deprecated in favour of other options and removed completely in Java 8. It will not be discussed in this article.

8.2. Using the Compiler Key

The -processor compiler key is a standard JDK facility to augment the source processing stage of the compiler with your own annotation processor.

Note that the processor itself and the annotation have to be already compiled as classes in a separate compilation and present on the classpath, so the first thing you should do is:

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

Then you do the actual compilation of your sources with the -processor key specifying the annotation processor class you’ve just compiled:

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

To specify several annotation processors in one go, you can separate their class names with commas, like this:

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

8.3. Using Maven

The maven-compiler-plugin allows specifying annotation processors as part of its configuration.

Here’s an example of adding annotation processor for the compiler plugin. You could also specify the directory to put generated sources into, using the generatedSourcesDirectory configuration parameter.

Note that the BuilderProcessor class should already be compiled, for instance, imported from another jar in the build dependencies:

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.baeldung.annotation.processor.BuilderProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>

</plugins>
</build>

8.4. Adding a Processor Jar to the Classpath

Instead of specifying the annotation processor in the compiler options, you may simply add a specially structured jar with the processor class to the classpath of the compiler.

To pick it up automatically, the compiler has to know the name of the processor class. So you have to specify it in the META-INF/services/javax.annotation.processing.Processor file as a fully qualified class name of the processor:

1
com.baeldung.annotation.processor.BuilderProcessor

You can also specify several processors from this jar to pick up automatically by separating them with a new line:

1
2
3
package1.Processor1
package2.Processor2
package3.Processor3

If you use Maven to build this jar and try to put this file directly into the src/main/resources/META-INF/services directory, you’ll encounter the following error:

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

This is because the compiler tries to use this file during the source-processing stage of the module itself when the BuilderProcessor file is not yet compiled. The file has to be either put inside another resource directory and copied to the META-INF/services directory during the resource copying stage of the Maven build, or (even better) generated during the build.

The Google auto-service library, discussed in the following section, allows generating this file using a simple annotation.

8.5. Using the Google *auto-service* Library

To generate the registration file automatically, you can use the @AutoService annotation from the Google’s auto-service library, like this:

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

This annotation is itself processed by the annotation processor from the auto-service library. This processor generates the META-INF/services/javax.annotation.processing.Processor file containing the BuilderProcessor class name.

9. Conclusion

In this article, we’ve demonstrated source-level annotation processing using an example of generating a Builder class for a POJO. We have also provided several alternative ways of registering annotation processors in your project.

The source code for the article is available on GitHub.


官方qpi文档

可以看到,AbstractProcessor实现了接口Processor,那么,我们在来看下Processor的api文档:

javax.annotation.processing
Interface Processor

All Known Implementing Classes:
AbstractProcessor


public interface Processor

The interface for an annotation processor.

Annotation processing happens in a sequence of rounds. On each round, a processor may be asked to [process](https://link.jianshu.com?t=http://docs.oracle.com/javase/7/docs/api/javax/annotation/processing/Processor.html#process(java.util.Set, javax.annotation.processing.RoundEnvironment)) a subset of the annotations found on the source and class files produced by a prior round. The inputs to the first round of processing are the initial inputs to a run of the tool; these initial inputs can be regarded as the output of a virtual zeroth round of processing. If a processor was asked to process on a given round, it will be asked to process on subsequent rounds, including the last round, even if there are no annotations for it to process. The tool infrastructure may also ask a processor to process files generated implicitly by the tool’s operation.
Each implementation of a Processor must provide a public no-argument constructor to be used by tools to instantiate the processor. The tool infrastructure will interact with classes implementing this interface as follows:

  1. If an existing Processor object is not being used, to create an instance of a processor the tool calls the no-arg constructor of the processor class.
  2. Next, the tool calls the init method with an appropriate ProcessingEnvironment .
  3. Afterwards, the tool calls getSupportedAnnotationTypes , getSupportedOptions , and getSupportedSourceVersion . These methods are only called once per run, not on each round.
  4. As appropriate, the tool calls the [process
    ](https://link.jianshu.com?t=http://docs.oracle.com/javase/7/docs/api/javax/annotation/processing/Processor.html#process(java.util.Set, javax.annotation.processing.RoundEnvironment)) method on the Processor object; a new Processor object is not created for each round.

If a processor object is created and used without the above protocol being followed, then the processor’s behavior is not defined by this interface specification.The tool uses a discovery process to find annotation processors and decide whether or not they should be run. By configuring the tool, the set of potential processors can be controlled. For example, for a JavaCompiler the list of candidate processors to run can be set directly or controlled by a search path used for a service-style lookup. Other tool implementations may have different configuration mechanisms, such as command line options; for details, refer to the particular tool’s documentation. Which processors the tool asks to [run](https://link.jianshu.com?t=http://docs.oracle.com/javase/7/docs/api/javax/annotation/processing/Processor.html#process(java.util.Set, javax.annotation.processing.RoundEnvironment)) is a function of what annotations are present on the root elements, what annotation types a processor processes, and whether or not a processor [claims the annotations it processes](https://link.jianshu.com?t=http://docs.oracle.com/javase/7/docs/api/javax/annotation/processing/Processor.html#process(java.util.Set, javax.annotation.processing.RoundEnvironment)). A processor will be asked to process a subset of the annotation types it supports, possibly an empty set. For a given round, the tool computes the set of annotation types on the root elements. If there is at least one annotation type present, as processors claim annotation types, they are removed from the set of unmatched annotations. When the set is empty or no more processors are available, the round has run to completion. If there are no annotation types present, annotation processing still occurs but only universal processors which support processing “
can claim the (empty) set of annotation types.
Note that if a processor supports “

and returns true
, all annotations are claimed. Therefore, a universal processor being used to, for example, implement additional validity checks should return false
so as to not prevent other such checkers from being able to run.
If a processor throws an uncaught exception, the tool may cease other active annotation processors. If a processor raises an error, the current round will run to completion and the subsequent round will indicate an error was raised. Since annotation processors are run in a cooperative environment, a processor should throw an uncaught exception only in situations where no error recovery or reporting is feasible.
The tool environment is not required to support annotation processors that access environmental resources, either per round or cross-round, in a multi-threaded fashion.
If the methods that return configuration information about the annotation processor return null
, return other invalid input, or throw an exception, the tool infrastructure must treat this as an error condition.
To be robust when running in different tool implementations, an annotation processor should have the following properties:

  1. The result of processing a given input is not a function of the presence or absence of other inputs (orthogonality).
  2. Processing the same input produces the same output (consistency).
  3. Processing input A followed by processing input B is equivalent to processing B then A(commutativity)
  4. Processing an input does not rely on the presence of the output of other annotation processors (independence)

The Filer interface discusses restrictions on how processors can operate on files.
Note that implementors of this interface may find it convenient to extend AbstractProcessor rather than implementing this interface directly.

Since:
1.6

注解处理发生在一系列回合中.每个回合中,注解处理器都有可能被叫去处理由上一次注解产生的源码和类文件中的找到的注解子集.第一次注解处理回合的输入就是工具第一次运行的输入;这些初始输入可以认为是一个虚拟的第零次注解处理回合的输出.如果注解处理器被叫去处理一个特定的回合,那么接下来的回合它都会继续处理,即使后续回合没有它需要处理的注解.注解处理器有可能会被叫去处理由(编译)工具隐式生成的文件.每个注解处理器的实现都必须提供一个公有的无参构造函数,由工具进行实例化.工具会与实现该接口(Processor)的类进行如下交互:

  1. 如果一个已存在的Processor实例未被使用,(编译)工具会调用注解处理器的无参构造函数实例化出一个Processor对象.
  2. 接下来,(编译)工具会调用init函数,并传入一个合适的ProcessingEnvironment.
  3. 之后,(编译)工具会调用getSupportedAnnotationTypes,getSupportedOptions和getSupportedSourceVersion.这些方法只会在每一次运行时被调用一次,而不会在每个注解回合都被调用.
  4. 正常情况下,(编译)工具会调用注解处理器实例的process函数;每个注解回合并不会产生新的注解实例.

如果一个注解处理器实例被创建,但是使用却没有遵循上述协议,那么这个注解处理器的行为并未被该接口规范定义.(编译)工具使用搜索程序去找到注解处理器并决定它们是否得以运行.通过配置(编译)工具,潜在的注解处理器可以被控制.比如,对于javaCompiler,候选处理器可以直接被指定或者通过使用service-style查找指定搜索路径进行控制.其他(编译)工具可以具有不同的配置机制,比如控制行选项;具体点讲,参考特定工具文档.(编译)工具会调用运行的注解处理器是由root elements指示的注解,是注解处理器处理的注解类型和注解处理器声明它要处理的注解的方法.注解处理器会被叫去处理它支持的注解类型子集,有可能是一个空的集合.在给定回合,(编译)工具会计算root elements的注解类型集合.如果有最少一个注解类型存在,就是注解处理器声明的注解类型之一,它们就会被从未匹配的注解类型集合中移除.当(未匹配)注解集合为空或者没有其它的注解处理器,那么该注解处理回合就结束了.如果没有声明注解类型,只有通用处理器(支持处理”“声明(空)所有注解类型集合)仍然会进行注解处理.注意如果一个注解处理器支持”“并且返回true,则所有的注解类型都被声明.因此,一个通用注解处理器如果被用于实现附加有效检验,那么应该返回false,为了不防止这类检验器得以运行.如果一个注解处理器抛出了一个未捕获异常,(编译)工具可能会停止其他活动的注解处理器.如果一个注解处理器引起了一个错误,当前注解回合会结束,并且后续回合会指明一个错误产生了.因为注解处理器都是运行在共同协作的环境中,只有当错误恢复或报告提交是无法执行的情况下,注解处理器才允许抛出一个未捕获异常.
(编译)工具环境不要求要支持注解处理器能以多线程方式在每一回合或交叉回合能访问环境资源.
如果返回注解处理器的配置信息的方法返回null,返回其他无效输入,或者抛出一个异常,(编译)工具必须将这些当做是一个错误条件.
为了在不同的工具实现能够健壮运行,注解处理器必须有以下性能:

  1. 对于一个给定的输入的处理结果,不影响其他输入的存在或缺失(正交性)
  2. 处于相同的输入会产生相同的输出(一致性)
  3. 先处理输入A,然后处于输入B等同于先处理B在处理A(可交换性)
  4. 处理输入会依赖于其他注解处理器的输出(独立性)

Filer接口讨论了注解处理器操作文件的限定.
请知悉Processor的实现通过继承AbstractProcessor会比直接实现该接口更加方便.

简单总结如下:

  • Annotation Processor可能会被多次调用.
  • Annotation Processor被调用一次后,后续若还有注解处理,该Annotation Processor仍然会继续被调用.
  • 自定义Annotation Processor必须带有一个无参构造函数,让javac进行实例化.
  • 如果Annotation Processor抛出一个未捕获异常,javac可能会停止其他的Annotation Processor.只有在无法抛出错误或报告的情况下,才允许抛出异常.
  • Annotation Processor运行在一个独立的jvm中,所以可以将它看成是一个java应用程序.

作者:Whyn
链接:https://www.jianshu.com/p/b6b3283968e0
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


插件化注解处理API(Pluggable Annotation Processing API)

Java奇技淫巧-插件化注解处理API(Pluggable Annotation Processing API)

参考资料

简介

插件化注解处理(Pluggable Annotation Processing)APIJSR 269提供一套标准API来处理AnnotationsJSR 175,实际上JSR 269不仅仅用来处理Annotation,我觉得更强大的功能是它建立了Java 语言本身的一个模型,它把method、package、constructor、type、variable、enum、annotation等Java语言元素映射为Types和Elements,从而将Java语言的语义映射成为对象,我们可以在javax.lang.model包下面可以看到这些类。所以我们可以利用JSR 269提供的API来构建一个功能丰富的元编程(metaprogramming)环境。JSR 269用Annotation Processor在编译期间而不是运行期间处理Annotation, Annotation Processor相当于编译器的一个插件,所以称为插入式注解处理.如果Annotation Processor处理Annotation时(执行process方法)产生了新的Java代码,编译器会再调用一次Annotation Processor,如果第二次处理还有新代码产生,就会接着调用Annotation Processor,直到没有新代码产生为止。每执行一次process()方法被称为一个”round”,这样整个Annotation processing过程可以看作是一个round的序列。JSR 269主要被设计成为针对Tools或者容器的API。这个特性虽然在JavaSE 6已经存在,但是很少人知道它的存在。下一篇介绍的Java奇技淫巧-lombok就是使用这个特性实现编译期的代码插入的。另外,如果没有猜错,像IDEA在编写代码时候的标记语法错误的红色下划线也是通过这个特性实现的。KAPT(Annotation Processing for Kotlin),也就是Kotlin的编译也是通过此特性的。

Pluggable Annotation Processing API的核心是Annotation Processor即注解处理器,一般需要继承抽象类javax.annotation.processing.AbstractProcessor。注意,与运行时注解RetentionPolicy.RUNTIME不同,注解处理器只会处理编译期注解,也就是RetentionPolicy.SOURCE的注解类型,处理的阶段位于Java代码编译期间。

使用步骤

插件化注解处理API的使用步骤大概如下:

  • 1、自定义一个Annotation Processor,需要继承javax.annotation.processing.AbstractProcessor,并覆写process方法。
  • 2、自定义一个注解,注解的元注解需要指定@Retention(RetentionPolicy.SOURCE)
  • 3、需要在声明的自定义Annotation Processor中使用javax.annotation.processing.SupportedAnnotationTypes指定在第2步创建的注解类型的名称(注意需要全类名,”包名.注解类型名称”,否则会不生效)。
  • 4、需要在声明的自定义Annotation Processor中使用javax.annotation.processing.SupportedSourceVersion指定编译版本。
  • 5、可选操作,可以通在声明的自定义Annotation Processor中使用javax.annotation.processing.SupportedOptions指定编译参数。

实战例子

基础#

下面我们模仿一下测试框架Junit里面的@Test注解,在运行时通过Annotation Processor获取到使用了自定义的@Test注解对应的方法的信息。因为如果想要动态修改一个类或者方法的代码内容,需要使用到字节码修改工具例如ASM等,这些操作过于深入,日后再谈。先定义一个注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package club.throwable.processor;

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

/**
* @author throwable
* @version v1.0
* @description
* @since 2018/5/27 11:18
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Test {

}

定义一个注解处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SupportedAnnotationTypes(value = {"club.throwable.processor.Test"})
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
public class AnnotationProcessor extends AbstractProcessor {

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("Log in AnnotationProcessor.process");
for (TypeElement typeElement : annotations) {
System.out.println(typeElement);
}
System.out.println(roundEnv);
return true;
}
}

编写一个主类:

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

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

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

}
}

接着需要指定Processor,如果使用IDEA的话,Compiler->Annotation Processors中的Enable annotation processing必须勾选。然后可以通过下面几种方式指定指定Processor。

  • 1、直接使用编译参数指定,例如:javac -processor club.throwable.processor.AnnotationProcessor Main.java。
  • 2、通过服务注册指定,就是META-INF/services/javax.annotation.processing.Processor文件中添加club.throwable.processor.AnnotationProcessor。
  • 3、通过Maven的编译插件的配置指定如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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>
club.throwable.processor.AnnotationProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>

值得注意的是,以上三点生效的前提是club.throwable.processor.AnnotationProcessor已经被编译过,否则编译的时候就会报错:

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

解决方法有两种,第一种是提前使用命令或者IDEA右键club.throwable.processor.AnnotationProcessor对它进行编译;第二种是把club.throwable.processor.AnnotationProcessor放到一个独立的Jar包引入。我在这里使用第一种方式解决。

最后,使用Maven命令mvn compile进行编译。输出如下:

1
2
3
4
5
6
Log in AnnotationProcessor.process
[errorRaised=false, rootElements=[club.throwable.processor.Test,club.throwable.processor.Main, club.throwable.processor.AnnotationProcessor, processingOver=false]
Log in AnnotationProcessor.process
[errorRaised=false, rootElements=[], processingOver=false]
Log in AnnotationProcessor.process
[errorRaised=false, rootElements=[], processingOver=true]

可见编译期间AnnotationProcessor生效了。

进阶#

下面是一个例子直接修改类的代码,为实体类的Setter方法对应的属性生成一个Builder类,也就是原来的类如下:

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

private Integer age;
private String name;

public Integer getAge() {
return age;
}

@Builder
public void setAge(Integer age) {
this.age = age;
}

public String getName() {
return name;
}

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

生成的Builder类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}
}

自定义的注解如下:

1
2
3
4
5
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
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
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
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.lang.model.type.ExecutableType;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
* @author throwable
* @version v1.0
* @description
* @since 2018/5/27 11:21
*/
@SupportedAnnotationTypes(value = {"club.throwable.processor.builder.Builder"})
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
public class BuilderProcessor extends AbstractProcessor {

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement typeElement : annotations) {
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(typeElement);
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);
otherMethods.forEach(element ->
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@Builder must be applied to a setXxx method "
+ "with a single argument", element));
Map<String, String> setterMap = setters.stream().collect(Collectors.toMap(
setter -> setter.getSimpleName().toString(),
setter -> ((ExecutableType) setter.asType())
.getParameterTypes().get(0).toString()
));
String className = ((TypeElement) setters.get(0)
.getEnclosingElement()).getQualifiedName().toString();
try {
writeBuilderFile(className, setterMap);
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}

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.forEach((methodName, argumentType) -> {
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("}");
}
}
}

主类如下:

1
2
3
4
5
6
7
public class Main {

public static void main(String[] args) throws Exception{
//PersonBuilder在编译之后才会生成,这里需要编译后才能这样写
Person person = new PersonBuilder().setAge(25).setName("doge").build();
}
}

先手动编译BuilderProcessor,然后在META-INF/services/javax.annotation.processing.Processor文件中添加club.throwable.processor.builder.BuilderProcessor,最后执行Maven命令mvn compile进行编译。

编译后控制台输出:

1
[errorRaised=false, rootElements=[club.throwable.processor.builder.PersonBuilder], processingOver=false]

编译成功之后,target/classes包下面的club.throwable.processor.builder子包路径中会新增了一个类PersonBuilder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package club.throwable.processor.builder;

public class PersonBuilder {
private Person object = new Person();

public PersonBuilder() {
}

public Person build() {
return this.object;
}

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

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

这个类就是编译期新增的。在这个例子中,编译期新增的类貌似没有什么作用。但是,如果像lombok那样对原来的实体类添加新的方法,那样的话就比较有用了。因为些类或者方法是编译期添加的,因此在代码中直接使用会标红。因此,lombok提供了IDEA或者eclipse的插件,插件的功能的实现估计也是用了插件式注解处理API。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!