android的编译时注解,老生常谈,外面的例子都是bindView,脑壳看疼了,自己学习和编写下。
而且现在已经进化到kotlin2.0了,google也逐渐放弃kapt,进入维护状态。所以要好好看看本贴。
参考我的工程:
https://github.com/jzlhll/AndroidComponts
ClassNameAnnotations
ClassNameAnnotations-compiler
ClassNameAnnotations-ksp
app
四个模块参考。
一、kotlin项目+kapt
1. 新建注解的单独模块,注意是java/kotlin library:
配置gradle为:
plugins {
id 'java-library'
id 'kotlin'
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
添加自定义注解类:
@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来生成文件。详细在注意事项里面。
我这里就为了方便,也减少学习成本,自行整一个模版代码(这个模版代码可以自己写好一个类,拷贝到string codeTemplate),把生成部分通过string.replace进行不就好了?
然后简单的通过processingEnv.filer.createSourceFile,write了就好了。我自认为是一个不错的办法。
3. 主工程
剩下就简单了,app/build.gradle修改:
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
//id 'com.google.devtools.ksp'
id 'kotlin-kapt' //添加
}
...
//my apt
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一样去引用了。
二、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!!)
gradle:
根目录的build.gradle添加:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
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')
}
多引入了symbol-processing-api,注意kotlin.android, devtools.ksp与之相同对应,查看https://github.com/google/ksp/releases。
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
}
}
主工程app引入
类似前面kapt的,主工程app/build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.devtools.ksp'
}
//my apt
implementation project(':ClassNameAnnotations')
ksp project(':ClassNameAnnotations-ksp')
申明ksp和dependencies部分的引入即可,使用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已经开始不再维护kapt了。
1.3 很多人把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,再放一个文件。
而studio中显示的是跟包名一样。导致ksp的时候,搞了好久一直编译不过,提示[ksp] No providers found in processor classpath。好在有这句话,终于在ksp下解决了。之前搞kapt,怎么都搞不好。也没有提示。最后基于这个目录问题,总算2个都编译通过。