1.针对注解相关添加,分为注解对应的目标,以及执行的时机
| AnnotationTarget | 解释 |
|---|---|
| CLASS | 类、接口、对象声明 |
| ANNOTATION_CLASS | 注解类本身 |
| TYPE | 类型使用处(例如泛型、类型别名、函数返回类型等) |
| FIELD | 字段(包括 Java 字段) |
| PROPERTY | Kotlin 属性声明 |
| PROPERTY_GETTER | 属性 getter |
| PROPERTY_SETTER | 属性 setter |
| VALUE_PARAMETER | 方法参数 |
| CONSTRUCTOR | 构造方法 |
| FUNCTION | 函数、lambda |
| EXPRESSION | 表达式位置 |
| FILE | 文件级别 (@file:xxx) |
| LOCAL_VARIABLE | 局部变量 |
| AnnotationRetention | 存活范围 | 特点 |
|---|---|---|
| SOURCE | 只在源码中 | 编译后丢失,多用于编译检查、lint、ksp |
| BINARY | 保留到 class 文件,但运行时不可反射 | Kotlin 默认值。APT/KSP常用 |
| RUNTIME | class 文件 + 运行时可用反射读取 | 适用于路由、序列化、依赖注入等 |
例如在使用 ksp 生成ARouter 类时添加的注解信息如下:
@Target(AnnotationTarget.CLASS) //应用在类上
@Retention(AnnotationRetention.BINARY)
annotation class ARouter(val path: String, val group: String = "")
2.解析注解信息,需要添加的依赖
如果想要使用 ksp 需要添加如下依赖;首先module 必须支持 ksp;在 module 的build.gradle.kts中添加如下
plugins {
id("com.google.devtools.ksp")
}
在工程级的 build.gradle.kts 中添加如下
plugins {
id("com.google.devtools.ksp") version "2.0.21-1.0.28" apply false
}
这个地方需要注意:version 的版本信息;需要同你的 kotlin版本匹配;
如果你仅仅想要在 某个module 中使用 ksp,其他不使用,也可以在 module 下的 build.gradle.kts 中添加 版本信息;同时移除工程级 build.gradle中 ksp 的配置即可;
3.注解解析,获取注解内数据,注解类信息获取,注解元素信息获取;
想要通过 ksp 解析注解的信息,创建的module 属于kotlin/java library 别错了。同样需要添加如下依赖:
//用于生成类文件的依赖 kotlinpoet
implementation("com.squareup:kotlinpoet:1.16.0")
implementation("com.squareup:kotlinpoet-ksp:1.16.0")
//用于解析 annotation 的依赖
implementation("com.google.auto.service:auto-service-annotations:1.1.0")
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
implementation("com.google.devtools.ksp:symbol-processing-api:2.0.21-1.0.28")
同样也要添加支持 ksp 的 id 在 plugin 中;
创建ARouterProvider 这个是 ksp 解析注解的入口;ksp 采用增量模式;也就是会多次执行该类的create方法;代码如下:
@AutoService(SymbolProcessorProvider::class)
class ARouterProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return ARouterProcessor(environment)
}
}
注意:必须在类上添加 「@AutoService(SymbolProcessorProvider::class) 」 ,否则入口无效;「return ARouterProcessor(environment) 」为执行注解解析以及生成文件的类;基本功能如下:
@SupportedOptions(Constants.MODULE_NAME, Constants.PACKAGE_NAME_APT)
class ARouterProcessor(private val environment:
SymbolProcessorEnvironment) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
return emptylist()
}
override fun finish() {
super.finish()
}
}
解析注解,以及生成文件的整体是在process 方法中执行;如果这个地方同 kapt基本一致;只是对应的参数不同而已;
「@SupportedOptions(Constants.MODULE_NAME, Constants.PACKAGE_NAME_APT)」这个注解目的 用于从 module 中获取 ksp 的值;例如如下,在module.gradle.kts 定义参数;
ndroid
ksp {
arg("moduleName", project.name)
arg("packageNameForAPT", packageApt)
}
在ARouterProcessor 可以直接解析:
moduleName = environment.options[Constants.MODULE_NAME]
packageNameApt = environment.options[Constants.PACKAGE_NAME_APT]
4.解析注解主要描述类:
首先获取到添加类注解的全部数据信息;
val symbols = resolver.getSymbolsWithAnnotation(ARouter::class.java.name)
.filterIsInstance<KSClassDeclaration>()
.filter { it.validate() }
.toList()
注意:如果是注解添加到类上使用的是 「KSClassDeclaration」;如果是类中的元素就是
「KSPropertyDeclaration」了;
这个地方就获取到了添加注解的全部数据集合;通过遍历 symbols ;就可以获取到注解内部的字段信息;类信息;例如:
symbols.forEach {
//获取到指定的注解 当前是获取 ARouter的注解,有些类可能添加多个注解
val annotation = it.annotations.first {
it.shortName.asString() == Constants.ROUTER_NAME
}
//通过指定注解获取到注解内部的参数值;
var path =
annotation.arguments.firstOrNull { it.name?.asString() == Constants.ROUTER_PARAM_PATH }
?.value.toString()
it.qualifiedName?.asString()//全路径类名
it.simpleName.asString()//类名
it.classKind//类的类型 ,例如 接口类型,枚举类型
it.superTypes//父类
it.modifiers//修饰符 public
//以及等等
}
通过上述就可以获取到添加注解类的相关数据信息;这时你想搜集添加注解的类的信息,在这里就可以进行收集操作了;
5.如何生成对应的类
在 ksp 中为了更方便的生成类信息,一般依赖 kotlinpeot;如果你写的是 kapt 那么需要依赖 javapeot ;这个地方是有所区别的;javapoet 不支持 kotlin新的特性。例如 var,val ,集合等;
使用 kotlinpeot 需要添加 kotlinpeot 依赖;相关依赖在上方可以查看;
通过 kotlinpeot 生成类,我是参照 javapeot 习惯写的;方式如下 例如生成的代码是如下:
public class ARouter_Path_home : ARouterLoadPath {
public override fun loadPath(): MutableMap<String, ARouterPathBean> {
val pathMap = mutableMapOf<String,ARouterPathBean>()
pathMap["home"] = ARouterPathBean.create("activity", HomeActivity::class.java,
"/home/HomeActivity", "home")
return pathMap
}
}
1)先生成对应的返回值类型 MutableMap<String, ARouterPathBean>
2)创建方法public override fun loadPath(): MutableMap<String, ARouterPathBean>
3)创建方法内 局部变量 val pathMap = mutableMapOf<String,ARouterPathBean>()
4)进行集合赋值
5)添加返回值
6)创建类;
7)创建文件;
其实就是从类内部的方法开始执行创建任务;从内向外 执行创建;
返回值创建 MutableMap<String, ARouterPathBean>
val returnType = MUTABLE_MAP.parameterizedBy(STRING, ARouterPathBean::class.asClassName())
//这个MUTABLE_MAP MutableMap的ClassName;
//如果你的返回值是 String,可以直接使用 STRING;
//如果是自定义类型,可以通过 类::class.asClassName;或者ClassName(类路径,类名)
//ClassName.bestGuess(类全路径) 生成 ClassName
有些的返回值是这样的 MutableMap<String, Class> 对应的代码如下:WildcardTypeName可以生成 泛型中的 out 可以生成 In
MUTABLE_MAP.parameterizedBy(
STRING, Class::class.asClassName().parameterizedBy(
WildcardTypeName.producerOf(ClassName.bestGuess(Constants.PARAMS_PAGE_LOAD))
)
)
创建方法是通过FunSpec 进行创建,方法中局部变量,返回值等一切都需要通过FunSpec进行创建处理;
val method = FunSpec.builder(Constants.FUNCTION_LOAD_PATH)
.returns(returnType)//返回值 对应上述
.addModifiers(KModifier.PUBLIC)//修饰符 public
.addModifiers(KModifier.OVERRIDE) // ← 这里加 override 不属于注解了
集合创建
method.addStatement(
"val pathMap = mutableMapOf<%T,%T>()",
STRING, ARouterPathBean::class.asClassName()
)
| 占位符 | 意义 | 典型输入 | 输出示例 |
|---|---|---|---|
%T | 类型(TypeName, ClassName, KClass) | String::class, MyClass::class.java | kotlin.String, com.demo.MyClass |
%L | 字面量(literal) | 10, true, Enum.ACTIVITY.name, 变量名 | 10, true, ACTIVITY |
%S | 字符串(string literal) | "abc" | "abc"(自动加双引号) |
%% | 转义 % | % | % |
上述是对应 kotlinpoet 的占位符;这个地方注意 在javaPoet 中是用 「$」符号,kotlin 用的是「%」
集合添加数据:
pathMap.forEach { key, list ->
//将数据添加到集合中,先遍历集合
list.forEach {
// type: String?, aClass: Class<*>?, path: String?, group: String?
// %S 会生成 "null"(字符串 "null")%L 会生成 null(真正的 null)
/* *
* | 你以为 | 实际上 |
* | ------------------- | ---------------------------------------- |
* | `%L` 能直接接受 String? | KotlinPoet 会把 **整个实参视为 Any,而不是 literal** |
* | null 会自动变成 null | 传 String? 会变成 `"null"` |
* | addStatement 自动推断类型 | addStatement 参数必须完全符合 `%…` 规则 | **/
//生成数据添加到集合中
val type = it.type?.let { CodeBlock.of("%S", it) } ?: CodeBlock.of("null")
val path = it.path?.let { CodeBlock.of("%S", it) } ?: CodeBlock.of("null")
val group = it.group?.let { CodeBlock.of("%S", it) } ?: CodeBlock.of("null")
logger.warn("router >>>>> parsePath for map put $group ... path $path")
method.addStatement(
"pathMap[%L] = %T.create(%L, %T::class.java, %L, %L)",
group,
ARouterPathBean::class.asClassName(),
type,
it.ksDeclaration!!.toClassName(),
path,
group
)
}
//添加返回值
method.addStatement("return pathMap")
}
创建类
//创建文件信息,文件地址,文件名,
var className = Constants.FILE_PATH_NAME + key//类名
val typeSpec = TypeSpec.classBuilder(className)
.addFunction(method.build())//将方法添加到类中,可以添加多个方法
.addModifiers(KModifier.PUBLIC)//添加修饰符
//.addProperty()//添加属性值
.addSuperinterface(pathParent)//默认是接口,如果不清楚接口还是类,可以添加判断
//创建文件
//builder 第一个参数是 包名 ,第二个参数是类名
FileSpec.builder("$packageNameApt${Constants.FILE_PATH_MIDDLE}", className)
.addType(typeSpec.build())//添加类
.build()
//生成类文件
.writeTo(environment.codeGenerator, true, symbols.mapNotNull { it.containingFile })
上述就是创建模拟类的过程;同样还要注意在 process方法中添加如下代码;
override fun process(resolver: Resolver): List<KSAnnotated> {
//因为 ksp 是增量模式,其实就是多次加载;如果第一次没有加载完成,后续会再次加载,同时每次都会创建一个 process 因此定义的 flag 是没有用的
val symbolsARouter = resolver.getSymbolsWithAnnotation(ARouter::class.qualifiedName!!)
.filterIsInstance<KSClassDeclaration>()
if (symbolsARouter.any()) {
parseARouter(resolver)
}
}
目的:为了避免在执行增量时,出现频繁创建文件导致的无法编译问题;
如果需要创建类的属性变量可以参考如下:
//创建 集合信息 var pathMap = mutableMapOf<String, ARouterPathBean>()
// val mapType = ClassName("kotlin.collections", "MutableMap") 适用于 类的内部变量
var mapType = MutableMap::class.asClassName()
val valueType = ClassName("kotlin", "Class").parameterizedBy(STAR) // Class<*>
//这个地方是生产属性值
val pathMapProperty = PropertySpec.builder(
"pathMap",
mapType.parameterizedBy(String::class.asTypeName(), valueType)
)
.initializer("mutableMapOf()") // 这里直接调用 mutableMapOf()
.mutable(true)
.build()
通过类变量获取到对应的类信息代码如下:
private fun parseParams(resolver: Resolver, symbols: Sequence<KSPropertyDeclaration>) {
logger.warn("router >>>>> parseParams ")
var params = symbols.filter { it.validate() }
.toList()
logger.warn("router >>>>> parseParams size ${params.size}")
//获取到添加参数注解的全部信息
params.forEach {
//获取到注解中的信息
val annotation =
it.annotations.first { it.shortName.asString() == Constants.PARAMS_NAME }
//获取到别名
val alias =
annotation.arguments.firstOrNull { it.name?.asString() == Constants.PARAMS_PARAM_ALIAS }?.value.toString()
//判断是否是添加到了变量上
/* it.simpleName//名字
it.modifiers//修饰符
it.isMutable//是否是 var
it.qualifiedName?.asString() //这个是类全路径名+变量名
it.parentDeclaration as KSClassDeclaration //获取类的信息
(it.parentDeclaration as KSClassDeclaration).qualifiedName?.asString() 全类名*/
val classDeclaration = it.parentDeclaration as KSClassDeclaration
logger.warn(
"router >>>>> ${it.simpleName.asString()} " +
"... modifier ${it.modifiers}" +
" ... var ${it.isMutable} ... alias $alias" +
".... qualifiedName ${it.qualifiedName?.asString()}" +
" ... class ${classDeclaration.qualifiedName?.asString()} " +
"type ... ${it.type}"
)
//如果使用的修饰符 不是var 就进行提示
if (!it.isMutable) {
throw IllegalStateException("在注解参数的使用的修饰符 使用var")
}
//判断类是不是 Activity
if (!isActivityPage(resolver, classDeclaration.asStarProjectedType())) {
throw IllegalStateException("注解的 类必须是 Activity 的子类")
}
执行编译的命令如下:在 AS 的编译器中执行上述命令即可;
./gradlew assembleDebug
注意:如果想要打印日志ksp 提供的是 environment.logger 对应有info,warn,error 方法;
这里特别注意个坑。就是用 error打印日志,会导致编译不通过,并提示如下错误:
A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction
因为使用 error 打印日志,编译时就认为出现了问题;不会编译通过;这个地方和 kapt 日志打印不一样;info 的日志在编译时,有的时候无法展示;
ksp 使用中出现的问题:
1.需要 ksp 的版本和 kotlin 的版本对应,否之编译不通过;
2.在创建解析注解类的 module需要是java/kotlin library ,同时别忘记添加依赖
3.别忘记在 provider 上添加入口注解@AutoService(SymbolProcessorProvider::class)
4.获取到 module 中的 值; 同apt 不一致 需要设置 ksp{}
5.kotlin的重载表示 override 不再是添加注解的形式,而是修饰符;