Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >使用注解自动生成代码

使用注解自动生成代码

作者头像
GeeJoe
发布于 2021-12-08 13:01:19
发布于 2021-12-08 13:01:19
1K00
代码可运行
举报
文章被收录于专栏:掘金文章掘金文章
运行总次数:0
代码可运行

使用场景

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
abstract class Car {
  fun brand(): String // 每辆车都有一个品牌
}
复制代码

各个牌子的车

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class CarAnnotation(val brand: String)
复制代码

然后在各个子类中加上这个注解

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface CarAnnotation {
   String brand();
}
复制代码

kotlin 定义注解的方式

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class CarAnnotation(val brand: String)
复制代码

自定义注解处理器

注解处理器是如何工作的

编译的时候,编译器会扫描所有注册的注解处理器,然后记录下每个注解处理器所支持的注解(通过 getSupportedAnnotationTypes 返回)

注解处理会执行很多轮。编译器首先会读取 Java/Kotin 源文件,然后查看文件中是否有使用注解,如果有使用,则调用其对应的注释处理器,这个注解处理器(可能会)生成新的带有注解的 Java 源文件,生成的新文件也会参与编译,然后再次调用其相应的注释处理器,然后再次生成更多的 Java 源文件,就这样一直循环,直到没有新的文件生成。

如何实现一个注解处理器

继承 AbstractProcessor 实现自定义注解处理器

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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
代码运行次数:0
运行
AI代码解释
复制
com.example.code.CarAnnotationProcessor

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
plugins {
    id 'kotlin-kapt'
}
// 或者
apply kapt

使用 JavaPoet or KotlinPoet 生成代码

JavaPoet 和 KotlinPoet 是一个生成 Java/Kotlin 代码的库

在上面的例子中,我们需要扫描出所有标注了 @CarAnnotation 注解的类,然后自动生成一个 CarFactory

1.首先找到所有标注了注解的代码

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 获取所有标注了 @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
代码运行次数:0
运行
AI代码解释
复制
// 根据 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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
kapt.use.worker.api=true

一定要加上这一行,否则断点不会生效(这里只适合 Kotlin,Java 怎么配置需要手动 google 一下)

3.运行以下命令

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
./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
代码运行次数:0
运行
AI代码解释
复制
> 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
代码运行次数:0
运行
AI代码解释
复制
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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
js-函数的prototype
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/163403.html原文链接:https://javaforall.cn
全栈程序员站长
2022/09/15
7520
js原型链
1.每一个构造函数都有一个prototype属性,默认指向一个空Object对象(原型对象)
切图仔
2022/09/08
2.2K0
js原型链
19原型
在 Javascript中,函数是一个包含属性和方法的Function类型的对象。而原型( Prototype)就是Function类型对象的一个属性。
Dreamy.TZK
2020/06/03
3620
【JavaScript】 进阶教程 施工中~
每个函数都有一个prototype属性。它默认指向一个Object空对象(即成为:原型对象)
杨丝儿
2022/02/24
1.4K0
【JavaScript】 进阶教程 施工中~
557 原型prototype和原型链__proto__:原理,函数的三种角色,for in,手写new
Object.create(xxx):创建一个空对象,并且让把xxx作为创建对象的原型(空对象.__proto __ = xxx),xxx必须是对象或者null,如果xxx是null,则创建一个没有任何原型指向的空对象
全栈程序员站长
2022/09/07
2200
557 原型prototype和原型链__proto__:原理,函数的三种角色,for in,手写new
JS原型链温故
js的数据类型主要分为基本类型和引用类型。基本类型包括String、Number、Boolean、undefined、null。引用类型包括Object。
前端_AWhile
2019/08/29
1.3K0
zepto设计和源码分析(推荐) 原
视频附加信息链接:http://www.kancloud.cn/wangfupeng/zepto-design-srouce/173680
晓歌
2018/08/15
3970
zepto设计和源码分析(推荐)
                                                                            原
JS基础知识总结(三):原型、原型链
上一篇JS基础知识总结(二)主要了介绍深拷贝、浅拷贝的基础知识,本文将介绍JS原型、原型链的有关内容。
前端林子
2019/01/05
2.3K1
JS基础知识总结(三):原型、原型链
[我的理解]Javascript的原型与原型链
一、原型与原型链的定义 原型:为其他对象提供共享属性的对象     注:当构造器创建一个对象,为了解决对象的属性引用,该对象会隐式引用构造器的"prototype"属性。程序通过constructor.prototype可以直接引用到构造器的"prototype"属性。并且添加到对象原型里的属性,会通过继承与所有共享此原型的对象共享。 原型链:每个由构造器创建的对象,都有一个隐式引用(叫做对象的原型)链接到构造器的"prototype"属性。再者,原型可能有一个非空隐式引用链接到它自己的原型,以此类推,这叫
sam dragon
2018/01/17
8790
[我的理解]Javascript的原型与原型链
Javascript原型链您了解多少
JS面向对象中的原型 每一个函数都有一个属性 即原型对象(显式原型:prototype)这个原型对象默认指向一个Object空对象,同时每一个原型对象(prototype)都有一个属性(constructor)又指向构造函数(构造函数和它的原型对象相互引用),同时每一个实例对象又有一个__proto__属性(隐式原型),这个属性指向其构造函数的原型对象 (Fn.prototype===fn.__proto__)。
切图仔
2022/09/08
1900
Javascript原型链您了解多少
掌握原型链,再炒冷饭系列
原型链是一个比较抽象的概念,每当被问起这个问题时,总会回答得不是那么准确,好像懂,但自己好像又不太懂,真是尴尬了
Maic
2022/08/29
2010
掌握原型链,再炒冷饭系列
javascript 基础_JavaScript高级编程
1.分类: -基本类型 -String:任意字符串 -Number:任意的数字 -boolean: true/false -undefined:未定义 -null:空
全栈程序员站长
2022/09/24
1.6K0
javascript 基础_JavaScript高级编程
关于原型链的心得体会
实例对象的隐式指向(__proto__)的原型等于构造器的显式指向的(prototype)原型。
砖业洋__
2023/05/06
1820
关于原型链的心得体会
JS题目总结:原型链/new/json/MVC/Promise
解读: 上图中,Object,Function,Array,Boolean都是构造函数
代码之风
2018/10/31
1.1K0
JavaScript学习笔记022-原型链0原型继承0对象的深浅拷贝extends
Author:Mr.柳上原 付出不亚于任何的努力 愿我们所有的努力,都不会被生活辜负 不忘初心,方得始终 这几天一直在做node项目实训 学到了很多实际企业开发知识 学的东西 跟要运用起来的东西 就好像教会你1+1=2 然后让你做高等代数 还需要加倍的努力啊 兄弟 <!DOCTYPE html> <!-- 文档类型:标准html文档 --> <html lang='en'> <!-- html根标签 翻译文字:英文 --> <head> <!-- 网页头部 --> <meat charset='UTF
Mr. 柳上原
2018/09/05
4180
详解原型与原型链
其实,刚开始学 JavaScript 时,就有学过原型与原型链的相关知识了,只是当时还没有养成写笔记的习惯,导致现在已经忘的七七八八了。
赤蓝紫
2023/01/02
4040
详解原型与原型链
JavaScript学习总结(四)——this、原型链、javascript面向对象
根据题目要求,对给定的文章进行摘要总结。
张果
2018/01/04
1.5K0
JavaScript学习总结(四)——this、原型链、javascript面向对象
JavaScript显式原型与隐式原型
在JavaScript中,每个函数都有一个特殊的属性称为"prototype"。这个"prototype"属性是一个对象,它定义了该函数创建的对象的共享属性和方法。
堕落飞鸟
2023/05/17
3280
深入理解原型对象和原型链
原型对象和原型链在前端的工作中虽然不怎么显式的使用到,但是也会隐式的使用了,比如使用的jquery,vue等啦。在进入正题的时候,我们还是需要明白什么是__proto__,prototype等知识点,主要讲解构造函数,这篇博文大多是问答形式进行...
Jimmy_is_jimmy
2019/07/31
6160
再看JavaScript,那些遗漏或易混淆的知识点(4)
原型继承就是可以使一个对象可以使用另一个对象上面的某一些属性,要求是这个对象没有这个属性。如果有这个属性,就直接使用自己的了(访问器属性除外)。
踏浪
2021/12/24
3120
再看JavaScript,那些遗漏或易混淆的知识点(4)
相关推荐
js-函数的prototype
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验