Usage scenarios

Consider this scenario: We are an automobile manufacturer, and we produce vehicles of various brands, such as BMW, Mercedes, Audi, and so on. For object oriented development, we define a base class Car

abstract class Car {
  fun brand(a): String // Every car has a brand
}
Copy the code

B: All kinds of cars

class BMW : Car {
	override fun brand(a): String {
    return "BMW"}}class Benz : Car {
	override fun brand(a): String {
    return "Benz"}}class Audi : Car {
	override fun brand(a): String {
    return "Audi"}}Copy the code

We’re a car manufacturer, we make cars, not porters, and we need a production shop, so we need to define a factory class, CardFactory

class CardFactory {
  fun produceCar(brand: String): Car {
        when (brand) {
            "BMW" -> return BMW()
            "Benz" -> return Benz()
            "Audi" -> return Audi()
        }
    }
}
Copy the code

Looks very perfect, using the factory mode, very advanced, need to produce what brand of car, directly pass a brand name can produce the corresponding brand of car. We put the production process in the charge of xiao Ming, the backbone of the company.

As our business grows, we produce more and more Car brands, but it doesn’t matter. Thanks to our good packaging, we just need to inherit Car class to realize new brand cars, and then add a when -> case judgment to CardFactory class. Xiao Ming is very familiar with this production line, so every time there is a new brand is not difficult to xiao Ming.

Later, the company grew bigger and bigger, and Xiao Ming was promoted from the basic backbone to the department Leader. In order to improve work efficiency, the realization of the automobile brand was entrusted to Xiao Bai, while the person in charge of the factory was assigned to Xiao Hei. Since Xiao Bai was only responsible for the realization of the automobile, and Xiao Hei was only responsible for the management of the factory, one problem often appeared: Xiaobai realized a new brand of car, while Xiaobai did not have the production logic to add a new brand of car in the factory, which led to problems in the production line

In order to solve this problem, Xiao Ming came up with a solution: in fact, every time there is a new brand of car, the factory only needs to add a judgment logic, which is very boring and even a little redundant. There is an optimizable point here, as long as the Car implementation class is identified, the new code for the factory class is fixed, that is, the template code is fixed. So Ming invented a scheme to automatically generate factory class code based on Annotation Processor and compile-time Annotation

Let’s start with a custom annotation class, @CarAnnotation

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class CarAnnotation(val brand: String)
Copy the code

Then add this annotation to each subclass

@CarAnnotation("BMW")
class BMW : Car {
	override fun brand(a): String {
    return "BMW"}}@CarAnnotation("Benz")
class Benz : Car {
	override fun brand(a): String {
    return "Benz"}}@CarAnnotation("Audi")
class Audi : Car {
	override fun brand(a): String {
    return "Audi"}}Copy the code

Then through the annotation code generator invented by Xiao Ming can customize the generation of the following code

class CardFactory {
  fun produceCar(brand: String): Car {
        when (brand) {
            "BMW" -> return BMW()
            "Benz" -> return Benz()
            "Audi" -> return Audi()
        }
    }
}
Copy the code

Yeah, it’s exactly the same code we just wrote, except it’s all automatically generated, and if you have a new car, you just add the CarAnnotation on top of the new subclass, and you don’t have to worry about forgetting to add the template code to the factory class. Lower maintenance costs (Hei can now settle accounts in the accounting room), more efficiency, and less error probability (new needs only need to pay attention to a Car subclass)

Material preparation

  • Custom annotations
  • Annotation Processor
  • JavaPoet or KotlinPoet

Two New Java Library Modules are required

  • Custom annotated Module
  • Custom annotation handler module

Why separate the two projects? If annotations and annotation handler are in the same Module, then the main project needs to implement module, but the annotation handler is only used at compile time, and the related code does not actually need to be included in the APK package, so it is best to separate the two projects.

Add dependencies where annotations are needed

implementation project(':car-anntation') // Annotation project
kapt project(':car-processor') // Annotation handler project, for Kotlin (this must be added if the code using annotation is Kotlin code, otherwise annotation handler will not work)
annotationProcessor project(':car-processor') // Annotate processor engineering, for Java (kapt is compatible with annotationProcessor, not the opposite)

// Note that the Kotlin version is added
apply kapy
/ / or
plugins {
    id 'kotlin-kapt'
}
Copy the code

Custom annotations

A meta-note (a note applied to a note) :

@target defines the scope that an annotation can use, which can be classes, methods, properties, variables, and so on

Retention Defines the scope of annotation Retention, including source code, compile time, and run time

MustBeDocumented is documented in a Doc

The way Java defines annotations

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface CarAnnotation {
   String brand(a);
}
Copy the code

The way Kotlin defines annotations

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class CarAnnotation(val brand: String)
Copy the code

Custom annotation handlers

How does the annotation processor work

Compilation, the compiler will scan all registered annotation processor, and then record the footnotes support each annotation processor (via getSupportedAnnotationTypes return)

Annotation processing goes through many rounds. Compiler will first read Java/Kotin source file, and then see if there are any more use annotations file, if use, is to call its corresponding annotation processor, the annotation processor (may be) to generate new Java source files with annotations, generated a new file will also participate in compiling, then call its corresponding annotation processor again, More Java source files are generated again, and so on until no new files are generated.

How to implement an annotation handler

Implement custom annotation processors by inheriting AbstractProcessor

@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class CarAnnotationProcessor : AbstractProcessor() {

    override fun getSupportedAnnotationTypes(a): MutableSet<String> {
        return mutableSetOf(CarAnnotation::class.java.canonicalName)
    }

    override fun process(
        set: MutableSet<out TypeElement>,
        roundEnvironment: RoundEnvironment
    ): Boolean {
      // Implement logic here
     	// Return false to leave processing to another annotation handler, otherwise return true}}Copy the code
  1. Overwrite getSupportedAnnotationTypes () method, return to deal with what custom annotations, you can also use the @ SupportedAnnotationTypes () it is the return value of the process () method of the first parameter

  2. GetSupportedSourceVersion () returns the latest Java version, you can also use annotations @ SupportedSourceVersion (SourceVersion. RELEASE_8)

  3. You need to implement the process() method in a subclass, where you can handle the custom logic by getting all the classes in your code that annotate an annotation

  4. Registered annotation Processor in the annotations engineering meta-inf/services new file path javax.mail. Annotation. Processing. The Processor and add a line in the file annotation Processor of the fully qualified name

    com.example.code.CarAnnotationProcessor
    Copy the code

    Or use Google’s automatic registration Processor library with an annotation @AutoService(Processor:: Class) that needs to be relied on in the annotation Processor project

    implementation 'com. Google. Auto. Services: auto - service: 1.0 rc4'
    kapt 'com. Google. Auto. Services: auto - service: 1.0 rc4' / / kotlin version
    annotationProcessor 'com. Google. Auto. Services: auto - service: 1.0 rc4' / / Java version
    Copy the code

    Note that if you use Kotlin, you need to add it to build.gradle

    plugins {
        id 'kotlin-kapt'
    }
    / / or
    apply kapt
    Copy the code

Use JavaPoet or KotlinPoet to generate the code

JavaPoet and KotlinPoet are a library that generates Java/Kotlin code

In the example above, we need to scan all the classes with the @CarAnnotation annotation and automatically generate a CarFactory class

  1. Start by finding all the annotated code

    // Get all classes labeled @car
    val cardList = roundEnvironment.getElementsAnnotatedWith(CarAnnotation::class.java)
         // This is enforced to TypeElement to get more useful information
        .map { it as TypeElement }
        .map {
            val annotation = it.getAnnotation(CarAnnotation::class.java) // Get the annotation instance
            val brand = annotation.brand // Get the brand in the annotation
            val carClazz = it.javaClass.canonicalName // Get the annotated class name
            brand to carClazz // Change it to a Map
        }
    Copy the code
  2. The code is then pieced together from the information obtained above

    // Generate "brand" -> return Car() according to Map
    val sb = StringBuilder()
    sb.appendln("when(brand) {")
    cardList.forEach {
        sb.appendln("\"${it.first}\" -> return ${it.second}()")
    }
    sb.append("}")
    Copy the code
  3. Use KotlinPoet to generate the code, the specific API can refer to the official website

    FileSpec.builder("com.example.code"."CarFactory")
                .addType(
                    TypeSpec.classBuilder("CarFactory")
                        .addFunction(
                            FunSpec.builder("produceCar")
                                .addParameter("brandName", String::class.asTypeName())
                                .addModifiers(KModifier.PUBLIC)
                                .returns(Car::class.asTypeName().copy(nullable = true))
                                .addStatement(sb.toString())
                                .build()
                        )
                        .build()
                )
                .build()
                .writeTo(File(kaptKotlinGeneratedDir)) // Write to the file
    Copy the code
  4. It is ok to build in the build/generated/source/kaptKotlin/debug see the generated code

How do I Debug the Annotation Processor

Because the annotation processor is running at compile time, if we want to write code Debug will be a bit of trouble, through the log output is not convenient, how to achieve annotation processor breakpoint debugging

☞ Debug Annotation Processor in Kotlin

  1. Create one in IDEARemote Configurarion

  2. Modify the gradle.properties file

    kapt.use.worker.api=true
    Copy the code

    Be sure to add this line, otherwise the breakpoint won’t work (this is only for Kotlin, you need to manually Google Java)

  3. Run the following command

    ./gradlew --no-daemon -Dorg.gradle.debug=true -Dkotlin.daemon.jvm.options="-Xdebug,-Xrunjdwp:transport=dt_socket\,address=5005\,server=y\,suspend=n" {module}:assembleDebug
    Copy the code

    It will get stuck after it runs> Starting DaemonAt this point, we select the one created earlierRemote ConfigurationThen click the Debug button

    The command above will then continue running until it reaches the breakpoint

    Gradle needs to be clean before each run, otherwise the Annotation Processor may not execute

Record on pit

  1. The annotation Processor is not in effect, and none of the Processor’s methods are executed

    Check that the project using the annotation handler is using the correct dependency method. If the project using the annotation handler uses the Apply Kotlin-kapt plugin for build.gradle, AnnotationProcessor {your_porcessor_module} is not available in annotationProcessor {your_porcessor_module}.

    > Configure project :app
    app: 'annotationProcessor' dependencies won't be recognized as kapt annotation processors. Please change the configuration name to 'kapt' for these artifacts: 'xxxx'.
    Copy the code
    • If you are kotlin engineering, please usekapt {your_porcessor_module}Depends on the kapt Gradle pluginapply kotlin-kapt
    • If you are a Java project, please useannotationProcessor {your_porcessor_module}Depends on, and does not need to addapply kotlin-kapt
    • kaptCan be compatible withannotationProcessor, so if you are a Java/Kotlin hybrid project, usekaptIt is ok
  2. Annotation processor init and getSupportedAnnotationTypes are carried out, but not the process method

    In the article “the principle of” annotation processor has said, only we use annotations in the code (getSupportedAnnotationTypes return those annotations), the corresponding annotation processor executing process method, so:

    • The process method is not executed if there are no annotations in the code at all

    • If the code using the annotations is Kotlin code, then you must use kapt {your_porcessor_module} to rely on the kapt gradle plugin apply Kotlin-kapt. Otherwise, using annotationProcessor {your_porcessor_module} will cause process not to execute

  3. The process() method is executed multiple times. How do I ensure that the logic to write files is not called multiple times

    Can be in the process () method by calling the val processingOver = roundEnvironment. ProcessingOver () to determine whether the process for the first time () : The value processingOver is false indicates the first execution

  4. Sometimes we want to retrieve an argument from an annotation, and if the argument happens to be of type Class<*>, an error occurs when we try to retrieve a different Class object in the process() method. This is because the Annotation Processor may not compile the Class at the time of execution, so we can use the following method to save the name of the Class, so that we can get the Class through reflection and other methods later

    val parserClazzName = try {
     	// If the class was already compiled, you can get its name here, otherwise MirroredTypeException was thrown
     	val clazz = it.getAnnotation(HyperSpan::class.java).parser
     	clazz.java.canonicalName
    } catch (mte: MirroredTypeException) {
      // If not already compiled, get the Class name here, and then try to get the Class using reflection
      ((mte.typeMirror as DeclaredType).asElement() as TypeElement).qualifiedName.toString()
    }
    Copy the code