Android programmers are excited about annotation processors because some of the most popular frameworks in development, such as ButterKnife, EventBus, Dagger, and Ali’s ARouter, use annotation processor technology. Simple annotations, simple API, ultra-high performance and many other advantages, this article will take you from the overall start to discuss the following APT technology is how to play.

What is the APT

APT, the full name of which is Annotation Processing Tool, is an Annotation Processing Tool of JavAC. It detects and finds annotations in source code files and automatically generates codes according to annotations, helping developers to reduce the writing of many repeated codes.

Many popular frameworks use this idea, such as Butterknife, Dragger, Room, and component-based frameworks all use compile-time annotations to automatically generate code and simplify usage.

Common understanding: according to the rules, help us to generate code, generate class files

What is the Element

Element refers to a node or Element, and we often refer to HTML as a structured language because HTML has many formal tag restrictions, each of which is an Element:

<! DOCTYPEhtml>
<html>
<head>
<meta charset="utf-8">
<title>I eyed man</title>
</head>
<body>
    <div>.</div>
</body>
</html>
Copy the code

For Java source files, this is also a structured language. Each part of the source code is a specific type of Element, meaning that Element represents elements in the source file, such as packages, classes, fields, methods, and so on. Java’s Element is an interface, and there are five types of extension classes derived from Element:

package com.example;       // PackageElement PackageElement/node

public class Main<T> {     // TypeElement class element/node; Where 
      
        belongs to the TypeParameterElement generic element/node
      

    private int x;         // VariableElement variable, enumeration, method parameter element/node

    public Main(a) {        // ExecuteableElement constructor, method element/node}}Copy the code
  • PackageElement represents a PackageElement. Provides access to information about packages and their members.
  • TypeElement represents a class or interface element. Provides access to information about types and their members.
  • TypeParameterElement represents a generic element
  • A VariableElement represents a field, enum constant, method or constructor parameter, local variable, or exception parameter
  • ExecuteableElement Represents a method, constructor, or initializer (static or instance) for a class or interface.

Element is an interface. Common apis are as follows:

public interface Element extends AnnotatedConstruct {
    // Get the element type, the actual object type
    TypeMirror asType(a);
    // Get the type of Element and determine which Element it is
    ElementKind getKind(a);
    // Get modifiers, such as public static final keywords
    Set<Modifier> getModifiers(a);
    // Get the name, without the package name
    Name getSimpleName(a);
    // Returns the parent node containing the node, as opposed to the getEnclosedElements() method
    Element getEnclosingElement(a);
    // Returns the child nodes directly contained under this node, such as the class nodes contained under the package node
    List<? extends Element> getEnclosedElements();

    boolean equals(Object var1);

    int hashCode(a);
 
    List<? extends AnnotationMirror> getAnnotationMirrors();

    <A extends Annotation> A getAnnotation(Class<A> var1);

    <R, P> R accept(ElementVisitor<R, P> var1, P var2);`
}
Copy the code

Since source files are structured data, we can get parent or child elements for an Element:

TypeElement person= ... ;  
// Traverse its children
for (Element e : person.getEnclosedElements()){ 
    // Get the nearest parent of the child element
    Element parent = e.getEnclosingElement();  
}
Copy the code

We find that Element sometimes represents multiple elements. For example, TypeElement represents a class or interface. In this case, we can distinguish it by element.getkind () :

Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Who.class);
for (Element element : elements) {
    if (element.getKind() == ElementKind.CLASS) {
        // If the element is a class

    } else if (element.getKind() == ElementKind.INTERFACE) {
        // If the current element is an interface}}Copy the code

The ElementKind enumeration declaration has these:

Enumerated type species
PACKAGE package
ENUM The enumeration
CLASS class
ANNOTATION_TYPE annotations
INTERFACE interface
ENUM_CONSTANT Enumerated constants
FIELD field
PARAMETER parameter
LOCAL_VARIABLE The local variable
EXCEPTION_PARAMETER Exception parameters
METHOD methods
CONSTRUCTOR The constructor
OTHER other

Other elements such as VariableElement have special apis:

  • GetConstantValue () : Gets the value of the initialization variable.

Everything else is relatively simple.

Annotation processor implementation process

Common APT frameworks are used to build three modules, two Java modules, one Android Lib module:

  • Apt-annotation holds annotations
  • Apt-processor houses the custom annotation processor, where Java code compilation and generation rules are declared
  • Apt-api is the API exposed to the user. How do we call the Java code generated by us and need to provide API support

Among them, apt-processor needs to rely on apt-annotation, because relevant annotations in apt-annotation are used to obtain more class information

Next, let’s take a look at the annotation processor implementation process, generally speaking, the following steps are needed:

  1. Annotation handler declaration
  2. Annotation handler registration
  3. Annotation processor file generation
  4. Annotation handler call

Annotation handler declaration

AbstractProcessor each annotation processor inherits from AbstractProcessor and implements the following five methods:

public class MyProcessor extends AbstractProcessor {

    /** * annotates the processor initialization method, equivalent to the Activity's onCreate method. * *@paramProcessingEnvironment This entry can provide several utility classes that can be used when writing code to generate rules in the future
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    /** * Declare the annotation handler to generate Java code rules, write your code to scan, evaluate, and process annotations, and generate Java files here. * *@paramSet supports a collection of annotations for processing. *@paramRoundEnvironment represents the current or previous operating environment and can be used to find node information under the specified annotation. *@returnIf true is returned, the annotations have been processed and the subsequent Processor does not need to process them. If false is returned, these annotations are not processed and may require subsequent processors to process them. * /
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }

    /** * returns a collection of all annotations supported by the current annotation handler. Add the annotations that the current annotation processor needs to process. If the type matches, the process () method is called. * *@return* /
    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        return super.getSupportedAnnotationTypes();
    }

    /** * you need that version of the JDK to compile **@return* /
    @Override
    public SourceVersion getSupportedSourceVersion(a) {
        return super.getSupportedSourceVersion();
    }

    /** * Receives incoming arguments. The most common form is in Gradle 'javaCompileOptions' **@return* /
    @Override
    public Set<String> getSupportedOptions(a) {
        return super.getSupportedOptions(); }}Copy the code

Which getSupportedAnnotationTypes (), getSupportedSourceVersion () and getSupportedOptions () also can use annotations to declare, such as:

@SupportedAnnotationTypes({"com.simple.annotation.MyAnnotation"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedOptions("MODULE_NAME")
public class MyProcessor extends AbstractProcessor {
    
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false; }}Copy the code

MyAnnotation is the annotation supported by our annotation processor, which is a list. As long as the annotation we use in development is in this list, it can be received by the annotation processor, and then obtain more node information through these annotations. MODULE_NAME is an external variable registered in Gradle that the annotation handler can reference when compiling, such as AROUTER_MODULE_NAME registered by ARouter in the defaultConfig closure of build.gradle. Used to generate routing table groups in the future:

javaCompileOptions {
    annotationProcessorOptions {
        arguments = [AROUTER_MODULE_NAME: project.getName()]
    }
}
Copy the code

Annotation handler registration

How do I register an annotation handler with the Java compiler? There are two ways: manual registration and automatic registration.

Manual registration is the old, basic way of packaging all custom annotation handlers into a JAR package and then referring to that JAR to generate the corresponding Java code. Before that you need to declare a particular file javax.mail. The annotation. Processing. The Processor to the meta-inf/services directory, packaged together into the jars.

Meta-inf/Services is an information package. The files and directories in the directory are approved and interpreted by the Java platform to configure applications, extenders, class loaders, and service files, which are generated automatically when the JAR is packaged

Including javax.mail. The annotation. Processing. The contents of the Processor file for each annotation Processor legal full name list, each element line segmentation

com.simple.processor.MyProcessor
com.simple.processor.MyProcessor1
com.simple.processor.MyProcessor2
Copy the code

Automatic registration is easy. You only need to declare a dependency in build-gradle of apt-processor:

implementation 'com. Google. Auto. Services: auto - service: 1.0 rc4'
Copy the code

Add an annotation @autoService (processor.class) to the class name of the corresponding annotation handler:

@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.simple.annotation.MyAnnotation"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedOptions("MODULE_NAME")
public class MyProcessor extends AbstractProcessor {...Copy the code

Annotation processor file generation

This step is the essence of APT technique, writing the generation rules that correspond to the Java code we want. In the beginning we mentioned the concept of Element nodes. The compilation process also filters and parses the node information from the source file. Of course, the generation of Java code also filters the node information and splices it according to the rules.

Remember the init method we used to declare our custom annotator? Here we typically instantiate several tools:

    /** * node utility class (classes, functions, attributes are nodes) */
    private Elements mElementUtils;

    /** * information tools */
    private Types mTypeUtils;

    /** * File generator */
    private Filer mFiler;

    /** * Log information printer */
    private Messager mMessager;

    private String mModuleName;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mElementUtils = processingEnv.getElementUtils();
        mTypeUtils = processingEnv.getTypeUtils();
        mFiler = processingEnv.getFiler();
        mMessager = processingEnv.getMessager();

        mModuleName = processingEnv.getOptions().get("MODULE_NAME");
    }
Copy the code

Among them:

  • MElementUtils node utility class, which obtains the specified node information. From the node information, you can further obtain the type and name of the node.
  • MTypeUtils Class Information utility class, often used for type determination
  • MFiler file generator that generates the specified file
  • MMessager Log tool class, used to print log information

Now you can use these tools in the process method. Annotation processor file generation rules are mainly implemented in the Process method. There are also two ways to implement file generation rules:

  1. The normal way of writing files.
  2. Use javapoet framework to write.

For example, you want to generate the following file:

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, APT!"); }}Copy the code

The conventional way of writing a file is to use writer. write, which is relatively rigid and requires every letter to be written. Sometimes, even the import code of the guide package needs to be superimposed verbatim.

StringBuilder builder = new StringBuilder()
                .append("package com.example.helloworld; \n\n")
                .append("public final class HelloWorld{\n")
                .append("\tpublic static void main(String[] args) {\n")
                .append("\t\tSystem.out.println(\"Hello, APT! \ "); \n")
                .append("\t}\n")
                .append("}");

        Writer writer = null;
        try {
            JavaFileObject source = mFiler.createSourceFile("com.example.helloworld");
            writer = source.openWriter();
            writer.write(builder.toString());
        } catch (IOException e) {
            throw new RuntimeException("APT process error", e);
        } finally {
            if(writer ! =null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    //Silent}}}Copy the code

EventBus is known for doing this, even with the latest version 3.0.

Using the Square team’s Javapoet framework to write files is more like writing code in an editor. Common JavaPoet classes are as follows:

  • TypeSpec———— a class that generates classes, interfaces, and enumerates objects
  • MethodSpec———— The class used to generate method objects
  • ParameterSpec———— The class used to generate parameter objects
  • AnnotationSpec———— The class used to generate annotation objects
  • FieldSpec———— is used to configure the classes that generate member variables
  • ClassName———— An object generated by the package name and Class name, which is equivalent to specifying a Class for it in JavaPoet
  • ParameterizedTypeName———— generates classes containing generics using MainClass and IncludeClass
  • JavaFile———— A class that controls the output of generated Java files

JavaPoet generation rules are as follows:

// Build the main function
MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC) // Specify the method modifier
    .returns(void.class) // Specify the return type
    .addParameter(String[].class, "args") // Add parameters
    .addStatement("$T.out.println($S)", System.class, "Hello, APT!") // Build the method body
    .build();

/ / build
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL) // Specify class modifiers
    .addMethod(main) // Add method
    .build();

// Specify the package path to build the body of the file
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

// Create a file
javaFile.writeTo(System.out);
Copy the code

The JavaPoet usage documentation is more detailed. There is not much description here. You can check the official documentation :github.com/square/java… And, of course, some netizens specially compiled some Chinese tutorial: blog.csdn.net/l540675759/…

Whichever way we write our Java code generation rules, I recommend writing a named Java code template first, and then writing from that template.

In addition, one thing we need to do before writing Java code generation rules is to retrieve and parse annotations supported by the annotation processor, obtain the necessary node information, and then combine this information into our generation rules so that our annotations make sense:

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    if (set == null || set.isEmpty()) {
        return false;
    }

    Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(MyAnnotation.class);
    StringBuilder annotations = new StringBuilder();
    if(rootElements ! =null && !rootElements.isEmpty()) {
        for (Element element : rootElements) {
            annotations.append(element.getSimpleName() + ",");
        }
    }

    String s = annotations.toString();
    mMessager.printMessage(Diagnostic.Kind.NOTE, "All annotated class information:" + s);

    // Build the main function
    MethodSpec main = MethodSpec.methodBuilder("main")
            .addModifiers(Modifier.PUBLIC, Modifier.STATIC) // Specify the method modifier
            .returns(void.class) // Specify the return type
            .addParameter(String[].class, "args") // Add parameters
            .addStatement("$T.out.println($S)", System.class, s) // Build the method body
            .build();

    / / build
    TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL) // Specify class modifiers
            .addMethod(main) // Add method
            .build();

    // Specify the package path to build the body of the file
    JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld).build();
    try {
        // Create a file
        javaFile.writeTo(mFiler);
    } catch (IOException e) {
        e.printStackTrace();
    }

    return true;
}
Copy the code

Annotation handler call

We will create a new Android module called apt-API. First of all, when we write the code, these files are not compiled yet, so we can’t use them directly. Second, APT is a dynamic framework, that is to say, the development does not need to care about what the hell to generate, as long as I tell how to use it, generating thousands of files are internal details, the development does not need to worry about.

For example, if we want to instantiate HelloWorld above, we can write the corresponding API as follows:

public class MyAptApi {

    public static void init(a) {
        try{ Class<? > c = Class.forName("com.example.helloworld.HelloWorld");
            c.newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch(InstantiationException e) { e.printStackTrace(); }}}Copy the code

All you need to do is focus on MyAptApi’s init() method and call it where appropriate:

MyAptApi.init();
Copy the code

Typically we want to use the Java class files we generate, but more often we implement the class files into some off-the-shelf interface that can be used by interface proxies.

The last

Annotation processor APT technology is widely used, some frameworks applied it looks “mystery”, but always have a routine, this article only from APT interpretation on the overall structure of the basic course of using, the master brought in by personal practice, the deeper still hope everyone for further mining, had better be to go through some framework principle deep learning, Examples include ButterKnife and EventBus.

Two amway articles that have been well summarized by others:

  • www.jianshu.com/p/857aea5b5…
  • Blog.csdn.net/dirksmaller…