Java教程

注解处理器

本文主要是介绍注解处理器,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

本文代码可以在 giagor/AptGo - github 找到

研究的原因

最近在学习 Dagger 的时候,发现写几个注解然后编译,Dagger 就可以生成一些类给我们使用,感觉很神奇,所以就找了些资料学习一波。这种处理的技术被称作 Annotation Processing Tool(APT),即注解处理器。

处理注解有两种方法:

  1. 应用运行时通过反射获取注解的信息,对运行时的性能有损失,Retrofit 就是通过该方法处理注解。
  2. 通过 APT 在编译时获取并处理注解的信息,这种方法因为要在编译时通过 IO 生成额外的类,会导致编译时间变长,Dagger 使用的是这种方法。

注解处理器(APT):javac 的一种处理注解工具,用来在编译期扫描和处理注解,通过注解来生成 Java 文件,它只能生成新的源文件而不能修改已经存在的源文件通过这种方式,可以让我们编程中减少很多的代码,解放生产力

了解 Element

在实现注解处理器之前,需要先了解 java 中的一个概念 Element:它是一个接口,可以表示程序中的包、类、方法、字段等元素。Element 的子接口有下面这些

image-20220723213709479

PackageElement:表示包元素,提供对有关包及其成员的信息的访问。

TypeElement:表示一个类或者接口程序元素,提供对有关类型及其成员信息的访问。

TypeParameterElement:表示一个泛型元素。

VariableElement:表示一个字段、enum常量、方法或者构造器的参数、局部变量、资源变量或异常参数。

ExecutableElement:表示类或者接口的方法、构造器、初始代码块(静态或实例)。

......

Element 声明了下面的方法:

public interface Element extends javax.lang.model.AnnotatedConstruct {
    // 返回此元素定义的类型,实际的对象类型
	TypeMirror asType();
    // 获取 Element 的类型,判断是哪种 Element
    ElementKind getKind();
    // 获取修饰符,如 public static final 等关键字
    Set<Modifier> getModifiers();
    // 获取名字,不带包名
    Name getSimpleName();
    // 返回包含该节点的父节点,与 getEnclosedElements() 方法相反
    Element getEnclosingElement();
    // 返回该节点下直接包含的子节点,例如包节点下包含的类节点
    List<? extends Element> getEnclosedElements();
    
    @Override
    boolean equals(Object obj);
    
    @Override
    int hashCode(); 
    
    @Override
    List<? extends AnnotationMirror> getAnnotationMirrors();
    
    @Override
    <A extends Annotation> A getAnnotation(Class<A> annotationType);
    
    <R, P> R accept(ElementVisitor<R, P> v, P p);
}

自己实现一个 APT

功能:实现一个可以对类进行标注的注解,在编译的时候可以自动生成一个类,该类含有原来类的成员变量,并且对外提供 getset 方法。

注解模块

新建 annotation 模块:在 AS 中 File -> New -> New Module -> Java or Kotlin Library 语言选择 Kotlin。这个 Module 主要用来存放我们定义的注解。

annotation/build.gradle 中添加下面的依赖:

implementation 'androidx.annotation:annotation:1.2.0'

annotation 模块中创建一个注解:

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class ExtractField()

处理器模块

新建 compiler 模块:在 AS 中 File -> New -> New Module -> Java or Kotlin Library 语言选择 Kotlin。这个 Module 主要存放注解处理器,并且对注解处理器进行注册。

现在应用程序中有三个模块:

image-20220723220612884

compiler/build.gradle 中添加下面两个依赖:

// 我们需要对之前定义的注解进行处理
implementation project(path: ':annotation')
// 主要用于生成 kotlin 文件
implementation 'com.squareup:kotlinpoet:0.7.0'

创建一个 Processor 类对注解进行处理,该类继承至 AbstractProcessor

class Processor : AbstractProcessor() {...}

接着我们要将注解处理器注册到编译器当中,创建下面的目录:

image-20220723220950515

javax.annotation.processing.Processor 文件中写的是 Processor 的包名加类名,在这里就是:

com.example.compiler.Processor

这一步也可以采用 auto-service 这个库进行自动注册

接着主要是编写 Processor 这个类,分为下面几个步骤:

  1. 初始化工作:这里我会在本地初始化一个 log.txt 文件,用于记录一些日志信息,方便调试,实际开发中不需要这个。
  2. getSupportedAnnotationTypes():返回一个当前注解处理器所有支持的注解的集合。当前注解处理器需要处理哪种注解就加入哪种注解,如果类型符合,就会调用 process() 方法。
  3. getSupportedSourceVersion(): 需要通过哪个版本的 jdk 来进行编译。
  4. process:核心方法,在这里对注解进行处理,并生成相应的类。

日志文件的维护

这一部分非核心流程,不感兴趣的话,可以跳过

一开始是写在代码中使用 print 查看一些变量信息,但是没找到 print 的信息最终输出到了哪里,所以就想到了在电脑本地去创建一个文件,把想要知道的信息直接写到文件里就好了。

日志文件的路径(路径可以自行替换):

    companion object {
        ...
        // 日志文件的路径
        const val LOG_FILE_PATH = "D:\\AndroidStudioProjects\\demo\\log.txt"
    }

初始化时就创建好日志文件:

    override fun init(processingEnv: ProcessingEnvironment?) {
        super.init(processingEnv)
        // 创建日志文件
        createLogFile()
    }
	...
    // 创建日志文件
    private fun createLogFile() {
        kotlin.runCatching {
            val logFile = File(LOG_FILE_PATH)
            if (logFile.exists()) {
                logFile.delete()
            }
            logFile.createNewFile()
        }
    }

    // 把信息写到日志文件中,每写入一条信息,就换行一次
    private fun logInfo(vararg info: String) {
        kotlin.runCatching {
            val logFile = File(LOG_FILE_PATH)
            if (logFile.exists()) {
                info.forEach {
                    logFile.appendText(it)
                    logFile.appendText("\n")
                }
            }
        }
    }

接着在代码中就可以通过 logInfo("info1", "info2") 这样的形式记录调试信息,在电脑本地打开 log.txt 文件就可以方便查看输出的调试信息了。

后面发现,好像可以使用 processingEnv.messager.printMessage 来打印信息到 Build 窗口中

支持的注解及源码版本

    override fun getSupportedAnnotationTypes(): MutableSet<String> {
        return mutableSetOf(ExtractField::class.java.name)
    }

    override fun getSupportedSourceVersion(): SourceVersion {
        return SourceVersion.latestSupported()
    }

对注解进行处理

process 方法如下:

    override fun process(
        annotations: MutableSet<out TypeElement>?,
        roundEnv: RoundEnvironment
    ): Boolean {
        logInfo("Processor.process 方法被调用")
        val set = roundEnv.getElementsAnnotatedWith(ExtractField::class.java)
        if (set == null || set.isEmpty()) {
            return false
        }

        set.forEach { element ->
            if (element.kind != ElementKind.CLASS) {
                processingEnv.messager.printMessage(
                    Diagnostic.Kind.ERROR,
                    "Only classes can be annotated"
                )
                return@forEach
            }
            processAnnotation(element)
        }
        return true
    }

RoundEnvironment:表示当前或是之前的运行环境,可以通过该对象查找指定注解下的节点信息。

process 方法返回值:如果返回 true,则这些注解已处理,后续的「注解处理器」无需再处理它们;如果返回 false,则这些注解未处理并且可能要求后续「注解处理器」处理它们

父类 AbstractProcessor 提供了一个 processingEnv 实例,可以直接在我们定义的 Processor 里面使用,它提供注解处理的环境,代表了注解处理器框架提供的一个上下文环境,例如它可以提供下面的信息:

  • getMessager():返回一个消息器,其可以用于报告错误、警告、或者其它通知。例如原本我们的注解应该只能使用在类上,但是业务方却错误地将它使用在了方法上,就可以使用 Messager 在编译时给出错误信息。
  • getElementUtils():返回一个类,这个类具有可以操作 Element 的一些工具方法。

process 方法的逻辑:首先通过 RoundEnvironmentgetElementsAnnotatedWith 方法获取到被 ExtractField 注解的元素,若获取到的集合为空,则返回 false,否则调用 processAnnotation 方法对 Element 进行处理并返回 trueprocessAnnotation 方法如下:

    companion object {
        const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"
		...
    }   
	...
	private fun processAnnotation(element: Element) {
        // 获取元素类名、包名
        val className = element.simpleName.toString()
        val pack = processingEnv.elementUtils.getPackageOf(element).toString()
        // 生成类的类名
        val fileName = "ExtractField$className"
        // 表示一个 kotlin 文件,指定包名和类名
        val fileBuilder = FileSpec.builder(pack, fileName)
        // 表示要生成的类
        val classBuilder = TypeSpec.classBuilder(fileName)

        logInfo("className:$className", "pack:$pack", "fileName:$fileName")
        // 获取 Element 的子节点,只对字段进行处理
        for (childElement in element.enclosedElements) {
            if (childElement.kind == ElementKind.FIELD) {
                // 向类里添加字段
                addProperty(classBuilder, childElement)
                logInfo("FieldType:${childElement.asType().asTypeName().asNullable()}")
                // 向类里添加字段的 get 方法
                addGetFunc(classBuilder, childElement)
                // 向类里添加字段的 set 方法
                addSetFunc(classBuilder,childElement)
            }
        }
        // 向 fileBuilder 表示的 kotlin 文件中写入 classBuilder 类
        val file = fileBuilder.addType(classBuilder.build()).build()
        // 获取生成的文件所在的目录
        val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME]
        logInfo("kaptKotlinGeneratedDir:$kaptKotlinGeneratedDir")
        // 将 file 表示的文件写入目录 
        file.writeTo(File(kaptKotlinGeneratedDir))
    }

用到的辅助方法:

    // 往 classBuilder 代表的类中添加 element 字段,其中类型为可空的,初始值为 null  
    private fun addProperty(classBuilder: TypeSpec.Builder, element: Element) {
        classBuilder.addProperty(
            PropertySpec.varBuilder(
                element.simpleName.toString(),
                element.asType().asTypeName().asNullable(),
                KModifier.PRIVATE
            )
                .initializer("null")
                .build()
        )
    }

    // 往 classBuilder 代表的类中添加 element 的 getter 方法
    private fun addGetFunc(classBuilder: TypeSpec.Builder, element: Element) {
        classBuilder.addFunction(
            FunSpec.builder("getThe${element.simpleName}")
                .returns(element.asType().asTypeName().asNullable())
                .addStatement("return ${element.simpleName}")
                .build()
        )
    }

    // 往 classBuilder 代表的类中添加 element 的 setter 方法
    private fun addSetFunc(classBuilder: TypeSpec.Builder, element: Element) {
        classBuilder.addFunction(
            FunSpec.builder("setThe${element.simpleName}")
                .addParameter(
                    ParameterSpec.builder(
                        "${element.simpleName}",
                        element.asType().asTypeName().asNullable()
                    ).build()
                )
                .addStatement("this.${element.simpleName} = ${element.simpleName}")
                .build()
        )
    }

验证

app/build.gradle 中声明如下:

plugins {
	...
    id 'kotlin-kapt'
}
...
dependencies {
	...
    // 声明自己定义的注解处理器    
    kapt project(':compiler')
    // 代码中要使用自己定义到的注解
    implementation project(path: ':annotation')
}

app 模块中声明下面两个类:

@ExtractField
data class Rectangle(val length : Int, val width : Int)
@ExtractField
class Boy {
    val age : Int = 3
}

编译项目,接着就可以自动生成下面的两个类:

image-20220723232854885

ExtractFieldRectangle:

class ExtractFieldRectangle {
    private var length: Int? = null

    private var width: Int? = null

    fun getThelength(): Int? = length

    fun setThelength(length: Int?) {
        this.length = length
    }

    fun getThewidth(): Int? = width

    fun setThewidth(width: Int?) {
        this.width = width
    }
}

ExtractFieldBoy:

class ExtractFieldBoy {
    private var age: Int? = null

    fun getTheage(): Int? = age

    fun setTheage(age: Int?) {
        this.age = age
    }
}

顺便看下编译时生成的日志文件 log.txt

image-20220723233351762

编译之后就可以在代码中使用刚刚生成的类了:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val rectangle : ExtractFieldRectangle = ExtractFieldRectangle()
        rectangle.setThelength(20)
        rectangle.setThewidth(15)
        Log.d("abcde", "length:${rectangle.getThelength()},width:${rectangle.getThewidth()}")    

        val boy : ExtractFieldBoy = ExtractFieldBoy()
        boy.setTheage(18)
        Log.d("abcde", "age:${boy.getTheage()}")
    }
}

输出结果如下:

D/abcde: length:20,width:15
D/abcde: age:18

原理浅析

我们在代码中注解某些元素(如字段、函数、类等)后,在编译时编译器会检查 AbstractProcessor 的子类,调用其 process 方法,方法的参数是添加了该注解的所有代码元素,我们接着在 process 方法中根据注解元素在编译期输出对应的 Java 代码。

看看 java source -> dex 的过程:

image-20220727234332302

其中 JavaCompiler 参与的阶段再细分:

image-20220727234352139

注解处理器 处理的主要步骤:

  1. 在 java 编译器中构建
  2. 编译器开始执行未执行过的注解处理器
  3. 循环处理注解元素(Element),找到被该注解所修饰的类、方法、或者属性
  4. 生成对应的类,并写入文件
  5. 判断是否所有的注解处理器都已执行完毕,如果没有,继续下一个注解处理器的执行(回到步骤1)
image-20220730151002849

注解的处理是一轮一轮的。当编译到达预编译阶段时,第一轮开始,如果这一轮生成任何带有注解的新文件,则下一轮以生成的文件作为其输入开始。这种情况一直持续到处理器生成的新文件中不含有注解。

image-20220730152324002

kapt

使用 Java 开发 Android 应用,使用注解处理器的话是在 build.gradle 文件中使用 annotationProcessor 引入相关的注解处理器 。使用 Kotlin 开发 Android 应用时,要引入注解处理器就得使用 kaptkapt 也是 APT 工具的一种,使用 kapt 需要引入对应的 plugin

plugins {
    ...
    id 'kotlin-kapt'
}

前面 app 模块引用注解处理器的 compiler 模块就是使用了 kapt

参考

  1. Idiomatic Kotlin: Annotation Processor and Code Generation - Tompee Balauag。
  2. Android 注解处理器 - 简书。
  3. Android注解处理器APT技术探究 - 掘金。
  4. 含有调试「注解处理器」的介绍:Kotlin版注解处理器Annotation Processor - 掘金。
这篇关于注解处理器的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!