Java Annotation Processing and Creating a Builder – Java注释处理和创建生成器

最后修改: 2016年 9月 30日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

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.

本文是Java源码级注释处理的介绍,并提供了在编译过程中使用该技术生成额外源码文件的例子。

2. Applications of Annotation Processing

2.注释处理的应用

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

源码级注解处理首次出现在Java 5中。它是一种方便的技术,用于在编译阶段生成额外的源文件。

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.

源文件不一定是Java文件–你可以根据源代码中的注释,生成任何类型的描述、元数据、文档、资源或任何其他类型的文件。

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.

注释处理被积极地用于许多无处不在的Java库中,例如,在QueryDSL和JPA中生成元类,在Lombok库中用模板代码增强类的功能。

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.

需要注意的一件事是注释处理API的限制–它只能用于生成新的文件,而不能改变现有的文件

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.

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

3. Annotation Processing API

3.注释处理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.

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

4. Setting Up the Project

4.设置项目

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.

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

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.

annotation-processor模块的设置如下。我们将使用谷歌的自动服务库来生成处理器元数据文件,这一点将在后面讨论,而maven-compiler-plugin为Java 8源代码进行了调整。这些依赖关系的版本被提取到属性部分。

Latest versions of the auto-service library and maven-compiler-plugin can be found in Maven Central repository:

自动服务库和maven-compiler-plugin的最新版本可以在Maven Central资源库中找到。

<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:

带有注释源的annotation-user Maven模块不需要任何特殊调整,只需在依赖关系部分添加对注释处理器模块的依赖即可。

<dependency>
    <groupId>com.baeldung</groupId>
    <artifactId>annotation-processing</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

5. Defining an Annotation

5.定义一个注释

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

假设我们的annotation-user模块中有一个简单的POJO类,有几个字段。

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:

我们想创建一个构建器辅助类,以便更流畅地实例化Person类。

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.

这个PersonBuilder类是一个明显的选择,因为它的结构完全由Personsetter方法定义。

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:

让我们在annotation-processor模块中为setter方法创建一个@BuilderProperty注释。它将允许我们为每个有setter方法注释的类生成Builder类。

@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.

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

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

SOURCE保留策略意味着该注释仅在源处理期间可用,在运行时不可用。

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

带有@BuilderProperty注解的属性的Person类将看起来如下。

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.实现一个处理器

6.1. Creating an AbstractProcessor Subclass

6.1.创建一个AbstractProcessor子类

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

我们将从扩展annotation-processor Maven模块内的AbstractProcessor类开始。

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.

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

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.

@AutoService注解是自动服务库的一部分,允许生成处理器元数据,这将在下面的章节中解释。

@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.

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

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.

我们必须实现的唯一方法是process方法,它本身就进行处理。编译器会对每个包含匹配注释的源文件调用该方法。

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.

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

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.

如果你的注解处理器已经处理了所有传递的注解,并且你不希望它们被传递给列表中的其他注解处理器,那么返回的boolean值应该是true

6.2. Gathering Data

6.2.收集数据

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.

首先,我们需要遍历在类中发现的所有注解类型–在我们的例子中,annotations集将有一个对应于@BuilderProperty注解的单一元素,即使这个注解在源文件中出现多次。

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

不过,为了完整起见,最好还是把process方法实现为一个迭代周期。

@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.

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

@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.

@BuilderProperty 注解的用户可能会错误地注解那些实际上不是设置器的方法。设置器方法的名称应该以set开头,并且该方法应该接收一个参数。所以让我们把麦子和糠秕分开。

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:

在下面的代码中,我们使用Collectors.partitioningBy()收集器将注释的方法分成两个集合:正确注释的设置器和其他错误注释的方法。

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.

这里我们使用Element.asType()方法来接收TypeMirror类的一个实例,这给了我们一些内省类型的能力,尽管我们只处于源处理阶段。

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:

我们应该就错误的注释方法向用户发出警告,所以让我们使用可从AbstractProcessor.processingEnv保护字段访问的Messager实例。下面几行将在源处理阶段为每个被错误注释的元素输出一个错误。

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:

当然,如果正确的setters集合是空的,那么继续当前类型元素集合迭代就没有意义了。

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:

如果setters集合至少有一个元素,我们将使用它从包围的元素中获得完全合格的类名,在setter方法的情况下,这似乎是源类本身。

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:

我们需要生成一个构建器类的最后一点信息是设置器的名称和其参数类型的名称之间的映射。

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

6.3.生成输出文件

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:

为了生成输出文件,我们将使用Filer实例,由AbstractProcessor.processingEnv保护属性中的对象再次提供。

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.

下面提供了writeBuilderFile方法的完整代码。我们只需要计算包名、完全合格的构建器类名以及源类和构建器类的简单类名。其余的代码是非常直接的。

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

7.运行实例

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.

要看到代码生成的效果,你应该从共同的父根编译两个模块,或者先编译annotation-processor模块,然后编译annotation-user模块。

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:

生成的PersonBuilder类可以在annotation-user/target/generated-sources/annotations/com/baeldung/annotation/PersonBuilder.java文件中找到,看起来应该是这样。

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

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

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

8.1.使用注释处理器工具

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.

apt工具是一个特殊的命令行工具,用于处理源文件。它是Java 5的一部分,但从Java 7开始,它就被废弃了,转而使用其他选项,并在Java 8中完全删除。本文将不讨论它。

8.2. Using the Compiler Key

8.2.使用编译器钥匙

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

-processor编译器键是一个标准的JDK设施,可以用你自己的注释处理器来增强编译器的源处理阶段。

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:

请注意,处理器本身和注解必须已经在单独的编译中被编译为类,并存在于classpath上,所以你应该做的第一件事是。

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:

然后你用-processor键指定你刚刚编译的注释处理器类,对你的源进行实际编译。

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:

要一次性指定几个注解处理器,你可以用逗号来分隔它们的类名,像这样。

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

8.3. Using Maven

8.3.使用Maven

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

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

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.

下面是一个为编译器插件添加注释处理器的例子。你也可以使用generatedSourcesDirectory配置参数,指定将生成的源码放入的目录。

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

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

<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

8.4.在Classpath中添加一个处理器Jar

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.

你可以不在编译器选项中指定注释处理器,而只是在编译器的classpath中添加一个带有处理器类的特殊结构的jar。

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:

为了自动获取它,编译器必须知道处理器类的名称。所以你必须在META-INF/services/javax.annotation.processing.Processor文件中指定它作为处理器的全限定类名。

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:

你也可以从这个罐子里指定几个处理器,用新的一行来隔开,让它们自动取走。

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:

如果你用Maven构建这个jar,并试图将这个文件直接放入src/main/resources/META-INF/services目录,你会遇到以下错误。

[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.

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

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

8.5.使用谷歌的自动服务

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

为了自动生成注册文件,你可以使用谷歌@AutoService库中的注释,像这样。

@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.

这个注解本身被来自自动服务库的注解处理器处理。这个处理器会生成META-INF/services/javax.annotation.processing.Processor文件,其中包含BuilderProcessor类名称。

9. Conclusion

9.结论

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.

在这篇文章中,我们用一个为POJO生成Builder类的例子演示了源码级注解处理。我们还提供了几种在你的项目中注册注解处理器的替代方法。

The source code for the article is available on GitHub.

文章的源代码可在GitHub上获得