Pluggable Annotation Processing API

Keywords: Java Maven Lombok encoding

Java Magic - Pluggable Annotation Processing API

Reference material

brief introduction

Pluggable Annotation Processing API JSR 269 Provide a standard API to handle Annotations JSR 175 In fact, JSR 269 is not only used to deal with Annotation. I think the more powerful function of JSR 269 is that it builds a model of Java language itself. It maps methods, packages, constructor s, type s, variable s, enum s, annotations and other Java language elements into Types and Elements, thus mapping the semantics of Java language into objects, which can be seen under the javax.lang.model package. These classes. So we can use the API provided by JSR 269 to build a rich metaprogramming environment. JSR 269 uses Annotation Processor to process Annotation during compilation rather than during runtime. Annotation Processor is equivalent to a plug-in of the compiler, so it is called plug-in annotation processing. If Annotation Processor generates new Java code when it processes Annotation (executing process method), the compiler will call Annotation Processor again, and if the second process has a new generation. When the code is generated, Annotation Processor is then called until no new code is generated. Each execution of the process() method is called a "round" so that the entire Annotation processing process can be seen as a sequence of rounds. JSR 269 is mainly designed as an API for Tools or containers. Although this feature already exists in Java SE 6, few people know it exists. The next Java magic, lombok, uses this feature to implement compile-time code insertion. In addition, if there is no guess error, the red underscores that mark grammatical errors when IDEA writes code are also implemented through this feature. KAPT(Annotation Processing for Kotlin), or Kotlin's compilation, is also through this feature.

The core of Pluggable Annotation Processing API is Annotation Processor (Annotation Processor), which generally needs to inherit the abstract class javax.annotation.processing.AbstractProcessor. Note that unlike the runtime annotation RetentionPolicy.RUNTIME, the annotation processor only handles compile-time annotations, that is, the annotation type of RetentionPolicy.SOURCE, which is processed during Java code compilation.

Using steps

The usage steps of plug-in annotation processing API are as follows:

  • 1. To customize an Annotation Processor, you need to inherit javax.annotation.processing.AbstractProcessor and override the process method.
  • 2. Customize a comment. The Meta-comment of the comment needs to specify @Retention(RetentionPolicy.SOURCE).
  • 3. You need to use javax. annotation. processing. Supported Annotation Types in the declared custom Annotation Processor to specify the name of the annotation type created in step 2 (note that you need the full class name, "package name. annotation type name", otherwise it will not take effect).
  • 4. You need to specify the compiled version using javax.annotation.processing.SupportedSourceVersion in the declared custom Annotation Processor.
  • 5. Optional operations to specify compilation parameters using javax.annotation.processing.SupportedOptions in the declared custom Annotation Processor.

Practical example

Basics

Let's mimic the @Test annotation in Junit, the testing framework. Through Annotation at runtime Processor gets information about methods that use custom @Test annotations . Because if you want to modify the code content of a class or method dynamically, you need to use bytecode modification tools such as ASM. These operations are too deep and we will talk about them later. First, define a comment:

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

Define an annotation processor:

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

Write a main class:

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{

    }
}

Next, you need to specify Processor. If you use IDEA, Enable annotation processing in Compiler - > Annotation Processors must be checked. You can then specify the Processor in the following ways.

  • 1. Use compilation parameters directly, such as javac-processor club. throwable. processor. Annotation Processor Main. java.
  • 2. To specify through service registration is to add club. throwable. processor. Annotation Processor to the META-INF/services/javax.annotation.processing.Processor file.
  • 3. The configuration of the compiler plug-in through Maven is specified as follows:
    xml <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>

It is worth noting that the precondition for the above three points to take effect is that club. throwable. processor. Annotation Processor has been compiled, otherwise the compilation will be wrong:

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

There are two solutions, the first is to compile it in advance using commands or IDEA right-click club. throwable. processor. Annotation Processor; the second is to introduce club. throwable. processor. Annotation Processor into a separate Jar package. I use the first method here.

Finally, compile using the Maven command mvn compile. The output is as follows:

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]

So Annotation Processor takes effect during compilation.

Advanced

The following is an example of directly modifying the code of the class to generate a Builder class for the corresponding properties of the Setter method of the entity class, that is, the original class is as follows:

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

The generated Builder class is as follows:

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

Custom comments are as follows:

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

}

The custom annotation processor is as follows:

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("}");
        }
    }
}

The main categories are as follows:

public class Main {

    public static void main(String[] args) throws Exception{
      //PersonBuilder is generated after compilation, which requires compilation before it can be written.
      Person person  = new PersonBuilder().setAge(25).setName("doge").build();
    }
}

Builder Processor is manually compiled first, then club. throwable. processor. builder. Builder Processor is added to META-INF/services/javax.annotation.processing.Processor file, and finally compiled by executing the Maven command mvn compile.

Compiled console output:

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

After successful compilation, a new PersonBuilder class is added to the club.throwable.processor.builder subpackage path under the target/classes package:

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

This class is added at compile time. In this case, new classes added at compile time seem to have little effect. However, it would be useful to add new methods to the original entity class, as lombok did. Because some classes or methods are added at compile time, direct use in the code will be marked red. Therefore, lombok provides plug-ins for IDEA or eclipse, and the implementation of plug-in functions is estimated to use plug-in annotations to process API s.

Summary

When I learned about the Pluggable Annotation Processing API, almost all of the search engine searches for Android development through plug-in annotations to process API compile-time dynamic Add-On code and so on, so it can be seen that the use of this function is relatively extensive. Perhaps the actual examples in this article do not reflect the power of the Pluggable Annotation Processing API, so there is time to write some code generation plug-ins based on this function, such as lombok, which will be introduced in the next article.

(End of this article)

Posted by Dave2711 on Tue, 11 Dec 2018 17:06:06 -0800