@[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:
@Override
: indicates that the current method definition overrides methods in the superclass;@Deprecated
: Use this annotation to cause the compiler to issue a warning;@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,CLASS Annotations 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:
-
Annotation processing is a tool built into JavAC for scanning and processing annotations at compile time.
-
It can create new source files; However, it cannot modify existing ones.
-
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:
-
SupportedSourceVersion specifies that this processor supports Java 8
-
All annotation processors must inherit the AbstractProcessor class.
-
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.
-
Process is the core method called in each annotation processing round.
-
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.
- Represents the RouterAnnotationInit class to import;
- Represents the RouterAnnotationHandler class to import;
- 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:
- Define a package name
generatedPackage
, the name forfileName
The file; - Add a file named
fileName
In the class; - 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:
addInitClass
是FileSpec.Builder
To which it performs the following operations:- Add a file named
fileName
In the class; - This class implements
routerAnnotationInit
Interface;
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:
addInitMethod
是TypeSpec.Builder
To which it performs the following operations:- Add a class named
init
The method; - This method overrides an abstract method;
addParameter
Adds parameters to the function definition, overridden by this methodinit
The method takes one argument:handler
;- Returns a
UNIT
; - 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:
- If the root element of the round is not annotated, the annotation collection is empty and returns
false
, indicating that our custom annotation handler will not be processed. - CodeBlock is
init
Method, as there may be more than one in a moduleActivity
beRouter
Annotation annotation, so we need to collect all activities marked by the Router; - The name of the file to generate is
AnnotationInit
Add a random UUID to prevent duplicate files generated between multiple modules; - Element is the element annotated with the Router annotation, in this case the Activity class;
- Gets an annotation for this element, that is
Router
Annotations to get the url of the parameters in the Router; - Get the simple name of this element, the Activity’s class name;
- Gets the package name for this element, that is, the package name for the Activity;
- Generates the fully qualified name of the target Activity, for example:
com.guiying712.android.MainActivity
- Assembly method body;
- Generated based on the above information
RouterInit_X
Class, remember that each module should only generate oneRouterInit_X
Class.
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.