使用注解自动生成代码

2021-12-08 21:01:19 浏览数 (1)

使用场景

考虑这样一种场景:我们是一个汽车生产商,我们生产各种品牌的汽车,比如宝马、奔驰、奥迪等等,为了面向对象开发,我们定义一个基类 Car

代码语言:javascript复制
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

代码语言:javascript复制
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

代码语言:javascript复制
@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 实现自定义注解处理器

代码语言:javascript复制
@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
    }
}
复制代码
  1. 覆写 getSupportedAnnotationTypes() 方法,返回要处理哪些自定义注解,也可以使用 @SupportedAnnotationTypes() 它的返回值是 process() 方法的第一个参数
  2. getSupportedSourceVersion() 返回最新 Java 版本就好了,也可以使用注解 @SupportedSourceVersion(SourceVersion.RELEASE_8)
  3. 需要在子类中实现 process() 方法,在这里可以通过获取代码中标注了某个注解的所有类,然后处理自定义的逻辑
  4. 注册注解处理器,在注解工程的 META-INF/services 路径下新增文件 javax.annotation.processing.Processor 并在文件中增加一行注解处理器的全限定名
代码语言:javascript复制
com.example.code.CarAnnotationProcessor

或者使用 google 的自动注册处理器库,加上一个注解@AutoService(Processor::class)就可以了,需要在注解处理器工程中依赖

代码语言:javascript复制
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中加上

代码语言:javascript复制
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文件

代码语言:javascript复制
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} 的话,注解处理器是不会生效的,并且编译时会输出如下日志:

代码语言:javascript复制
> 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.注解处理器的 initgetSupportedAnnotationTypes 都执行了,但是 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

代码语言:javascript复制
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()
}

0 人点赞