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
-
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
-
GetSupportedSourceVersion () returns the latest Java version, you can also use annotations @ SupportedSourceVersion (SourceVersion. RELEASE_8)
-
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
-
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
-
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
-
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
-
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
-
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
-
Create one in IDEARemote Configurarion
-
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)
-
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 Daemon
At this point, we select the one created earlierRemote Configuration
Then click the Debug buttonThe 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
-
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 use
kapt {your_porcessor_module}
Depends on the kapt Gradle pluginapply kotlin-kapt
- If you are a Java project, please use
annotationProcessor {your_porcessor_module}
Depends on, and does not need to addapply kotlin-kapt
kapt
Can be compatible withannotationProcessor
, so if you are a Java/Kotlin hybrid project, usekapt
It is ok
- If you are kotlin engineering, please use
-
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
-
-
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
-
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