@[toc]

Annotation processing is a powerful tool for generating code for Java programs. In this article, you’ll develop an annotation and an annotation handler that automatically generates routing information initialization code based on routing parameters for a given Activity class.

Note: This code is written entirely in Kotlin.

What are annotations

To quote the definition of annotations from Chapter 20 of Java Programming Ideas:

Annotations (also known as metadata) give us a formalized way to add information to our code, making it easy to use that data at a later point in time.

There are three standard annotations built into Java:

  1. @Override: indicates that the current method definition overrides methods in the superclass;
  2. @Deprecated: Use this annotation to cause the compiler to issue a warning;
  3. @SuppressWarnings: Disables improper compiler warnings;

Java also provides four additional types of annotations for the creation of new annotations, also known as meta-annotations.

annotations meaning
@Target Indicates where the annotation can be used. possibleElementTypeInclude:CONSTRUCTOR: constructor declaration,FIELD: domain declaration (including enum instances),LOCAL_VARIABLE: local variable declaration,METHOD: method declaration,PACKAGE: package declaration,PARAMETER: Parameter declaration,TYPE: class, interface (including annotation type), or enum declaration
@Retention Indicates the level at which the annotation information needs to be saved. optionalRetentionPolicyParameters include:SOURCE: Annotations are discarded by the compiler,CLASSAnnotations are available in the class file, but are discarded by the VM,RUNTIME: THE VM will also retain annotations at run time, so they can be read by reflection
@Documented Include this annotation in Javadoc.
@Inherited Allows subclasses to inherit annotations from their parent class.

Create annotation

The first step is to create a new module to hold our annotations.

It is common practice to keep annotations and handlers in separate modules.

Select File ▸ New ▸ New Module, then select Java or Kotlin Library, Module name: Annotations, class name: Router, language: Kotlin, fill in the information to complete the creation.

Create an annotated Router as follows:

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Router(val url: String)
Copy the code

@target uses TYPE to indicate that we will only use the Router on classes, and @Retention uses SOURCE to indicate that the Router only needs to exist during SOURCE compilation.

Annotations can have parameters that allow us to add more information.

What is an annotation processor

The annotation processor helps us do more with less, which means that less code (annotations) magically translates into more functionality. Here is an introduction to the annotation processor:

  1. Annotation processing is a tool built into JavAC for scanning and processing annotations at compile time.

  2. It can create new source files; However, it cannot modify existing ones.

  3. It’s done in turns. When compilation reaches the precompilation stage, the first round begins. If this round generates any new files, the next round starts with the generated files as its input. This continues until the processor has processed all the new files.

Javac is a Java language programming compiler. Java Compiler. The Javac tool reads the definitions of classes and interfaces written in the Java language and compiles them into class files of bytecode.

The following figure illustrates the process:

Writing annotation handlers

Create a Java or Kotlin Library module again with the module name processor and class name Processor, using Kotlin as the language and using our custom annotations for the processor. So open the processor/build.gradle and add the following to the dependency block:

implementation project(':annotations')
Copy the code

Open processor.kt and replace the import and class declaration with the following:

import com.guiying712.annotations.Router
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.RoundEnvironment
import javax.annotation.processing.SupportedSourceVersion
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.TypeElement

@SupportedSourceVersion(SourceVersion.RELEASE_8) / / 1
class Processor : AbstractProcessor(a){ / / 2

 override fun getSupportedAnnotationTypes(a) = mutableSetOf(Router::class.java.canonicalName) / / 3

 override fun process(annotations: MutableSet
       
        ? , roundEnv: RoundEnvironment)
       : Boolean { / / 4

   // TODO
   return true / / 5}}Copy the code

The above code is explained below:

  1. SupportedSourceVersion specifies that this processor supports Java 8

  2. All annotation processors must inherit the AbstractProcessor class.

  3. GetSupportedAnnotationTypes () defines the processor at run time to find a set of annotations. If no element in the target module is annotated with an annotation from the collection, the processor will not run.

  4. Process is the core method called in each annotation processing round.

  5. If all goes well, process must return true.

Next we will register the annotation handler. To do this, we must create a special file:

We must register the processor with Javac so that the compiler knows to call it at compile time.

Expand Processor ▸ SRC ▸ main and add a new directory called Resources. Then add a subdirectory to the resource and name it meta-INF (capital letters must be required). Finally, add a subdirectory in meta-INF named Services. In the services to add an empty file and name it javax.mail. Annotation. Processing. The Processor.

Open file javax.mail. The annotation. Processing. The Processor and will we create fully qualified name of the Processor as its content. As follows:

com.guiying712.processor.Processor
Copy the code

The compiler now knows about our custom processor and will run it during its precompilation phase.

Of course, the above method is too cumbersome, so Google gave me the development of automatic registration tool AutoService, open the processor/build.gradle dependency block to add the following content:

implementation "Com. Google. Auto. Services: auto - service: 1.2.1." "
Copy the code

Open processor.kt and add a annotation @autoService (processor.class) as follows:

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class Processor : AbstractProcessor(a){
Copy the code

AutoService will be generated in the output class folder file meta-inf/services/javax.mail annotation. Processing. The Processor. This file will contain:

com.guiying712.processor.Processor
Copy the code

Use an annotation handler to generate code

Create an Android Library module to store routing framework related classes. The module name is Router and the language is Kotlin.

Create a class called RouterAnnotationHandler to process the routing table, and pass all routing information to this class:

package com.guiying712.router

interface RouterAnnotationHandler {

    fun register(url: String, target: String)

}
Copy the code

Create a class called RouterAnnotationInit that initializes Router annotation information and sends it to handler.

package com.guiying712.router

interface RouterAnnotationInit {

    fun init(handler: RouterAnnotationHandler)

}
Copy the code

The final code generated by the annotation processor looks like this:

package com.guiying712.router.generated

import com.guiying712.router.RouterAnnotationHandler
import com.guiying712.router.RouterAnnotationInit

class AnnotationInit_efb0660a2fd741f3a44a1d521a6f6b18 : RouterAnnotationInit {
  override fun init(handler: RouterAnnotationHandler) {
    handler.register("/demo"."com.guiying712.demo.MainActivity")
    handler.register("/demo2"."com.guiying712.demo.LoginActivity")}}Copy the code

Next we write an annotation handler to generate the above code.

We will use KotlinPoet to generate the Kotlin source file, open the processor/build.gradle and add the following dependencies:

implementation 'com. Squareup: kotlinpoet: 1.10.2'
Copy the code

The source code files created by annotation processing must be in a special folder with the path kapt/kotlin/generated.

To tell the annotation Processor where to put its generated file, add an accompanying object to the Processor class and add the following code:

companion object {
 	const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"
}
Copy the code

Then add the following code to the first line of the process method:

valkaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME] ? :return false
Copy the code

This line of code checks to see if the processor can find the necessary folder and write the file to it. If so, the processor returns the provided usage path. Otherwise, the processor aborts and returns false from the process method.

Create a new file and name it RouterCodeBuilder.kt:

class RouterCodeBuilder(
    private val kaptKotlinGeneratedDir: String,
    private val fileName: String,
    private val code: CodeBlock
) {
}
Copy the code

The constructor takes three parameters: the path to generate code, the name of the class being written, and the code block. Then add some constants to this class:

private val routerAnnotationInit = ClassName("com.guiying712.router"."RouterAnnotationInit") / / 1
private val routerAnnotationHandler = ClassName("com.guiying712.router"."RouterAnnotationHandler") / / 2
private val generatedPackage = "com.guiying712.router.generated" / / 3
Copy the code

ClassName is a KotlinPoet API class that wraps the fully qualified name of a class to create the necessary imports at the top of the generated Kotlin source file.

  1. Represents the RouterAnnotationInit class to import;
  2. Represents the RouterAnnotationHandler class to import;
  3. The package name of the generated class;
 fun buildFile(a) = FileSpec.builder(generatedPackage, fileName) / / 1
        .addInitClass() / / 2
        .build()
        .writeTo(File(kaptKotlinGeneratedDir)) / / 3
Copy the code

Explain the code above:

  1. Define a package namegeneratedPackage, the name forfileNameThe file;
  2. Add a file namedfileNameIn the class;
  3. Write the generated file to the kaptKotlinGeneratedDir folder.

KotlinPoet uses TypeSpec to define class code.

A useful trick is to create a private extension function on FileSpec.Builder so that we can neatly insert snippets into the buildFile() method call chain created above.

 private fun FileSpec.Builder.addInitClass(a) = apply { / / 1
        addType(TypeSpec.classBuilder(fileName)  / / 2
                .addSuperinterface(routerAnnotationInit) / / 3
                .addInitMethod(code) / / 3
                .build()
        )
    }

Copy the code

Explain the code above:

  1. addInitClassFileSpec.BuilderTo which it performs the following operations:
  2. Add a file namedfileNameIn the class;
  3. This class implementsrouterAnnotationInitInterface;
private fun TypeSpec.Builder.addInitMethod(code: CodeBlock) = apply { / / 1
        addFunction(FunSpec.builder("init") / / 2
                .addModifiers(KModifier.OVERRIDE) / / 3
                .addParameter("handler", routerAnnotationHandler) / / 4
                .returns(UNIT) / / 5
                .addCode(code) / / 6
                .build()
        )
    }
Copy the code

Explain the code above:

  1. addInitMethodTypeSpec.BuilderTo which it performs the following operations:
  2. Add a class namedinitThe method;
  3. This method overrides an abstract method;
  4. addParameterAdds parameters to the function definition, overridden by this methodinitThe method takes one argument:handler ;
  5. Returns aUNIT;
  6. Add the code block to the method body;

The last step is to insert it into the processor. Open processor.kt and replace the TODO of the process method with:

override fun process(annotations: MutableSet<out TypeElement>? , roundEnv:RoundEnvironment): Boolean {
        if (annotations == null || annotations.isEmpty()) return false / / 1

        valkaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME] ? :return false

        val codeBuilder = CodeBlock.Builder() / / 2
        val fileName = "AnnotationInit" + "_" + UUID.randomUUID().toString().replace("-"."") / / 3

        roundEnv.getElementsAnnotatedWith(Router::class.java)
            .forEach { element ->  / / 4
                val annotation = element.getAnnotation(Router::class.java) / / 5
                val url = annotation.url
                val className = element.simpleName.toString() / / 6
                val packageName = processingEnv.elementUtils.getPackageOf(element).toString() / / 7

                val target = "$packageName.$className" / / 8
                codeBuilder.addStatement("handler.register(%S, %S)", url, target)  / / 9
            }
        RouterCodeBuilder(kaptKotlinGeneratedDir, fileName, codeBuilder.build()).buildFile() / / 10
        return true 
    }

Copy the code

Explain the above code:

  1. If the root element of the round is not annotated, the annotation collection is empty and returnsfalse, indicating that our custom annotation handler will not be processed.
  2. CodeBlock isinitMethod, as there may be more than one in a moduleActivitybeRouterAnnotation annotation, so we need to collect all activities marked by the Router;
  3. The name of the file to generate isAnnotationInitAdd a random UUID to prevent duplicate files generated between multiple modules;
  4. Element is the element annotated with the Router annotation, in this case the Activity class;
  5. Gets an annotation for this element, that isRouterAnnotations to get the url of the parameters in the Router;
  6. Get the simple name of this element, the Activity’s class name;
  7. Gets the package name for this element, that is, the package name for the Activity;
  8. Generates the fully qualified name of the target Activity, for example:com.guiying712.android.MainActivity
  9. Assembly method body;
  10. Generated based on the above informationRouterInit_XClass, remember that each module should only generate oneRouterInit_XClass.

The custom processor can now find the code element that uses the Router annotation, extract the data from it, and then generate a new Kotlin source file based on that information.

Use annotation handlers in Android projects

Open app/build.gradle and add the following dependencies to it:

implementation project(':annotations') / / 1
implementation project(':router') / / 2
kapt project(':processor') / / 3
Copy the code

Open mainActivity.kt and annotate the class with Router:

@Router("/mian")
class MainActivity : AppCompatActivity() {}Copy the code

Finally, build and run the project, and then open the Build folder and look for the generated files as shown below:

Debug annotation handler

1. Select Edit configuration:

2. Create a Remote configuration, name it, and save it:

Open AndroidStudio Terminal, because we are debugging the code during Kotlin Kapt compile time, so execute the following command:

gradlew clean build --no-daemon -Dorg.gradle.debug=true -Dkotlin.compiler.execution.strategy="in-process" -Dkotlin.daemon.jvm.options="-Xdebug,-Xrunjdwp:transport=dt_socket\,address=5005\,server=y\,suspend=n"
Copy the code

If Java AnnotationProcessor is debugging code at compile time, execute the following command:

gradlew.bat --no-daemon -Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac
Copy the code

4. When Starting Daemon starts, place a breakpoint where you want to debug, run the Debug button, wait for a moment (slow and patient) and start debugging step by step in AbstractProcessor.

Log and handle errors in the processor