使用场景
考虑这样一种场景:我们是一个汽车生产商,我们生产各种品牌的汽车,比如宝马、奔驰、奥迪等等,为了面向对象开发,我们定义一个基类 Car
abstract class Car {
fun brand(): String // 每辆车都有一个品牌
}
复制代码
各个牌子的车
代码语言:javascript复制class BMW : Car {
override fun brand(): String {
return "BMW"
}
}
class Benz : Car {
override fun brand(): String {
return "Benz"
}
}
class Audi : Car {
override fun brand(): String {
return "Audi"
}
}
复制代码
我们是汽车生产商,我们生产车,而不是搬运工,我们需要一个生产车间,因此我们需要定义一个工厂类 CardFactory
class CardFactory {
fun produceCar(brand: String): Car {
when (brand) {
"BMW" -> return BMW()
"Benz" -> return Benz()
"Audi" -> return Audi()
}
}
}
复制代码
看起来非常完美,使用了工厂模式,很高级,需要生产什么牌子的车,直接传一个品牌名字就可以生产出对应牌子的汽车了。我们把这一套生产流程交给公司的骨干 小明 负责。
随着我们的生意越做越大,我们生产的汽车品牌越来越多,但是没有关系,得益于我们良好的封装,我们只需要继承 Car
类,实现新品牌汽车,然后在工厂类 CardFactory
中增加一个 when -> case 的判断就好了,由于小明非常熟悉这一套生产流水线,所以每一次有新增品牌都难不倒小明。
后来公司越做越大,小明从基础骨干晋升为部门 Leader,为了提高工作效率,汽车品牌的实现交给 小白 负责,工厂的负责人分配给了 小黑 ,由于小白只负责汽车的实现,小黑只负责工厂的管理,所以常常出现一个问题:小白实现了一个新品牌汽车,而小黑没有在工厂中新增新品牌汽车的生产逻辑,这就导致生产线出现了问题
为了解决这个问题,小明想到了一个方法:其实每次有新增品牌的汽车,工厂类只需要增加一个判断逻辑即可,工作十分枯燥,甚至有点冗余。这里有一个可优化的点,只要 Car 的实现类确定之后,工厂类的新增代码就是固定的,即模板代码是确定的。于是小明发明了一套基于 Annotation Processor
和编译时注解实现的自动生成工厂类代码的方案
首先自定义一个注解类 @CarAnnotation
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class CarAnnotation(val brand: String)
复制代码
然后在各个子类中加上这个注解
代码语言:javascript复制@CarAnnotation("BMW")
class BMW : Car {
override fun brand(): String {
return "BMW"
}
}
@CarAnnotation("Benz")
class Benz : Car {
override fun brand(): String {
return "Benz"
}
}
@CarAnnotation("Audi")
class Audi : Car {
override fun brand(): String {
return "Audi"
}
}
复制代码
然后通过小明发明的注解代码生成器 就可以自定生成以下代码
代码语言:javascript复制class CardFactory {
fun produceCar(brand: String): Car {
when (brand) {
"BMW" -> return BMW()
"Benz" -> return Benz()
"Audi" -> return Audi()
}
}
}
复制代码
对,和刚刚我们手写的代码一模一样,只不过这一切都是自动生成的,后面如果有新增品牌的汽车,只需要在新的子类上面,加上 CarAnnotation
注解即可,再也不用担心忘记在工厂类中新增模板代码的问题。维护成本变低(小黑可以财务室结账了)、效率更高、出错概率也更小了(新增需求只需要关注一个 Car 子类即可)
材料准备
- 自定义注解
- Annotation Processor
- JavaPoet or KotlinPoet
需要新建两个 Java library Module
- 自定注解的 module
- 自定义注解处理器的 module
为什么需要分开两个工程?如果注解和注解处理器放在同一个 module 里,那么主工程就需要 implementation 这个 module,但是注解处理器只在编译时需要用到,相关的代码其实是不需要参与到 apk 打包里面的,所以最好的办法是分开两个工程。
需要使用注解的地方添加依赖
代码语言:javascript复制implementation project(':car-anntation') // 注解工程
kapt project(':car-processor') // 注解处理器工程, for kotlin (如果使用注解的代码是 Kotlin 代码,必须加上这个,否则注解处理器不生效)
annotationProcessor project(':car-processor') // 注解处理器工程, for java (kapt 可以兼容 annotationProcessor,反之不行)
// 注意 Kotlin 版本要加上
apply kapy
// 或者
plugins {
id 'kotlin-kapt'
}
复制代码
自定义注解
元注解(作用在注解上面的注解):
@Target
定义注解可使用的范围,可以是类、方法、属性、变量等等
Retention
定义注解保留的范围,有源代码、编译时、运行时三种
MustBeDocumented
是否可生成在 Doc 里面
Java 定义注解的方式
代码语言:javascript复制@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface CarAnnotation {
String brand();
}
复制代码
kotlin 定义注解的方式
代码语言:javascript复制@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class CarAnnotation(val brand: String)
复制代码
自定义注解处理器
注解处理器是如何工作的
编译的时候,编译器会扫描所有注册的注解处理器,然后记录下每个注解处理器所支持的注解(通过 getSupportedAnnotationTypes
返回)
注解处理会执行很多轮。编译器首先会读取 Java/Kotin 源文件,然后查看文件中是否有使用注解,如果有使用,则调用其对应的注释处理器,这个注解处理器(可能会)生成新的带有注解的 Java 源文件,生成的新文件也会参与编译,然后再次调用其相应的注释处理器,然后再次生成更多的 Java 源文件,就这样一直循环,直到没有新的文件生成。
如何实现一个注解处理器
继承 AbstractProcessor
实现自定义注解处理器
@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class CarAnnotationProcessor : AbstractProcessor() {
override fun getSupportedAnnotationTypes(): MutableSet<String> {
return mutableSetOf(CarAnnotation::class.java.canonicalName)
}
override fun process(
set: MutableSet<out TypeElement>,
roundEnvironment: RoundEnvironment
): Boolean {
// 在这里实现逻辑
// 返回 false 代表不处理,交给其他注解处理器继续处理,否则返回 true
}
}
复制代码
- 覆写
getSupportedAnnotationTypes()
方法,返回要处理哪些自定义注解,也可以使用@SupportedAnnotationTypes()
它的返回值是process()
方法的第一个参数 -
getSupportedSourceVersion()
返回最新 Java 版本就好了,也可以使用注解@SupportedSourceVersion(SourceVersion.RELEASE_8)
- 需要在子类中实现
process()
方法,在这里可以通过获取代码中标注了某个注解的所有类,然后处理自定义的逻辑 - 注册注解处理器,在注解工程的 META-INF/services 路径下新增文件 javax.annotation.processing.Processor 并在文件中增加一行注解处理器的全限定名
com.example.code.CarAnnotationProcessor
或者使用 google 的自动注册处理器库,加上一个注解@AutoService(Processor::class)
就可以了,需要在注解处理器工程中依赖
implementation 'com.google.auto.service:auto-service:1.0-rc4'
kapt 'com.google.auto.service:auto-service:1.0-rc4' // kotlin 版本
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4' // Java 版本
特别注意如果使用 Kotlin 的话,要需要在build.gradle
中加上
plugins {
id 'kotlin-kapt'
}
// 或者
apply kapt
使用 JavaPoet or KotlinPoet 生成代码
JavaPoet 和 KotlinPoet 是一个生成 Java/Kotlin 代码的库
在上面的例子中,我们需要扫描出所有标注了 @CarAnnotation
注解的类,然后自动生成一个 CarFactory
类
1.首先找到所有标注了注解的代码
代码语言:javascript复制// 获取所有标注了 @Car 的类
val cardList = roundEnvironment.getElementsAnnotatedWith(CarAnnotation::class.java)
// 这里强转成 TypeElement 是为了方便获取更多有用的信息
.map { it as TypeElement }
.map {
val annotation = it.getAnnotation(CarAnnotation::class.java) // 获取注解实例
val brand = annotation.brand // 拿到注解中的 brand
val carClazz = it.javaClass.canonicalName // 拿到被注解的类名
brand to carClazz // 准换成一个 Map
}
2.然后根据上面获取到的信息拼凑成代码
代码语言:javascript复制// 根据 Map 生成 "brand" -> return Car() 的代码
val sb = StringBuilder()
sb.appendln("when(brand) {")
cardList.forEach {
sb.appendln(""${it.first}" -> return ${it.second}()")
}
sb.append("}")
3.用 KotlinPoet 生成代码,具体 API 可以参考官网
代码语言:javascript复制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)) // 写入文件
4.build 一下就可以在 build/generated/source/kaptKotlin/debug
下看到生成的代码了
如何 Debug Annotation Processor
由于注解处理器的运行时机是在编译的时候,如果我们希望在编写代码的时候 Debug 就会有些麻烦,通过日志输出的方式也不够方便,如何实现在注解处理器中断点调试呢
☞ Debug Annotation Processor in Kotlin
1.在 idea 中创建一个Remote Configurarion
2.修改gradle.properties
文件
kapt.use.worker.api=true
一定要加上这一行,否则断点不会生效(这里只适合 Kotlin,Java 怎么配置需要手动 google 一下)
3.运行以下命令
代码语言:javascript复制./gradlew --no-daemon -Dorg.gradle.debug=true -Dkotlin.daemon.jvm.options="-Xdebug,-Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=n" {module}:assembleDebug
运行之后会卡在> Starting Daemon
处,这个时候我们选中之前创建的Remote Configuration
然后点击 Debug 按钮
然后上面的命令才会继续运行,直到运行到断点所在的地方
每次运行之前需要 gradle clean 一下,否则可能 Annotation Processor 不会执行
踩坑记录
注解处理器不生效,所有 Processor 的方法都没有执行
检查一下使用注解处理器的工程是否使用了正确的依赖方式,如果使用注解处理器的工程的 build.gradle
使用了 apply kotlin-kapt
插件,但是 dependencies 处定义成 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'.
- 如果你是 kotlin 工程,请使用
kapt {your_porcessor_module}
的方式依赖,且需要依赖 kapt gradle 插件apply kotlin-kapt
- 如果你是 Java 工程,请使用
annotationProcessor {your_porcessor_module}
的方式依赖,且不需要加上apply kotlin-kapt
kapt
可以兼容annotationProcessor
,反之不行,所以如果你是 Java 和 kotlin 混用的工程,使用kapt
就可以了
2.注解处理器的 init
和 getSupportedAnnotationTypes
都执行了,但是 process
方法没有
在文章「注解处理器的工作原理」中有讲到,只有我们代码中使用了注解(getSupportedAnnotationTypes
的返回的那些注解)的时候,相应的注解处理器才会执行 process 方法,所以:
- 如果代码中根本没有使用到注解,process 方法是不执行的
- 如果使用注解的代码是 Kotlin 代码,那么必须使用
kapt {your_porcessor_module}
的方式依赖,且需要依赖 kapt gradle 插件apply kotlin-kapt
,否则如果使用annotationProcessor {your_porcessor_module}
也会导致 process 不执行
3.process()
方法会执行多次,如何保证写文件的逻辑不被多次调用
可以在 process()
方法中通过调用 val processingOver = roundEnvironment.processingOver()
判断是否第一次执行 process()
: processingOver
为 false 代表第一次执行
4.有时候我们想要拿到注解中的参数,如果这个参数刚好是 Class<*> 类型的,在 process()
方法中尝试获取换个 Class 对象的时候会发生错误,这是因为 Annotation Processor 在执行的时候这个类可能还没有参与编译,因此我们可以使用下面的方式先保存下这个类的名字,这样后续我们可以通过反射等方式来拿到这个 Class
val parserClazzName = try {
// 如果类已经编译了,这里可以直接拿到名字,否则会抛出 MirroredTypeException 异常
val clazz = it.getAnnotation(HyperSpan::class.java).parser
clazz.java.canonicalName
} catch (mte: MirroredTypeException) {
// 如果还没有编译,则通过这里拿到类名,之后尝试使用反射的方式拿到 Class
((mte.typeMirror as DeclaredType).asElement() as TypeElement).qualifiedName.toString()
}