×
Community Blog Java Annotation Processing Tool - AbstractProcessor

Java Annotation Processing Tool - AbstractProcessor

This article provides a detailed description of how Java annotation processors work and how they automatically generate code.

By Renchuang Shi (Shizhen)

1

Overview

The common annotations are divided into the following two categories:

  1. Annotations at Runtime: An annotation processor dynamically processes annotations at runtime using Reflection.
  2. Annotations at Compile Time: An annotation processor dynamically processes annotations at compile time.

Most of the annotations we usually use are annotations at runtime, such as @Autowire, @Resoure, and @Bean.

The compile-time annotations we usually use include @Lombok and @AutoService.

These compile-time annotations are used to automatically generate code, thus improving coding efficiency and avoiding heavy use of Reflection at runtime by using Reflection at compile time to generate auxiliary classes and methods for use at runtime.

How do these compile-time annotations work? How do they automatically generate code?

Annotation Processor

Annotation Processing Flow

One of the most critical classes in the annotation processing flow at compile time is the Processor, which is the interface of the annotation processor. All the logic we need to process annotations at compile time needs to implement this Processor interface. AbstractProcessor helps us write most of the processes, so we only need to implement this abstract class to define an annotation processor.

2

The annotation processing process requires multiple rounds to complete. Each round starts with the compiler searching for annotations in the source file and selecting the appropriate annotation processor (AbstractProcessor) for those annotations. Each annotation processor is called on the corresponding source in turn.

If any files are generated during this process, another round will begin with the generated files as input. This process continues until no new files are generated during the processing phase.

The Processing Steps of Annotation Processor

  • Build an annotation processor in the Java compiler
  • The compiler starts executing the unexecuted annotation processor.
  • Loop through the annotation element to find the class, method, or attribute that was modified by the annotation.
  • Generate the corresponding class and write it to the file
  • Determine whether all annotation processors have completed execution, and if not, continue with the annotation processing with the next annotation processor (return to step 1).

AbstractProcessor

This is the core abstract class of the annotation processor. Let's focus on the method.

getSupportedOptions()

The default implementation is to get the value from the annotation, SupportedOptions, and the value is a character array, such as:

@SupportedOptions({"name","age"})
public class SzzTestProcessor extends AbstractProcessor {
}

However, it seems that the interface is useless.

Some data indicate that this optional parameter can be obtained from processingEnv.

String resultPath = processingEnv.getOptions().get(parameter);

The obtained parameter is set through the input parameter -Akey=name at compile time, and it has nothing to do with the getSupportedOptions() method.

getSupportedAnnotationTypes

It is used to get the annotation types that the current annotation processing class supports. The default implementation is obtained from the SupportedAnnotationTypes annotation.

The annotation value is a string array, String [].

The matching annotations are passed through the process() method of the annotation processing class.

For example, the following uses *, a wildcard character, to support all annotations:

@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class PrintingProcessor extends AbstractProcessor {

}

Or you can rewrite this interface directly:

@Override
  public ImmutableSet<String> getSupportedAnnotationTypes() {
    return ImmutableSet.of(AutoService.class.getName());
  }

In the end, they are used for filtering because all annotations will be obtained during processing. Then, the annotations they can process will be obtained according to this configuration.

getSupportedSourceVersion

It is used to get the latest version that the annotation processor can support. The default implementation is obtained from the SupportedSourceVersion annotation, or you can rewrite the method by yourself. If there is no such version, the default value is RELEASE_6.

@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class PrintingProcessor extends AbstractProcessor {

}

Or you can rewrite the method to get the latest supported version (recommended).

@Override
  public SourceVersion getSupportedSourceVersion() {
   // Specify the latest version that is supported.
    return SourceVersion.latestSupported();
  }

init Initialization

init is the initialization method that passes in the ProcessingEnvironment object. In general, we don't need to rewrite it, just use abstract classes.

You can also rewrite it according to your needs.

@Override
    public synchronized void init(ProcessingEnvironment pe) {
        super.init(pe);
        System.out.println("SzzTestProcessor.init.....");
        // You can get the compiler parameters (the following two are the same).
        System.out.println(processingEnv.getOptions());
        System.out.println(pe.getOptions());

    }

You can obtain a lot of information. For example, you can obtain custom parameters of the compiler. Please see How to set input parameters for compilers below for more information about how to set custom parameters.

Some parameter descriptions:

Table1

Process() method

The process() method provides two parameters. The first is the collection of types of annotations we request to be processed, which is the annotation types we specify by rewriting the getSupportedAnnotationTypes() method. The second is the context for information about the current and previous loops.

The return value indicates whether these annotations are declared by this processor. If true is returned, these annotations are not processed by subsequent processors. If false is returned, these annotations can be processed by subsequent processors.

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("SzzTestProcessor.process.....;");

        return false;
    }

We can get annotation elements through the RoundEnvironment interface. Note: Annotations are only annotation types, and we do not know which instances are annotated. However, we can know which instances are annotated through RoundEnvironment.

Table2

Please see the Example of Custom Annotation Processor below for more information.

How to Register an Annotation Processor

The section above describes some core methods of the annotation processor, but how do we register the annotation processor?

It doesn't mean that after the AbstractProcessor class is implemented, the annotation processor will take effect. Since the annotation processor (AbstractProcessor) executes tasks at compile time and takes effect as a Jar package file, we need to package the AbstractProcessor as a separate module.

Then, reference the AbstractProcessor in the module that needs to use it.

When you package the module where the AbstractProcessor is located, please note the following:

AbstractProcessor essentially loads the SPI through the ServiceLoader, so there are two ways for AbstractProcessor to be registered.

1. Configure SPI

Step 1: Create a file called javax.annotation.processing.Processor under the resource/META-INF.services folder. The content inside the file is the fully-qualified class name of your annotation processor.

3

Step 2: Set the Processor to be disabled at compile-time. The reason is that if you do not disable the Processor, ServiceLoader will load the annotation processor you just set. However, since the Class file was not successfully loaded at compile time, the following exception will be thrown.

The service configuration file is incorrect, or when you are constructing the processor object, javax.annotation.processing.Processor, the exception error: Provider org.example.SzzTestProcessor not found is thrown.

If Maven is used for compilation, add the following configuration: <compilerArgument>-proc:none</compilerArgument>

          <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>
                </configuration>
                <executions>
                    <execution>
                        <id>default-compile</id>
                        <configuration>
                            <compilerArgument>-proc:none</compilerArgument>
                        </configuration>
                    </execution>
                    <execution>
                        <id>compile-project</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

Step 3: If the annotation processor is packaged, it can be provided to other modules for use.

2. Use @AutoService to Automatically Configure the SPI Configuration File

@AutoService is a small open-source plug-in from Google. It can automatically help you generate META-INF/services files, so you don't need to manually create configuration files.

In the section above <compilerArgument>-proc:none</compilerArgument> arguments are not needed.

Therefore, there will be no errors mentioned above, such as xxx not found at compile time. Since META-INF/services have not configured your annotation processor at compile time, no load exception will be thrown.

For example, if you use @AutoService(Processor.class), it will automatically generate the corresponding configuration file for you.

@AutoService(Processor.class)
public class SzzBuildProcessor extends AbstractProcessor {

}

4

In addition, the automatic generation of configuration files by @AutoService is implemented through AbstractProcessor.

How to Debug Compile-Time Code

We might want to debug our annotation processors, but the debugging at compile time is different from that at runtime.

Maven-Related Configurations Used to Specify the Effective Processor

If you use Maven to compile, there are some parameters you need to specify.

For example, you can specify the parameter and the source path where code is generated. The default value of the source path is target/generated-sources/annotations.

You do not need to set these parameters unless there are special circumstances.

<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>
                <!-- Set the path of generated source folders. The default path is as follows. Generally, you don't need to set this parameter unless you have your own special needs  -->
                <generatedSourcesDirectory>${project.build.directory} /generated-sources/</generatedSourcesDirectory>
                 <!-- Specifies the effective annotation processors. After this parameter is specified, only the configured annotation processors below will take effect. In general, it is unnecessary to specify this parameter, so you can delete all of the following -->
                <annotationProcessors>
                    <annotationProcessor>
                            org.example.SzzTestProcessor
                    </annotationProcessor>
                </annotationProcessors>
            </configuration>
        </plugin>

    </plugins>
</build>

Notes

Annotation and annotation processor are separate modules. Annotation processors are only used at compile time, and the annotation module only needs to introduce the Jar package file of the annotation processor. So, we need to separate the annotation processor into separate modules.

When packaging, please package the module of the annotation processor first.

The custom processor class is eventually called during compilation by packaging it into a Jar file.

5

Example of Custom Annotation Processor

Example 1: Automatically Generate a Builder Constructor

1. Demand Description

Let's suppose we have some simple POJO classes in our annotation user module that contain several fields:

public class Company {

    private String name;

    private String email ;

}

public class Personal {

    private String name;

    private String age;
}

We want to create a corresponding builder helper class to instantiate the POJO class more smoothly.

        Company company = new CompanyBuilder()
                .setName("ali").build();
        Personal personal = new PersonalBuilder()
                .setName("szz").build();

2. Demand Analysis

If there is no POJO class, it is too complicated to manually create the corresponding builder. We can automatically generate the corresponding builder for POJO classes in the form of annotations, but the builder is not generated for every POJO class, and it is generated on demand.

Step 1: Define a @BuildProperty annotation and annotate the method that needs to generate the corresponding set() method

Step 2: Customize the annotation processor to scan the @BuildProperty annotation and automatically generate a builder as required

For example, the code to generate the CompanyBuilder is listed below:

public class CompanyBuilder {

    private Company object = new Company();

    public Company build() {
        return object;
    }

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

}

3. Coding

Create an annotation processor module: szz-test-processor-handler

@BuildProperty

@Target(ElementType.METHOD) // Use the annotation on the method.
@Retention(RetentionPolicy.SOURCE) // It is available only during Source processing but unavailable at runtime.
public @interface BuildProperty {
}

Annotation Processor

@SupportedAnnotationTypes("org.example.BuildProperty") // Only process the annotation with the type of BuildProperty;
public class SzzBuildProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("SzzBuildProcessor.process ;");

        for (TypeElement annotation : annotations) {
            // Obtain all instances annotated by the annotation.
            Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);

            // Check whether the annotation starts with set and only one argument is used as required.
            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);

            // Print the case where the annotation was used incorrectly.
            otherMethods.forEach(element ->
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                            "@BuilderProperty must be applied to a setXxx method with a single argument", element));

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


            Map<String ,List<Element>> groupMap = new HashMap();

            // Group by fully-qualified class name. A builder is created for each class.
            setters.forEach(setter ->{
                // Fully-qualified class name
                String className = ((TypeElement) setter
                        .getEnclosingElement()).getQualifiedName().toString();
                List<Element> elements = groupMap.get(className);
                if(elements != null){
                    elements.add(setter);
                }else {
                    List<Element> newElements = new ArrayList<>();
                    newElements.add(setter);
                    groupMap.put(className,newElements);
                }
            });


            groupMap.forEach((groupSetterKey,groupSettervalue)->{
                // Obtain the class name SimpleName and the input parameters of the set() method.
                Map<String, String> setterMap = groupSettervalue.stream().collect(Collectors.toMap(
                        setter -> setter.getSimpleName().toString(),
                        setter -> ((ExecutableType) setter.asType())
                                .getParameterTypes().get(0).toString()
                ));
                try {
                    // Assemble the XXXBuild class and create the corresponding class file.
                    writeBuilderFile(groupSetterKey,setterMap);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }

            });
        }

        // Returning false indicates that other processors can continue to process the annotation after the current processor has processed it, and returning true indicates that other processors will no longer process the annotation after the current processor has processed it.
        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.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("}");
        }
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        System.out.println("----------");

        System.out.println(processingEnv.getOptions());

    }
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }


}

4. Register the Annotation Processor

6

5. Configure Compilation Parameters

Since the META-INF.services is manually configured here, we need to set the Processor to be disabled at compile time. Otherwise, it will be loaded by ServiceLoader, and the class not found exception will be thrown during compilation. The main parameters are listed below:

<compilerArgument>-proc:none</compilerArgument>

And the code is as follows:

<build>
        <pluginManagement>
            <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>
                    </configuration>
                    <executions>
                        <execution>
                            <id>default-compile</id>
                            <configuration>
                                <compilerArgument>-proc:none</compilerArgument>
                            </configuration>
                        </execution>
                        <execution>
                            <id>compile-project</id>
                            <phase>compile</phase>
                            <goals>
                                <goal>compile</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>

             </plugins>

        </pluginManagement>

    </build>

6. Perform Compilation and Packaging

Run the mvn install command, and other modules can be referenced.

7. The Demo Module Depends on the Annotation Processor

Create a new module (szz-test-demo) and make it depend on the szz-test-processor-handler above

Use annotations on some of the Company's methods:

7

8. Compiling the Demo Module Will Automatically Generate the BuildCompany Class

Once the Demo Module is compiled, the BuildXXX class will be generated in the target folder. Only the methods annotated with the @BuildProperty annotation will generate the corresponding methods.

If the @BuildProperty annotation is used in the wrong way, an exception will be printed out.

8

How to Set Input Parameters at Compile Time

We can get some custom parameters of the compiler in the interface of init initialization.

    String verify = processingEnv.getOptions().get("custom key");

Note: This obtained compiler parameter is the key starting with -A because it is obtained after filtering.

9

How can we set this custom parameter?

If you use IDEA for compilation:

-Akey=value or -Akey

10

If you use Maven for compilation:

11

1 2 1
Share on

Alibaba Cloud Community

887 posts | 199 followers

You may also like

Comments

Dikky Ryan Pratama July 13, 2023 at 1:33 am

Awesome!