Ksp创建类文件

18 阅读5分钟

1.针对注解相关添加,分为注解对应的目标,以及执行的时机

AnnotationTarget解释
CLASS类、接口、对象声明
ANNOTATION_CLASS注解类本身
TYPE类型使用处(例如泛型、类型别名、函数返回类型等)
FIELD字段(包括 Java 字段)
PROPERTYKotlin 属性声明
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常用
RUNTIMEclass 文件 + 运行时可用反射读取适用于路由、序列化、依赖注入等

例如在使用 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.javakotlin.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  不再是添加注解的形式,而是修饰符;