gradle编译时注解增量教程

Posted 不会写代码的丝丽

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gradle编译时注解增量教程相关的知识,希望对你有一定的参考价值。

前言

gradle增量注解官方指南

读者需要对apt有一定了解否则可能看不懂,这里提供两篇博主的文章:
注解处理器常用类说明
Java Pluginable Annotation processing

增量注解的定义和作用:
Gradle 4.7开始,gradle提供了增量apt,可以使上层开发者更快的编译.
笔者使用kapt为例,我们使用apt生成的来会放入/app/build/generated/source/kapt文件夹下:

博主写了一个apt,回使每个被@MyAnnotation注释的类生成一个对应的新java文件.

@MyAnnotation()
public class MyFour {
    int i1 =2;
}

@MyAnnotation()
public class MyTwo {
    int i1 =2;
}

@MyAnnotation()
public class MyThree {
    int i1 =2;
}

/app/build/generated/source/kapt文件的生成文件:
在这里插入图片描述

当我们取消MyFour的注解如下所示,再次编译

//@MyAnnotation()
public class MyFour {
    int i1 =2;
}

@MyAnnotation()
public class MyTwo {
    int i1 =2;
}

@MyAnnotation()
public class MyThree {
    int i1 =2;
}

你会发现/app/build/generated/source/kapt整个文件夹都会被删除,
在这里插入图片描述
在大型工程时这是一种不可接受的现象,每次改动相关代码会带来大量的编译时间消耗.这对这个问题我们希望之前的文件夹内容仅改动变化,而非重建删除.

这对上诉问题Gradle提供了相关解决方案,只需要注解开发者提供一些基础信息给Gradle即可.

Gradle 增量注解教程

Gradle增量注解处理器分为两种isolating,aggregating.你可以在resources/META-INF/gradle/incremental.annotation.processors下进行声明你属于那种增量处理器.

当然有时候我们只能在运行时确定自己是哪种增量处理器,或者动态开启增量,这种类别的我们称为dynamic.它同样需要resources/META-INF/gradle/incremental.annotation.processors下进行声明.

resources/META-INF/gradle/incremental.annotation.processors声明的语法如下:

注解处理器的全限定名,类别
注解处理器的全限定名2,类别

如:

org.gradle.EntityProcessor,isolating
org.gradle.EntityProcessor,aggregating
org.gradle.ServiceRegistryProcessor,dynamic

不管是哪种他们都有如下的限制:

  • 只能使用Filer API生成新的文件

  • 不能依赖于特定编译器API,比如说com.sun.source.util.Trees.(这个api一般用于修改输入的文件,在实际情况下我们都是新生成一个文件,而不是修改.可以参阅博主其他文章:Java Pluginable Annotation processing)

  • 如果你使用了 Filer#createResource这个api.(用于生成新的资源文件,比如xxx.xml,其中有一个参数location用确定新生成的文件放入在哪里),那么你的location参数仅能是CLASS_OUTPUT, SOURCE_OUTPUT, NATIVE_HEADER_OUTPUT. 在大多数情况我们都只会用这几个位置,除非你有特殊要求

class Filer{
	FileObject createResource(JavaFileManager.Location location,
                          CharSequence pkg,
                          CharSequence relativeName,
                          Element... originatingElements)
}

这里我相信有很多人看完一脸懵逼,大多数原因如下:

  1. 很多人直接使用javapoet库所以对Filer知道的并不多.(需要读者自行翻阅下doc文档,大致对api有一个了解)
  2. 没使用过apt修改过现有文件(比如生成set和get方法),而只是新建一个文件.(可参阅:Java Pluginable Annotation processing)
  3. 没使用apt生成过资源文件.(比较简单,也比较少用)

下面是一个直接使用Filer生成一个新的类的案例,希望对大家有帮助

class MyAnnotationCompiler : AbstractProcessor() {

    private var messager: Messager? = null
    private var elementUtils: Elements? = null
    private var typeUtils: Types? = null
    private var filer: Filer? = null

	override fun init(processor: ProcessingEnvironment) {


        super.init(processor)

        messager = processor.messager
        elementUtils = processor.elementUtils
        typeUtils = processor.typeUtils
        filer = processor.filer
    }
    override fun process(
        set: MutableSet<out TypeElement>,
        roundEnvironment: RoundEnvironment
    ): Boolean {
        if (set.isEmpty()) {
            return false
        }

        val elementsAnnotatedWith =
            roundEnvironment.getElementsAnnotatedWith(MyAnnotation::class.java)


        for (element in elementsAnnotatedWith) {

            val packageElement = elementUtils?.getPackageOf(element)!!

            val packageString = packageElement.qualifiedName.toString()

            val className = """${element.simpleName.toString()}Hello"""

            val createSourceFile =
            	//注意createSourceFile有两个参数,后面一个是可选参数,我们这里不传
            	//后面一个参数可用于增量构建,后文讲解
                filer?.createSourceFile(
                    "$packageString.$className"               
                )

            val writer = createSourceFile?.openWriter()


            writer?.apply {
                write("package ${packageElement};")
                write("\\n")
                write("class ${className}{}")
                close()
            }
         }
  	}
}            

isolating注解处理器

相对aggregating,dynamic这是最快的一种增量注解处理器,也是最容易理解的一个.

我们看下的这个类别特有的限制:

限制1. isolating要求每一个注解处理器仅能使用一个相关的编译时注解去生成新的文件,简单来说你不能用两个以上的注解去生成一个新文件

//注解 1
@MyAnnotation()
public class MyFour {
    int i1 =2;
}
//注解 2
@MyAnnotation2
class Test{

}

下面使用了两个注解生成一个文件,这种情况下是不能注册为isolating注解处理器.如果你需要这种情况请使用aggregating类别.

override fun process(
        set: MutableSet<out TypeElement>,
        roundEnvironment: RoundEnvironment
    ): Boolean {
        if (set.isEmpty()) {
            return false
        }
		//使用注解1
        val elementsAnnotatedWith =
            roundEnvironment.getElementsAnnotatedWith(MyAnnotation::class.java)
		//使用注解2
        val elementsAnnotated2 =
            roundEnvironment.getElementsAnnotatedWith(MyAnnotation2::class.java).first()
        val secondName2 = elementsAnnotated2.simpleName

		//结合两个注解一起生成类
        for (element in elementsAnnotatedWith) {

            val packageElement = elementUtils?.getPackageOf(element)!!

            val packageString = packageElement.qualifiedName.toString()
			//使用两个注解一起生成类文件夹名
            val className = """${element.simpleName.toString()}Hello${secondName2}"""

            val createSourceFile =
                filer?.createSourceFile(
                    "$packageString.$className",
                )

            val writer = createSourceFile?.openWriter()


            writer?.apply {
                write("package ${packageElement};")
                write("\\n")
                write("class ${className}{}")
                close()
            }


        }

        return true
    }

    override fun init(processor: ProcessingEnvironment) {
        super.init(processor)
        messager = processor.messager
        elementUtils = processor.elementUtils
        typeUtils = processor.typeUtils
        filer = processor.filer
    }

   
}
  • 限制2. 对于每个用filer生成的文件,必须要在构造时传入一个 originating element与其对应.

下面便是一个完全符合isolating规范的注解处理器.
添加一个声明到resources/META-INF/gradle/incremental.annotation.processors:

#incremental.annotation.processors
com.example.annotationcompiler.MyAnnotationCompiler,isolating
class MyAnnotationCompiler : AbstractProcessor() {

    private var messager: Messager? = null
    private var elementUtils: Elements? = null
    private var typeUtils: Types? = null
    private var filer: Filer? = null


    override fun process(
        set: MutableSet<out TypeElement>,
        roundEnvironment: RoundEnvironment
    ): Boolean {
        if (set.isEmpty()) {
            return false
        }

        val elementsAnnotatedWith =
            roundEnvironment.getElementsAnnotatedWith(MyAnnotation::class.java)


        for (element in elementsAnnotatedWith) {

            val packageElement = elementUtils?.getPackageOf(element)!!

            val packageString = packageElement.qualifiedName.toString()

            val className = """${element.simpleName.toString()}Hello"""

            val createSourceFile =
                //注意看第二个参数传入了一个element,并且只能传入一个
                filer?.createSourceFile(
                    "$packageString.$className",
                    element
                )

            val writer = createSourceFile?.openWriter()


            writer?.apply {
                write("package ${packageElement};")
                write("\\n")
                write("class ${className}{}")
                close()
            }


        }

        return true
    }

    override fun init(processor: ProcessingEnvironment) {
        super.init(processor)

        messager = processor.messager
        elementUtils = processor.elementUtils
        typeUtils = processor.typeUtils
        filer = processor.filer
    }

   
}

对于使用javapoet的同学可以看下面这个案例:

class MyAnnotationCompiler : AbstractProcessor() {

    private var messager: Messager? = null
    private var elementUtils: Elements? = null
    private var typeUtils: Types? = null
    private var filer: Filer? = null


    override fun process(
        set: MutableSet<out TypeElement>,
        roundEnvironment: RoundEnvironment
    ): Boolean {
        if (set.isEmpty()) {
            return false
        }

        val elementsAnnotatedWith =
            roundEnvironment.getElementsAnnotatedWith(MyAnnotation::class.java)


        for (element in elementsAnnotatedWith) {

            val packageElement = elementUtils?.getPackageOf(element)!!

            val packageString = packageElement.qualifiedName.toString()

            val className = """${element.simpleName.toString()}Hello"""


            val main = MethodSpec.methodBuilder("main")
                .addModifiers(Modifier.PUBLIC)
                .returns(Void.TYPE)
                .addParameter(Array<String>::class.java, "args")
                .addStatement("\\$T.out.println(\\$S)", System::class.java, "Hello, JavaPoet!")
                .build()

            val helloWorld = TypeSpec.classBuilder(className)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(main)
                //传入一个原始的element 才能变成增量
                .addOriginatingElement(element)
                .build()

            val javaFile = JavaFile.builder(packageString, helloWorld)
                .build()

            javaFile.writeTo(filer)

        }

        return true
    }

   
}

aggregating注解处理器

我们看到上面有一个问题isolating,不能使用多个注解生成一个文件,而aggregating是允许的.但是增量成功率和效率比不上isolating.

aggregating限制如下:

  • 限制1:注解的Retention必须是CLASS or RUNTIME

@Target({ElementType.TYPE})
//这是ok的
@Retention(RetentionPolicy.CLASS)
public @interface MyT {
}

@Target({ElementType.TYPE})
//可以的
@Retention(RetentionPolicy.RUNTIME)
public @interface MyT {
}

@Target({ElementType.TYPE})
//错误的
@Retention(RetentionPolicy.SOURCE)
public @interface MyT {
}

  • 限制2:如果用户传递-parameters编译器参数,则它们只能读取参数名称。(不理解,估计也不常用)

我们看一个合格的一个Demo

@Target(allowedTargets = [AnnotationTarget.CLASS, AnnotationTarget.FIELD, AnnotationTarget.FUNCTION])
@Retention(AnnotationRetention.BINARY)//等价class
annotation class MyAnnotation(val value: Int = 20, val value2: Int = 40) {

}
@Target(allowedTargets = [AnnotationTarget.CLASS, AnnotationTarget.FIELD, AnnotationTarget.FUNCTION])
@Retention(AnnotationRetention.BINARY)//等价class
annotation class MyAnnotation2(val value: Int = 20, val value2: Int = 40) {

}

添加一个声明到resources/META-INF/gradle/incremental.annotation.processors:

#incremental.annotation.processors
com.example.annotationcompiler.MyAnnotationCompiler,aggregating
class MyAnnotationCompiler : AbstractProcessor() {

    private var messager: Messager? = null
    private var elementUtils: Elements? = null
    private var typeUtils: Types? = null
    private var filer: Filer? = null


    override fun process(
        set: MutableSet<out TypeElement>,
        roundEnvironment: RoundEnvironment
    ): Boolean {
        if (set.isEmpty()) {
            return false
        }
		//注解1
        val elementsAnnotatedWith =
            roundEnvironment.getElementsAnnotatedWith(MyAnnotation::class.java)
		//注解2
        val elementsAnnotated2 =
            roundEnvironment.getElementsAnnotatedWith(MyAnnotation2::class.java).first()
        val secondName2 = elementsAnnotated2.simpleName


        for (element in elementsAnnotatedWith) {

            val packageElement = elementUtils?.getPackageOf(element)!!

            val packageString = packageElement.qualifiedName.toString()

            val className = """${element.simpleName.toString()}Hello${secondName2}"""

            val createSourceFile =
                filer?.createSourceFile(
                    "$packageString.$className"

                )

            val writer = createSourceFile?.openWriter()


            writer?.apply {
                write("package ${packageElement};")
                write("\\n")
                write("class ${className}{}")
                close()
            }


        }

        return true
    }

    override fun init(processor: ProcessingEnvironment) {


        super.init(processor)

        messager = processor.messager
        elementUtils = processor.elementUtils
        typeUtils = processor.typeUtils
        filer = processor.filer
      
    }

}

dynamic注解处理器

假设我们有一个需求,需要用户自己决定是否开启增量注解,那么dynamic就很适合.

kapt {
  useBuildCache = false
  
  arguments {
  	//将这个选项传递到注解处理器中
    arg("enableIncremental", "true")
  }
}

dynamic要求我们重写AbstractProcessor.getSupportedOptions函数,然后根据自身情况返回是哪一种增量注解处理器类型.
直接看一个Demo:

class MyAnnotationCompiler : AbstractProcessor() {

    private var messager: Messager? = null
    private var elementUtils: Elements? = null
    private var typeUtils: Types? = null
    private var filer: Filer? = null


    override fun process(
        set: MutableSet<out TypeElement>,
        roundEnvironment: RoundEnvironment
    ): Boolean {
        if (set.isEmpty()||set.size!=2) {
            return false
        }

        val elementsAnnotatedWith =
            roundEnvironment.getElementsAnnotatedWith(MyAnnotation::class.java)

        val elementsAnnotated2 =
            roundEnvironment.getElementsAnnotatedWith(MyAnnotation2::class.java).first()
        val secondName2 = elementsAnnotated2.simpleName


        for (element in elementsAnnotatedWith) {

            val packageElement = elementUtils?.getPackageOf(element)!!

            val packageString = packageElement.qualifiedName.toString()

            val className = """${element.simpleName.toString()}Hello${secondName2}"""

            val createSourceFile =
                filer?.createSourceFile(
                    "$packageString.$className"

                )

            val writer = createSourceFile?.openWriter()


            writer?.apply {
                write("package ${packageElement};")
                write("\\n")
                write("class ${className}{}")
                close()
            }

        }

        return true
    }

    val enableIncrementalOption = "enableIncremental"
    override fun init(processor: ProcessingEnvironment) {


        super.init(processor)

        messager = processor.messager
        elementUtils = processor.elementUtils
        typeUtils = processor.typeUtils
        filer = processor.filer
        //init优先调用,然后才调用getSupportedOptions
        val enableIncremental = processor.options[enableIncrementalOption]
        if ("true" == enableIncremental) {
            //根据你的注解处理器选择是哪种
            set.add("org.gradle.annotation.processing.aggregating")
//            set.add("org.gradle.annotation.processing.isolating")

        }
    }

    var set = mutableSetOf<String>(enableIncrementalOption)

    override fun getSupportedOptions(): MutableSet<String> {

        return set
    }
}

小技巧

在你的gradle.properties中加入以下属性:

//输出katp相关信息
kapt.verbose=true
#利用jdwp调试注解处理器
org.gradle.jvmargs= -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000

另外gradle自身有一个小bug,当你发现你的增量失效时,去删除使用处理器的build文件夹.
于是本文提了一个issue给gradle官方gradle issue

以上是关于gradle编译时注解增量教程的主要内容,如果未能解决你的问题,请参考以下文章

使用@Grab 注解编译 Groovy 项目时出错

没有可用的增量编译快照数据 gradle 错误

史上最快最强大的Gradle 5.0发布,新特性全解

Java注解教程及自定义注解

Kotlin中使用注解框架不起作用

自定义Gradle plugin Java AnnotationProcessor 和 Kotlin Kapt 断点调试