android2024 gradle8 Processor和ksp两种编译时注解实现

android编译时注解,老生常谈,外面的例子都是bindView,脑壳看疼了,自己学习和编写下。
而且现在已经进化到kotlin2.0,google也逐渐放弃kapt,进入维护状态。所以要好好看看本贴。
参考我的工程:
https://github.com/jzlhll/AndroidComponts
ClassNameAnnotations
ClassNameAnnotations-compiler
ClassNameAnnotations-ksp
app
四个模块。

一、编写kapt(abstractProcessor)

1. 新建注解的模块,注意是java/kotlin library:

请添加图片描述
配置build.gradle:

plugins {
    id 'java-library'
    id 'kotlin'
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

添加自定义注解的java类:

@Retention(RetentionPolicy.CLASS)
@Target(value = ElementType.TYPE)
public @interface EntroFrgName {
}

这是我的需求,目的就是标记一个类,用来收集所有标注了注解的类,把他们收集成一个List。

2.再创建compiler模块,也是java/kotlin library:

请添加图片描述
得到2个模块。

2.1 gradle:
plugins {
    id 'java-library'
    id 'kotlin'
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

dependencies {
    implementation project(':ClassNameAnnotations')
}
2.2 配置解析器辅助文件:

这一步可以通过autoservice来配置。查看文章末尾注意事项。
在main下面reosurces/META-INF/services/目录下,创建文件javax.annotation.processing.Processor
里面写上com.au.learning.classnamecompiler.MyProcessor ,
就是下面代码MyProcessor 的类路径。

2.3 编写注解解析代码:

class MyProcessor : AbstractProcessor() {
    private var processingEnv:ProcessingEnvironment? = null

    override fun init(processingEnv: ProcessingEnvironment?) {
        super.init(processingEnv)
        this.processingEnv = processingEnv
        processingEnv?.messager?.printMessage(Diagnostic.Kind.WARNING, "init...!")
    }

    /**
     * 所支持的注解合集
     */
    override fun getSupportedAnnotationTypes(): MutableSet<String> {
        return mutableSetOf(EntroFrgName::class.java.canonicalName)
    }

    private fun isElementInAnnotations(target:Element, annotations: Set<TypeElement>) : Boolean {
        for (annotation in annotations) {
            //匹配注释
            if (target == annotation) {
                return true
            }
        }
        return false
    }

    //Element代表程序中的包名、类、方法。即注解所支持的作用类型。
    fun getMyElements(annotations: Set<TypeElement>, elements: Set<Element?>): Set<TypeElement> {
        val result: MutableSet<TypeElement> = HashSet()
        //遍历包含的 package class method
        for (element in elements) {
            //匹配 class or interface
            if (element is TypeElement) {
                for (annotationMirror in element.annotationMirrors) {
                    val found = isElementInAnnotations(annotationMirror.annotationType.asElement(), annotations)
                    if (found) {
                        result.add(element)
                        break
                    }
                }
            }
        }
        return result
    }

    /**
     * @param annotations 需要处理的注解 即getSupportedAnnotationTypes被系统解析得到的注解
     * @param roundEnv 注解处理器所需的环境,帮助进行解析注解。
     */
    override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment?): Boolean {
        val elements = roundEnv?.rootElements?.let {
            if (annotations != null) {
                getMyElements(annotations, it)
            } else {
                null
            }
        }

        val names = AllEntroFragmentNamesTemplate()
        if (!elements.isNullOrEmpty()) {
            for (e in elements) {
                names.insert(e.qualifiedName.toString())
            }

            val code = names.end()
            processingEnv.filer?.let {
                try {
                    // 创建一个JavaFileObject来表示要生成的文件
                    val sourceFile: JavaFileObject = it.createSourceFile("com.allan.androidlearning.EntroList", null)
                    sourceFile.openWriter().use { writer ->
                        // 写入Java(或Kotlin)代码
                        writer.write(code)
                        writer.flush()
                    }
                } catch (e: IOException) {
                    processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "Failed to generate file: " + e.message)
                }
            }
        }

        return true
    }

    //一定要修改这里,避免无法生效
    override fun getSupportedSourceVersion(): SourceVersion {
        return SourceVersion.latestSupported()
    }
}


class AllEntroFragmentNamesTemplate : AbsCodeTemplate() {
    private val insertCode = StringBuilder()

    /**
     * com.allan.androidlearning.activities.LiveDataFragment.class
     */
    fun insert(javaClass:String) {
        insertCode.append("list.add(").append(javaClass).append(".class);").appendLine()
    }

    fun end() : String {
        return codeTemplate.replace("//insert001", insertCode.toString())
    }

    override val codeTemplate = """
package com.allan.androidlearning;

import androidx.fragment.app.Fragment;

import java.util.ArrayList;
import java.util.List;

public class EntroList {
    public List<Class<? extends Fragment>> getEntroList() {
        List<Class<? extends Fragment>> list = new ArrayList<>();
        //insert001
        return list;
    }
}
    """.trimIndent()
}

这里有2个可以进一步学习的东西,一是auto库帮你生成META-INF文件。
二是通过javapoet来生成文件。详细在文章末尾注意事项。
本质上APT的目的就是将未知的代码,写成一个具体的类,被现有代码去调用,我自然可以直接写出这个类。所以,我为了方便和减少学习成本,自行整了一个模版代码(这个模版代码可以自己写好一个类,拷贝到string codeTemplate),把生成部分通过string.replace处理即可。然后简单地通过processingEnv.filer.createSourceFile,write就可以完成,自认为是一个不错的办法。

3. 主工程

剩下就简单了,app/build.gradle修改:

	plugins {
	    id 'com.android.application'
	    id 'org.jetbrains.kotlin.android'
	    id 'kotlin-kapt' //添加
	}
	...
    //注解引如
    implementation project(':ClassNameAnnotations')
    //kotlin
    kapt project(':ClassNameAnnotations-compiler')
    //java工程换成annotationProcessor 
    //annotationProcessor project(':ClassNameAnnotations-compiler')

给代码添加自己的注解了:

@EntroFrgName
class CanvasFragment : ViewFragment() {

@EntroFrgName
class DialogsFragment : ViewFragment() {

编译:
调试过程,可以选择gradle->Tasks->other->kaptDebugKotlin来编译。比直接编译更快,更单一。
编译结果在:
请添加图片描述
再最后,把这个类,拿去类似BuildConfig一样去调用了。至此已经完成。

二、app模块是java工程

自然是用不了ksp的。
唯一修改是app/build.gradle:

    //java工程换成annotationProcessor 
    annotationProcessor project(':ClassNameAnnotations-compiler')

然后各个gradle中,无需kotlin相关的痕迹。略。

三、KSP

终于谈到ksp了。
跟上面kapt一样,创建2个java/kotlin的模块。一个注解模块,一个处理模块,(那个灰色的compiler代表着settings.gradle已经不加载,不使用,不管它)。
请添加图片描述
注解模块的注解可以使用kotlin的注解类,也可以继续使用java的注解类。
区别只是在provider的解析代码上有一点点区别:

//EntroFrgName是java的注解类
resolver.getSymbolsWithAnnotation(EntroFrgName::class.java.canonicalName)
//EntroFrgName是kotlin的注解类
resolver.getSymbolsWithAnnotation(EntroFrgName::class.qualifiedName!!)
1. gradle:
根目录的build.gradle添加:
plugins {
    id 'com.android.application' version '8.4.2' apply false
    id 'com.android.library' version '8.4.2' apply false
    id 'org.jetbrains.kotlin.android' version "1.9.24" apply false
    id 'com.google.devtools.ksp' version '1.9.24-1.0.20' apply false
}
ksp模块的build.gradle为:
plugins {
    id 'java-library'
    id 'kotlin'
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

dependencies {
    implementation project(':ClassNameAnnotations')
    implementation('com.google.devtools.ksp:symbol-processing-api:1.9.24-1.0.20')
}

注意kotlin.android, devtools.ksp与symbol-processing-api三者的版本对应,查看https://github.com/google/ksp/releases。

2. 配置解析器辅助文件:

src/main/resources/META-INF/services/目录下:
com.google.devtools.ksp.processing.SymbolProcessorProvider 文件。写下如下的名字。
com.au.learning.classnamecompiler.AllEntroFrgNamesProvider。就是下面的类名。

3. provider解析代码:
class AllEntroFrgNamesProvider : SymbolProcessorProvider{
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return TestKspSymbolProcessor(environment)
    }
}

/**
 * creator: lt  2022/10/20  lt.dygzs@qq.com
 * effect : ksp处理程序
 * warning:
 */
class TestKspSymbolProcessor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor {
    // 使用一个集合来跟踪已经处理过的符号
    private val processedSymbols = mutableSetOf<KSDeclaration>()

    override fun process(resolver: Resolver): List<KSAnnotated> {
        environment.logger.warn("process start....")

        val symbols = resolver.getSymbolsWithAnnotation(EntroFrgName::class.java.canonicalName)
        val ret = mutableListOf<KSAnnotated>()

        val allEntroFragmentNamesTemplate = AllEntroFragmentNamesTemplate()
        var hasMy = false

        symbols.toList().forEach { symbol->
            if (!symbol.validate())
                ret.add(symbol)
            else {
                if (symbol is KSClassDeclaration && symbol.classKind == ClassKind.CLASS) {
                    val qualifiedClassName = symbol.qualifiedName?.asString()
                    allEntroFragmentNamesTemplate.insert(qualifiedClassName!!)
                    hasMy = true
//                    symbol.accept(TestKspVisitor(environment), Unit)//处理符号
                } else {
                    ret.add(symbol)
                }
            }
        }

        if (hasMy) {
            val code = allEntroFragmentNamesTemplate.end()

            // 生成文件
            val file = environment.codeGenerator.createNewFile(
                dependencies = Dependencies(false),
                packageName = "com.allan.androidlearning",
                fileName = "EntroList"
            )

            // 写入文件内容
            OutputStreamWriter(file).use { writer ->
                writer.write(code)
            }
        }

        //返回无法处理的符号
        return ret
    }
}
4. 主工程app引入

类似前面kapt的,主工程app/build.gradle

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'com.google.devtools.ksp'
}

   implementation project(':ClassNameAnnotations')
   ksp project(':ClassNameAnnotations-ksp')

添加注解,编译后,最终生成的代码在:
请添加图片描述

注意事项

1. 注意点

1.1 打印日志用warn。android studio编译是默认不打印低级别的。

//Processor
processingEnv?.messager?.printMessage(Diagnostic.Kind.WARNING, "init...!")
//ksp
environment.logger.warn("process start....")

1.2 kapt已经逐渐放弃,kt2.0开始不再努力维护kapt。尽量迁移ksp。更快更有支持。

1.3 很多人使用glide,经常把kapt,annotationProcessor,ksp搞混。
我们可以看到,glide库:
请添加图片描述
它也是有2个process的模块的,一个是给老的kapt或者java(annotationProcessor)处理。一个是给ksp。我们如出一辙。

2. 进一步学习

第一个:
使用autoservice来自动注解MyProcessor ,让它帮我们生成META-INF里面的文件。这个autoservice就干这么点点事情。compiler这个模块添加gradle(自己在这里看最新版本,https://github.com/google/auto):

 annotationProcessor 'com.google.auto.service:auto-service:1.11.0'
 implementation 'com.google.auto.service:auto-service-annotations:1.11.0'

然后给我们的Processor类添加上注解:

@AutoService(value = {Processor.class})

这纯属于是,我还没有编写完自己的注解, 就已经使用上别的注解来给我的注解模块生成文件了。[手动狗头]。

第二个,使用javapoet来实现生成代码。需要自行了解他的api和class,函数的结构。有点学习成本。

3. 坑了一天

出现一个问题,始终找不到原因。原来是
请添加图片描述
META-INF下面是目录services,再放一个文件。之前搞成了META-INF.services这个错误的目录!
而studio中显示的却跟包名一样。导致ksp的时候,搞了好久一直编译不过,提示[ksp] No providers found in processor classpath。好在有这句话,终于在ksp下解决了,之后反推到kapt也解决了。之前搞kapt,怎么都搞不好,也没有提示。

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-11 01:36:02       3 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-11 01:36:02       3 阅读
  3. 在Django里面运行非项目文件

    2024-07-11 01:36:02       2 阅读
  4. Python语言-面向对象

    2024-07-11 01:36:02       2 阅读

热门阅读

  1. SQL Server 设置端口详解

    2024-07-11 01:36:02       9 阅读
  2. MyBatis 框架核心及面试知识要点

    2024-07-11 01:36:02       9 阅读
  3. NLP - 基于bert预训练模型的文本多分类示例

    2024-07-11 01:36:02       10 阅读
  4. 二刷算法训练营Day57 | 动态规划(17/17)

    2024-07-11 01:36:02       7 阅读
  5. 自定义业务非受检异常

    2024-07-11 01:36:02       10 阅读
  6. GPT-5 一年半后发布?对此你有何期待?

    2024-07-11 01:36:02       8 阅读
  7. DSC主备归档报错

    2024-07-11 01:36:02       10 阅读