What is APT?

APT is the Annotation Processing Tool. It is a javAC Tool that can be used to scan and process annotations at compile time.

Many mainstream tripartite libraries now use this technique, such as ARouter, ButterKnife, etc. This paper mainly records the use process of APT. Many of the details are already a bit hazy.

1. Custom annotations

Create a Java Module that defines annotation-related logic:


Define a custom annotation in the newly generated lib :FRC_TEST

/ * * *@author: frc
 * @description: Test notes *@date: 2021/2/2 10:18 PM
 */
@Target(ElementType.TYPE)// Place the annotation on the class
@Retention(RetentionPolicy.CLASS)// Annotate valid time
public @interface FRC_TEST {
}

Copy the code

1.1 yuan notes

Meta-annotations are annotations that can be used on annotations

Meta annotation: @target is used to mark the location of the current custom annotation, such as class, method, parameter, and so on.

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since1.8 * /
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since1.8 * /
    TYPE_USE
}
Copy the code

Meta annotation: Retention marks the Retention period of the current annotation, such as runtime, compile-time, etc.

public enum RetentionPolicy {
    /** * Annotations are to be discarded by the compiler. */
    SOURCE,

    /** * Annotations are to be recorded in the class file by the compiler * but need not be retained by the VM at run time.  This is the default * behavior. */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}


Copy the code

There’s a lot of confusion between SOURCE and CLASS. SOURCE is removed at compile time to give developers hints at the SOURCE stage, such as @override. CLASS is the default state, present at compile time and removed at run time. What it means is not recognized by the virtual machine, that is, it cannot be reflected at run time.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
Copy the code

So the FRC_TEST defined above is a compile-time annotation that can be used on a class or interface

2. Get annotations at compile time

The above custom FRC_TEST annotation needs to be taken at compile time. So this annotation is read at compile time. How do I read it?

Javac will scan all.java files before compiling them into.class files, and we can register a service to listen for the current scanning behavior. If scanning behavior is detected, the files annotated by FRC_TEST can be filtered out. Once you have these files, generate some auxiliary.java files before you actually compile. Compile to.class.

You can create a Java Module called FRC-Compiler to handle the annotation-related logic at compile time.

2.1 Annotation processing classes

First we need to define a class that inherits AbstractProcessor, which will be triggered at compile time to handle whatever the custom annotations want to do. We call this annotation handler class:

package com.rong.cheng.frc_compiler;

import java.util.Set;

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.TypeElement;

/ * * *@author: frc
 * @description: annotation processor for FRC_TEST annotations *@date: 2021/2/2 10:50 PM
 */
@SupportedSourceVersion(SourceVersion.RELEASE_8)// The supported Java version
@SupportedAnnotationTypes("com.rong.cheng.frc_annotation.FRC_TEST")// Which annotation is supported
class FrcTestAnnotationProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false; }}Copy the code
  • Inheritance AbstractProcessor

  • SupportedSourceVersion() Specifies the largest Java version supported by the file system

  • SupportedAnnotationTypes() specifies which annotations are to be processed by the current annotation handler. Javac will call back to the current annotation handler only when it scans for the specified annotation.

SupportedAnnotationTypes supports passing multiple annotations as follows, so a single annotation handler supports handling multiple annotations:

@Documented
@Target(TYPE)
@Retention(RUNTIME)
public @interface SupportedAnnotationTypes {
    /**
     * Returns the names of the supported annotation types.
     * @return the names of the supported annotation types
     */
    String [] value();
}

Copy the code

This defines which annotations to scan at compile time, so how can the current annotation handler be notified at compile time? You need to register the current class as a service.

2.2 Registration Service

This requires the use of Google AutoService

Add the following dependencies in build.gradle of the FRC-Compiler

dependencies {
    // AutoService is not available in annotationProcessor.
    compileOnly("Com. Google. Auto. Services: auto - service: 1.0 rc7." ")
    annotationProcessor("Com. Google. Auto. Services: auto - service: 1.0 rc7." "
    
     // Rely on annotation project
    implementation project(":frc-annotation")}Copy the code

Then add on FrcTestAnnotationProcessor @ AutoService annotation

@AutoService(Processor.class)// Register as a service
@SupportedSourceVersion(SourceVersion.RELEASE_8)// The supported Java version
@SupportedAnnotationTypes({"com.rong.cheng.frc_annotation.FRC_TEST"})// Which annotation is supported
class FrcTestAnnotationProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false; }}Copy the code

We verify success by printing a message through Messager:

@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.rong.cheng.frc_annotation.FRC_TEST"})// Which annotation is supported
@SupportedSourceVersion(SourceVersion.RELEASE_8)// The supported Java version
class FrcTestAnnotationProcessor extends AbstractProcessor {
    private  Messager messager;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager=processingEnv.getMessager();
      	// Note that diagnostic.kind. ERROR cannot be used, otherwise an exception will be generated
        messager.printMessage(Diagnostic.Kind.NOTE,"--------->FrcTestAnnotationProcessor init");
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE,"--------->FrcTestAnnotationProcessor process");

        return false; }}Copy the code

Then rely on the annotation handler in the main project:

dependencies {

    // Rely on custom annotations
    implementation project(":frc-annotation")
    // Use custom annotation handlers
    annotationProcessor project(":frc-compiler")
}
Copy the code

Note that if the annotation is used in Kotlin code you need to use Kapt to rely on the annotation

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'// The kapt plugin is required, and the plugin must be under 'kotlin-Android'
}

dependencies {
    // Rely on custom annotations
    implementation project(":frc-annotation")
    // Use custom annotation handlers
    kapt project(":frc-compiler")
}
Copy the code

We can determine if the annotation handler is working by manually checking the following files:

There are actually two problems with the above code:

Due to the previously defined annotation handler class when forget to add public modification, leading to compile time find FrcTestAnnotationProcessor class:

So FrcTestAnnotationProcessor must add public decoration

@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.rong.cheng.frc_annotation.FRC_TEST"})// Which annotation is supported
@SupportedSourceVersion(SourceVersion.RELEASE_8)// The supported Java version
public class FrcTestAnnotationProcessor extends AbstractProcessor {}Copy the code

The second problem is that only logs in init are printed:

This is because our FRC_TEST annotation is not actually used and will not be executed to the process method.

Add the following randomly in the main app:

@FRC_TEST
class MainActivity : AppCompatActivity(a){
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
Copy the code

This time the process method is executed

Two problems were found:

- Logs do not wrap - the process method fires twiceCopy the code

At this point reading the annotations at compile time is complete.

Note: If the compile does not respond, try the clean project first, then make

2.3 Read the parameters passed in by the Android project

Nowadays, multiple modules are developed in collaboration, and sometimes annotations need to be differentiated according to the situation of different modules. In this case, we need to read the configuration parameters passed by the Module.

2.3.1 Setting Parameters

The build.gradle configuration file provides parameters for annotation processing:

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.0"

    defaultConfig {
        applicationId "com.rong.cheng.study"
        minSdkVersion 19
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

        javaCompileOptions{
            annotationProcessorOptions{
                arguments = ["moduleName":"app"]}}}}Copy the code

JavaCompileOptions: Configuration options at Java compile time

AnnotationProcessorOptions: annotation processing configuration options

Arguments: configuration parameters, key: value.

Configure the configuration for annotation handling at Java compile time.

2.3.2 Reading Parameters

  • First you need to add to the annotation handler class@SupportedOptionsannotations
  • Annotation handler init when passedProcessingEnvironmentTo obtain

The code is as follows:

@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.rong.cheng.frc_annotation.FRC_TEST"})// Which annotation is supported
@SupportedSourceVersion(SourceVersion.RELEASE_8)// The supported Java version
@SupportedOptions("moduleName")// Reads the parameters passed in the project using the annotation handler
public class FrcTestAnnotationProcessor extends AbstractProcessor {

    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        messager.printMessage(Diagnostic.Kind.NOTE, "--------->FrcTestAnnotationProcessor init \n");
        String projectName = processingEnv.getOptions().get("moduleName");

        messager.printMessage(Diagnostic.Kind.NOTE, "--------->module name is :" + projectName);

    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE, "--------->FrcTestAnnotationProcessor process \n");

        return true; }}Copy the code

Compile to see the output:

2.3 Reading information about objects modified by annotations

We decorated the MainActivity class earlier with the FRC_TEST annotation. Now we need to read the current information for the class in the annotation handler so we can do something about it.

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE, "--------->FrcTestAnnotationProcessor process \n");

        // Get information about the class annotated by FRC_TEST
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(FRC_TEST.class);		
        // Since the same annotation can be used more than once, I'll iterate here
        for (Element element : elements) {
			// The annotated class is available from this Element object
            // Get the annotated class name
          messager.printMessage(Diagnostic.Kind.NOTE, "--------->class name \n"+element.getSimpleName());

        }


        return true;
    }
Copy the code

3. Generate helper classes

The essence of generating helper classes is to build a JavalEI-compliant file format and write it locally. The most direct way is to write the package name line by line, then write the imported class, and then write the class name, method and so on into the file one by one. For example, the EventBus framework:

We can also use tools to help us generate less code.

JavaPoet is such a tool:

Add Javapoet dependencies to the FRC-Compiler Module

dependencies{...//javapoet
    compileOnly("Com. Squareup: javapoet: 1.13.0.")... }Copy the code

Using the JavaPoet demo, we generate a HelloWorld class that prints “Hello, JavaPoet!” .

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE, "--------->FrcTestAnnotationProcessor process \n");

        // Get information about the class annotated by FRC_TEST
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(FRC_TEST.class);

        for (Element element : elements) {


            /** * package com.example.helloworld; * * public final class HelloWorld { * public static void main(String[] args) { * System.out.println("Hello, JavaPoet!" ); *} *} */

            // Generate a main method
            MethodSpec mainMethod = MethodSpec.methodBuilder("main")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(void.class)
                    .addParameter(String[].class, "args")
                    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                    .build();
            messager.printMessage(Diagnostic.Kind.NOTE, "--------->element name \n"+element.getSimpleName());

// // Generated class
            TypeSpec helloWorldClass = TypeSpec.classBuilder("HelloWorld")
                    .addMethod(mainMethod)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .build();

            // Write package information, the entire Java file information is complete
            JavaFile javaFile = JavaFile.builder("com.rong.cheng.study", helloWorldClass).build();

            // Write the javaFile build file locally. MFiler = processingenv.getFiler (); Get it in init
            try {
                javaFile.writeTo(mFiler);
            } catch (IOException e) {
                messager.printMessage(Diagnostic.Kind.NOTE, "Failed to generate HelloWorld file while generating FRC_TEST annotation handler:"+ e.getMessage()); e.printStackTrace(); }}Copy the code

When compiled, the following files are generated in the following location:

If kAPT is used to rely on annotations, the handler generates the file in the following location:

! [image-20210205160645956](/Users/frc/Library/Application Support/typora-user-images/image-20210205160645956.png)

Once generated, it can be used in source code:

So far the whole APT normal use process will pass.


This paper is just a simple summary of the use of APT, which is convenient for myself to refer to and share with you in the future. For practical use of APT, refer to ARouter.