【Android】APT与JavaPoet学习与实战

PS:本文讲解的APT全称为Annotation Processing Tool,而非是Android Performance Tuner,这两种工具简称皆为APT,前者是“注释处理工具”,后者是“Android性能调试器”。

本文分别使用Javakotlin 语言进行开发,代码已开源 APT-Demo

目录

  • APT
  • 仿ButterKnife组件绑定
    • 创建类
    • 注册类
      • SPI
      • AutoService
  • JavaPoet
  • 最终效果

APT

APT全称为Annotation Processing Tool,是Javac(Java编译器)的一个工具。通过APT,在Javac编译代码时可以通过注解拿到被注解的对象信息,根据需求自动生成模板代码。

PS:APT获取注解和生成代码都是在代码编译时期完成的。

APTJavac 的一个工具,使用的时候在 build.gradle 引入 java-library 插件,使用 java-library 插件的 Module 不应再使用与android相关的插件,避免报The 'java' plugin has been applied, but it is not compatible with the Android plugins.的兼容错误,这也就是为啥建个package就能解决的事情,非得建一个Module的原因。

plugins {
    id 'java-library'
}

仿ButterKnife组件绑定

创建类

  • BindView
    注解类,用于绑定的的xml资源id的存、取。
@Retention(RetentionPolicy.CLASS)
@Target(AnnotationTarget.FIELD)
annotation class BindView(val value: Int)

@Retention(RetentionPolicy.CLASS):表示这个注解保留到编译期
@Target(ElementType.FIELD):表示注解范围为类成员(构造方法、方法、成员变量)

  • AutoBind
    获取需要生成类的类名,拼接成使用APT生成的类文件名称,通过反射机制传值给APT生成类的inject方法。
object AutoBind {
    fun inject(target: Any) {
        val className = target.javaClass.canonicalName
        val helperName = "$className$\$Processor"
        try {
            val helper = Class.forName(helperName).getConstructor().newInstance() as IBindHelper
            helper.inject(target)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}
  • BindViewProcessor
    用于生成模板类及代码
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.binyouwei.lib_apt.annotation.BindView")
class BindViewProcessor : AbstractProcessor() {
    // 存储绑定的组件信息
    private val mBindActivity: MutableMap<TypeElement, MutableSet<ViewInfo>> = hashMapOf()
    // 处理工具类
    private var mElement: Elements? = null
    // 文件管理工作类
    private var mFiler: Filer? = null
    private var mTypeUtils : Types ? = null
    override fun init(processingEnv: ProcessingEnvironment?) {
        super.init(processingEnv)
        processingEnv?.apply {
            mElement = elementUtils
            mFiler = filer
            mTypeUtils = typeUtils
        }
    }
    override fun process(
        annotations: MutableSet<out TypeElement>?,
        roundEnv: RoundEnvironment?,
    ): Boolean {
        if (annotations != null && annotations.size != 0) {
            val elementsAnnotated = roundEnv?.getElementsAnnotatedWith(BindView::class.java)
            elementsAnnotated?.forEach {
                // VariableElement用于表示字段、枚举常量、方法或构造函数参数、局部变量、资源变量或异常参数。
                val variableElement = it as VariableElement
                // TypeElement表示类或接口程序元素
                val typeElement = variableElement.enclosingElement as TypeElement
                // 将viewInfos实例化
                var viewInfos = mBindActivity[typeElement]
                if (viewInfos == null) {
                    viewInfos = hashSetOf()
                    mBindActivity[typeElement] = viewInfos
                }
                // 拿到注解的值,这里的值是view xml布局的id
                val annotation = variableElement.getAnnotation(BindView::class.java)
                val xmlId = annotation.value
                viewInfos.add(ViewInfo(variableElement.simpleName.toString(), xmlId))
            }
            // 生成不同的Activity类,生成路径为...
            mBindActivity.keys.forEach {
                // 获取要绑定的view名称
                val className = it.simpleName.toString()
                // 获取绑定View所在类的包名
                val packageElement = mElement?.getPackageOf(it) as PackageElement
                val packageName = packageElement.qualifiedName.toString()
                // 生成的类的名字
                val generateCalssName = "$className$\$Processor"
                // 开始编写模板代码
                val code = StringBuilder()
                code.append("package $packageName;\n")
                code.append("import com.binyouwei.lib_apt.template.IBindHelper;\n")
                code.append("public class $generateCalssName implements IBindHelper {\n")
                code.append("\t@Override\n")
                code.append("\tpublic void inject(Object target) {\n")
                code.append("\t\t$className obj = ($className)target;\n")
                mBindActivity[it]?.forEach { viewInfo ->
                    viewInfo.viewName?.run {
                        val initView = "\t\tobj.set${substring(0, 1).uppercase()}${substring(1)}(obj.findViewById(${viewInfo.id}));\n"
                        code.append(initView)
                    }
                }
                code.append("\t}\n")
                code.append("}")
                val createSourceFile = mFiler?.createSourceFile(generateCalssName, it)
                val writer = createSourceFile?.openWriter()
                writer?.apply {
                    write(code.toString())
                    flush()
                    close()
                }
            }
            return true
        }
        return false
    }
    class ViewInfo(var viewName: String, var id: Int)
}
  • IBindHelper
    用于AutoBind类调用inject方法给APT生成的Processor类传递类名。
interface IBindHelper {
    fun inject(target: Any?)
}

注册类

注册自动生成代码的Processor类有两种方式,分别是SPIAutoService

SPI

SPI全称Service Provider Interface,是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件。

在这里,使用SPI来启用BindViewProcessor扩展类。由于SPI机制约定通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类,因此需要先创建指定文件夹。

Android StudioTerminal打开项目的模块main文件夹,输入命令行 mkdir -p resources/META-INF/services或手动在项目 main 目录下创建resources/META-INF/services文件夹。

【Android】APT与JavaPoet学习与实战_第1张图片
紧接着创建一个名为javax.annotation.processing.Processor的文件,并在文件里输入继承AbstractProcessor类的类路径(含包名和类名),在我这里填的就是com.binyouwei.lib_apt.processor.BindViewProcessor,文件目录如下:

【Android】APT与JavaPoet学习与实战_第2张图片
完成上述步骤,执行Build菜单栏下的Rebuild Project,就能够在app模块的项目路径\APT\app\build\generated\source\kapt\debug目录下生成一个BindViewProcessor类编写的模板类。

【Android】APT与JavaPoet学习与实战_第3张图片

如果嫌操作步骤麻烦,这一步可以使用Google提供的AutoService的注解代替,更方便快捷。

Android项目中,自定义注解需要引入注解处理器,如果在 Java 文件中使用自定义注解,需要使用 annotationProcessor 引入项目,若在Kotlin 文件中使用自定义注解,则添加 kapt(全称Kotlin Annotation Processing Tool) 插件,并使用 kapt 引入依赖。

apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'

但近期 JetBrains 官方发布的教程显示,kapt已经进入维护模式,且无开发 kapt 相关功能的计划,推荐我们使用 KSP(Kotlin Symbol Processing API),官方理由则是:与 kapt 相比,使用 KSP 的注释处理器的运行速度最多可以提高 2 倍。

SPI 相关文章 Java SPI (Service Provider Interface) 机制详解

AutoService

AutoService 是一个 java.util.ServiceLoader 风格服务提供程序的配置/元数据生成器,可用于替换 SPI 来注册类。

代替SPI注册类只有两个步骤:

  1. build.gradle添加依赖包。
apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'

dependencies {
    implementation 'com.google.auto.service:auto-service:1.0-rc7'
    kapt 'com.google.auto.service:auto-service:1.0-rc7'
}
  1. 使用@AutoService(Processor.class)注解绑定 BindViewProcessor
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.apt_annotation.BindView")
class BindViewProcessor : AbstractProcessor() {
	...
}

完成上述步骤并编译即可在项目所在路径\APT\app\build\generated\source\kapt\debug生成一个类,这个类里面生成的代码即模板代码。

感兴趣可参考以下文章
1、AutoService介绍
2、AutoService历史版本

PS:如果使用 SPIAutoService 没有正常生成类,都会导致 AutoBind 类报异常。

JavaPoet

JavaPoet 是一个用于生成 .Java 源文件的 Java API

JavaPoet 相关类:

描述
ParameterSpec 用于生成参数声明
FieldSpec 用于生成的字段声明
AnnotationSpec 在声明上生成的注释
JavaFile 包含单个顶级类的Java文件
ClassName 顶级类和成员类的完全限定类名
MethodSpec 用于生成的构造函数或方法声明
TypeSpec 用于生成的类、接口或枚举声明
CodeWriter JavaFile 类转换为既适合人工使用又适合javac使用的字符串。
NameAllocator 指定Java标识符名称以避免冲突、关键字和无效字符。要使用,请首先创建一个实例并分配所需的所有名称。通常是用户提供的名称和常量的混合。

JavaPoet的使用是蛮简单的,就这几个类,在它的 Github 也举了简单的例子,对着看就能知道自己应该如何根据自己的需求编写代码。

上面我们使用字符串拼接的方式实现类代码的编写,使用 javapoet 进行修改,只需要使用以下代码替换字符串拼接部分即可。

// 编写方法
val method = MethodSpec.methodBuilder("inject")
    .addModifiers(Modifier.PUBLIC)
    .returns(Void.TYPE)
    // 给inject方法添加Override注解
    .addAnnotation(Override::class.java)
    // 给inject方法添加Object类型的target参数
    .addParameter(Object::class.java, "target")
mBindActivity[it]?.forEach { viewInfo ->
    // 循环生成findviewbyId代码
    method.addStatement("$className obj = ($className)target;")
        .addStatement("obj.setText(obj.findViewById(${viewInfo.id}));")
}
// 编写类
val generateCalss = TypeSpec.classBuilder(generateCalssName)
     // 添加对类的修饰
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
     // 为类添加需要实现的接口
    .addSuperinterface(IBindHelper::class.java)
     // 为类添加方法
    .addMethod(method.build())
    .build()
// 新增一个文件并写入类
val javaFile = JavaFile.builder(packageName, generateCalss)
    .build()
javaFile.writeTo(mFiler)

使用 kapt 的编写的代码所生成的代码路径是和 java 版本的是不一样的,本示例代码生成的类路径如下:项目路径APTJava\app\build\generated\ap_generated_sources\debug\out\com\binyouwei\apt_java\MainActivity$$Autobind.java
【Android】APT与JavaPoet学习与实战_第4张图片

更多 JavaPoet 详情见 javapoet

最终效果

使用示例:

class MainActivity : AppCompatActivity() {
    @BindView(value = R.id.textView)
    var text: TextView ?=null
    @BindView(value = R.id.Id)
    var Id: TextView ?=null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        AutoBind.inject(this)
        text?.text = "APT测试成功"
    }
}

使用 APT 前后:

【Android】APT与JavaPoet学习与实战_第5张图片 【Android】APT与JavaPoet学习与实战_第6张图片

前往下载 KotlinJava 版代码 APT-Demo

参考文档
1、Android APT快速教程

你可能感兴趣的:(Android,android,kotlin,java,apt,JavaPoet)