This is the 8th day of my participation in the More text Challenge. For details, see more text Challenge

preface

Nice to meet you

In the last article in this series, we talked about annotations. If you haven’t seen the last article, you are advised to read the APT series for Android (part 2) : Annotations for APT Building Foundation. So far, we have finished the basic part of Apt, and then we will formally enter the study of Apt technology

Github Demo address, you can see the Demo to follow my ideas together analysis

Introduction to APT

1) What is APT?

APT is the full name Annotation Processing Tool. APT is a tool for handling comments. It detects source code files for comments and uses them for additional processing.

2) What is APT for?

APT can be annotated at compile time, giving us automatically generated code to simplify usage. APT technology is used by many popular frameworks such as ButterKnife, Retrofit, Arouter, EventBus, etc

2. APT Project

1) APT project creation

In general, APT has a rough implementation process:

Create a Java Module for writing annotations

Create a Java Module that reads the annotation information and generates the corresponding class file according to the specified rules

3. Create an Android Module, obtain the generated class through reflection, encapsulate it reasonably, and provide it to the upper layer for invocation

The diagram below:

This is my APT project, about the Module name can be arbitrary, according to the rules I said above to proceed

2) Module dependencies

Once the project is created, we need to clarify a dependency between each Module:

1. Because apt-processor needs to read the annotations of apT-annotation, apt-processor needs to rely on apt-annotation

//apt-processor build.gradle file
dependencies {
    implementation project(path: ':apt-annotation')
}
Copy the code

2. As the calling layer, app needs to rely on the above three modules

// App build.gradle file
dependencies {
    / /...
    implementation project(path: ':apt-api')
    implementation project(path: ':apt-annotation')
    annotationProcessor project(path: ':apt-processor')
}
Copy the code

APT project configuration is good, we can each Module for a specific code to write

3. Compiling apt-annotation

This Module is relatively easy to handle, just write the corresponding custom annotations, I wrote the following:

@Inherited
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface AptAnnotation {
    String desc(a) default "";
}
Copy the code

4. Apt-processor automatically generates code

This Module is relatively complex, and we divide it into the following three steps:

1. Annotate processor declarations

2. Annotate processor registration

3. The annotation processor generates class files

1) Annotate processor declarations

1, create a new class, the name of the class according to their preferences, inheritancejavax.annotation.processingThe AbstractProcessor class under this package and implements its abstract methods

public class AptAnnotationProcessor extends AbstractProcessor {
  
    /** * Write the logic for generating Java classes **@paramSet supports the collection of annotations * to be processed@paramRoundEnvironment uses this object to find node information * under the specified annotation@returnTrue: indicates that the annotations are processed and no further processing is required by the annotation handler. False: indicates that the annotation has not been processed and may require a subsequent annotation handler to process */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false; }}Copy the code

The first argument is called TypeElement. The first argument is called TypeElement. The first argument is called TypeElement.

Element is introduced

In fact, Java source files are a structured language, where each part of the source code corresponds to a specific type of Element, such as package, class, field, method, and so on:

package com.dream;         // PackageElement: PackageElement

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

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

    public Main(a) {        // ExecuteableElement: constructor, method element}}Copy the code

Element in Java is an interface with the following source code:

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

    @Override
    boolean equals(Object obj);
  
    @Override
    int hashCode(a);
  
    @Override
    List<? extends AnnotationMirror> getAnnotationMirrors();
  
    // Get the annotation
    @Override
    <A extends Annotation> A getAnnotation(Class<A> annotationType);
  
    <R, P> R accept(ElementVisitor<R, P> v, P p);
}
Copy the code

We can get some of this information from the Element (annotated ones are the usual ones)

There are five extension classes derived from Element:

1. PackageElement represents a PackageElement

TypeElement represents a class or interface element

TypeParameterElement represents a generic element

4. VariableElement represents a field, enum constant, method or constructor parameter, local variable, or exception parameter

5. ExecuteableElement represents a method, constructor, or initializer (static or instance) for a class or interface

As you can see, an Element sometimes represents more than one Element. For example, TypeElement represents a class or an interface, which we can distinguish by element.getkind () :

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

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

ElementKind is an enumeration class that can take many values, such as:

PACKAGE	/ / package
ENUM // Represents enumeration
CLASS / / class
ANNOTATION_TYPE	// Indicates annotations
INTERFACE // Indicates the interface
ENUM_CONSTANT // Represents an enumeration constant
FIELD // Indicates the field
PARAMETER // Indicates the parameter
LOCAL_VARIABLE // Represents a local variable
EXCEPTION_PARAMETER // Indicates the exception parameter
METHOD // Indicate the method
CONSTRUCTOR // Represents the constructor
OTHER // Other
Copy the code

With that said, let’s move on to Element

2. Override method interpretation

In addition to the abstract method we have to implement, there are four other commonly used methods we can override, as follows:

public class AptAnnotationProcessor extends AbstractProcessor {
    / /...
  
    /** * Node utility class (class, function, property are nodes) */
    private Elements mElementUtils;

    /** * information tool class */
    private Types mTypeUtils;

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

    /** * Log message printer */
    private Messager mMessager;
  
    /** do some initialization work **@paramThe processingEnvironment parameter provides several utility classes for writing */ to use when generating Java classes
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mElementUtils = processingEnv.getElementUtils();
        mTypeUtils = processingEnv.getTypeUtils();
        mFiler = processingEnv.getFiler();
        mMessager = processingEnv.getMessager();
    }
  
    /** * accept incoming parameters, most commonly in the form of javaCompileOptions configuration in the build.gradle script file **@returnProperty */
    @Override
    public Set<String> getSupportedOptions(a) {
        return super.getSupportedOptions();
    }

    /** * Specifies the set of annotations supported by the current annotation handler. If so, the process method ** is called@returnSupported collection of annotations */
    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        return super.getSupportedAnnotationTypes();
    }
		
    /** * compile the JDK version of the current annotation processor@return* / JDK version
    @Override
    public SourceVersion getSupportedSourceVersion(a) {
        return super.getSupportedSourceVersion(); }}Copy the code

Note: getSupportedAnnotationTypes (), getSupportedSourceVersion getSupportedOptions and () () the three methods, we can also use annotations in a way that provides:

@SupportedOptions("MODULE_NAME")
@SupportedAnnotationTypes("com.dream.apt_annotation.AptAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AptAnnotationProcessor extends AbstractProcessor {
    / /...
}
Copy the code

2) Annotate processor registration

The annotation handler declaration is ready. The next step is to register it. There are two ways to register it:

1. Manual registration

2. Automatic registration

Manual registration is cumbersome and fixed and error prone, not recommended to use, here will not talk about. Let’s focus on automatic enrollment

Automatic registration

1. First, import the following dependencies into the build.gradle file under the apt-processor Module:

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

Note: these two sentences must be added, otherwise the registration will not succeed, I stepped on the pit before

2. Add @AutoService(processor.class) to the annotation Processor to complete registration

@AutoService(Processor.class)
public class AptAnnotationProcessor extends AbstractProcessor {
    / /...
}
Copy the code

3) annotate the handler to generate class files

After registration, we can formally write the code to generate the Java class files, which can also be generated in two ways:

1, the normal way to write files

2, through the Javapoet framework to write

1 the way is more rigid, need to write every letter, not recommended to use, here will not speak. We’ll focus on generating Java class files through the Javapoet framework

Javapoet way

This way is more in line with a style of object-oriented coding, for those who are not familiar with Javapoet, you can go to Github to learn a wave of portal, here we introduce some of its common classes:

TypeSpec: Class that generates classes, interfaces, and enumerations

MethodSpec: The class used to generate method objects

ParameterSpec: specifies the class used to generate parameter objects

AnnotationSpec: Class used to generate annotation objects

FieldSpec: Used to configure the class that generates member variables

ClassName: An object generated by the package name and Class name, which in JavaPoet is equivalent to specifying a Class for it

ParameterizedTypeName: generates a Class containing generic types using MainClass and IncludeClass

JavaFile: Class that controls the output of the generated Java file

1. Import javapoet framework dependencies
implementation 'com. Squareup: javapoet: 1.13.0'
Copy the code
2. Generate Java class files according to the specified code template

For example, I made the following configuration in app build.gradle:

android {
    / /...
    defaultConfig {
        / /...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [MODULE_NAME: project.getName()]
            }
        }
    }
}
Copy the code

The following comment is made under MainActivity:

The code I want to generate is as follows:

Now let’s put this into practice:

@AutoService(Processor.class)
@SupportedOptions("MODULE_NAME")
@SupportedAnnotationTypes("com.dream.apt_annotation.AptAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AptAnnotationProcessor extends AbstractProcessor {
		
    // File generator
    Filer filer;
    / / module name
    private String mModuleName;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
      	// Initialize the file generator
        filer = processingEnvironment.getFiler();
      	// Obtain the corresponding value in build.gradle from key
        mModuleName = processingEnv.getOptions().get("MODULE_NAME");
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set == null || set.isEmpty()) {
            return false;
        }
				
      	// Get the node information under the current annotation
        Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(AptAnnotation.class);

        // Build the test function
        MethodSpec.Builder builder = MethodSpec.methodBuilder("test")
                .addModifiers(Modifier.PUBLIC) // Specify method modifiers
                .returns(void.class) // Specify the return type
                .addParameter(String.class, "param"); // Add parameters
        builder.addStatement("$T.out.println($S)", System.class, "Module:" + mModuleName);

        if(rootElements ! =null && !rootElements.isEmpty()) {
            for (Element element : rootElements) {
              	// Name of the current node
                String elementName = element.getSimpleName().toString();
              	// Attributes annotated under the current node
                String desc = element.getAnnotation(AptAnnotation.class).desc();
                // Build the body of the method
                builder.addStatement("$T.out.println($S)", System.class, 
                                     "Node:" + elementName + "" + "Description:" + desc);
            }
        }
        MethodSpec main =builder.build();

        // Build the HelloWorld class
        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC) // Specify the class modifier
                .addMethod(main) // Add the method
                .build();

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

        return true; }}Copy the code

After these steps, we can run the App and generate the code in the screenshot above. Now we need the final step, which is to use the generated code

Note: Gradle version 6.7.1 generates class files in different locations. My Gradle version 6.7.1 generates class files in the following locations:

Some earlier versions of Gradle generate class files in the /build/generated/source directory

5. Apt-api call generates code to complete business functions

The operation of this Module is relatively simple, that is, through reflection to obtain the generated class, the corresponding encapsulation can be used, I write as follows:

public class MyAptApi {

    @SuppressWarnings("all")
    public static void init(a) {
        try {
            Class c = Class.forName("com.dream.aptdemo.HelloWorld");
            Constructor declaredConstructor = c.getDeclaredConstructor();
            Object o = declaredConstructor.newInstance();
            Method test = c.getDeclaredMethod("test", String.class);
            test.invoke(o, "");
        } catch(Exception e) { e.printStackTrace(); }}}Copy the code

Next, we call it in the onCreate method of MainActivity:

Notation (@aptanNotation = "I am the notation on MainActivity ")
public class MainActivity extends AppCompatActivity {
  
    @aptannotation (desc = "I am the notation above onCreate ")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MyAptApi.init(); }}// Print the resultModule: APP node: MainActivity Description: I am the annotation node above MainActivity: onCreate Description: I am the annotation above onCreateCopy the code

Six, summarized

Some of the highlights of this article:

1, APT project needs to create different types of modules and Module dependencies

Java source files are really a structured language, with each part of the source code corresponding to a specific type of Element

3. Use auto-service to automatically register the annotation processor

4. Use Javapoet framework to write Java class files needed to be generated

5. Through reflection and appropriate encapsulation, provide the generated class’s function to the upper level call

Well, that’s the end of this article, hope to help you 🤝

Thanks for reading this article

The next trailer

In the next article, I’ll show you how I applied APT technology to create a replacement for a View created by reflection, 😄

References and Recommendations

APT technology exploration of Android annotation processor

Here is the full text, the original is not easy, welcome to like, collect, comment and forward, your recognition is the motivation of my creation

Follow my public account, search for sweereferees on wechat and updates will be received as soon as possible