java之编译期注解处理(lombook)

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

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

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

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

5. 定义注解

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

   
    public class Person {  

        private int age;  

        private String name;  

        // getters and setters …  

    }  

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

    
    Person person = new PersonBuilder()  
      .setAge(25)  
      .setName("John")  
      .build();  

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

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

   
    @Target(ElementType.METHOD)  
    @Retention(RetentionPolicy.SOURCE)  
    public @interface BuilderProperty {  
    }  

带有_ElementType.METHOD_参数的_@Target_注解确保此注解只能放在方法上。

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

具有使用_@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. 实现 Processor

6.1 创建AbstractProcessor子类

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

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

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

    
    @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 方法实现为迭代循环:

   
    @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_类,这些元素对应于_setName_和_setAge_方法。

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

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

  
    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_实例。以下行将在源处理阶段为每个错误注解的元素输出错误:

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

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

    
    if (setters.isEmpty()) {  
        continue;  
    }  

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

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

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

    
    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_实例:

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

下面提供了_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. 运行示例

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

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

    
    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设施,以增加编译器的源处理阶段,自己的注解处理器。

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

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

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

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

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

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

8.3 使用Maven

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

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

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

        </plugins>  
    </build>  

8.4 将处理器Jar添加到Classpath

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

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

    
    com.doleje.annotation.processor.BuilderProcessor  

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

   package1.Processor1``package2.Processor2``package3.Processor 

如果您使用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.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_注解,如下所示:

    
    @AutoService(Processor.class)  
    public BuilderProcessor extends AbstractProcessor {  
        // …  
    }  
   

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

9. 结论

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