前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用注解自动生成代码

使用注解自动生成代码

作者头像
GeeJoe
发布2021-12-08 21:01:19
8460
发布2021-12-08 21:01:19
举报
文章被收录于专栏:掘金文章

使用场景

考虑这样一种场景:我们是一个汽车生产商,我们生产各种品牌的汽车,比如宝马、奔驰、奥迪等等,为了面向对象开发,我们定义一个基类 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 子类即可)

材料准备

需要新建两个 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 源文件,就这样一直循环,直到没有新的文件生成。

img
img

如何实现一个注解处理器

继承 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()
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021年04月27日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 材料准备
  • 自定义注解
  • 自定义注解处理器
    • 注解处理器是如何工作的
      • 如何实现一个注解处理器
      • 使用 JavaPoet or KotlinPoet 生成代码
      • 如何 Debug Annotation Processor
      • 踩坑记录
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档