代码诗人——APT && JavaPoet

AOP 中, 我们以处理阶段为划分产生了很多可选的技术手段:

  • java 源代码阶段 (apt 、 ksp、 java)
  • class 字节码阶段 (asm javaassist)
  • dex 阶段
  • 运行时阶段 (动态代理、cglib、javaassist)

其中 apt 处理的是 java 源代码文件, 操作起来比较简单, 是一项被广泛使用的技术

APT(Annotation Processing Tool) 技术是编译期间对注解的处理技术, 项目中若有很多类具有相似的样板代码, 可以考虑将这些样板代码在编译期间进行处理。

具体使用中, 常常会搭配 javapoet 来编译期间生成一些样板类, 解放手工

apt

apt 简单来说做的工作: 通过输入(java文件), 找到带有需要处理的注解的元素, 读取这些注解的信息, 为后续的 代码植入做准备。

apt 是 gradle build 阶段一个 task 触发的

正常执行下 app:assembleDebug 触发的 gradle task 如下:

Starting Gradle Daemon...
Gradle Daemon started in 1 s 286 ms
> Task :annotation:compileKotlin UP-TO-DATE
> Task :annotation:compileJava UP-TO-DATE
> Task :annotation:compileGroovy NO-SOURCE
> Task :annotation:processResources UP-TO-DATE
> Task :annotation:classes UP-TO-DATE
> Task :annotation:inspectClassesForKotlinIC UP-TO-DATE
> Task :annotation:jar UP-TO-DATE
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:compileDebugAidl NO-SOURCE
> Task :app:compileDebugRenderscript NO-SOURCE
> Task :app:generateDebugBuildConfig UP-TO-DATE
> Task :app:checkDebugAarMetadata UP-TO-DATE
> Task :app:generateDebugResValues UP-TO-DATE
> Task :app:generateDebugResources UP-TO-DATE
> Task :app:mergeDebugResources UP-TO-DATE
> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
> Task :app:extractDeepLinksDebug UP-TO-DATE
> Task :app:processDebugMainManifest UP-TO-DATE
> Task :app:processDebugManifest UP-TO-DATE
> Task :app:processDebugManifestForPackage UP-TO-DATE
> Task :app:processDebugResources UP-TO-DATE
> Task :app:kaptGenerateStubsDebugKotlin UP-TO-DATE
> Task :app:kaptDebugKotlin UP-TO-DATE
> Task :app:compileDebugKotlin UP-TO-DATE
> Task :app:javaPreCompileDebug UP-TO-DATE
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
> Task :app:compileDebugSources UP-TO-DATE
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
> Task :app:mergeDebugShaders UP-TO-DATE
> Task :app:compileDebugShaders NO-SOURCE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:mergeDebugAssets UP-TO-DATE
> Task :app:compressDebugAssets UP-TO-DATE
> Task :app:processDebugJavaRes NO-SOURCE
> Task :app:mergeDebugJavaResource UP-TO-DATE
> Task :app:checkDebugDuplicateClasses UP-TO-DATE
> Task :app:desugarDebugFileDependencies UP-TO-DATE
> Task :app:mergeExtDexDebug UP-TO-DATE
> Task :app:dexBuilderDebug UP-TO-DATE
> Task :app:mergeProjectDexDebug UP-TO-DATE
> Task :app:mergeLibDexDebug UP-TO-DATE
> Task :app:mergeDebugJniLibFolders UP-TO-DATE
> Task :app:mergeDebugNativeLibs NO-SOURCE
> Task :app:stripDebugDebugSymbols NO-SOURCE
> Task :app:validateSigningDebug UP-TO-DATE
> Task :app:writeDebugAppMetadata UP-TO-DATE
> Task :app:writeDebugSigningConfigVersions UP-TO-DATE
> Task :app:packageDebug UP-TO-DATE
> Task :app:assembleDebug UP-TO-DATE

Task :app:kaptGenerateStubsDebugKotlin UP-TO-DATE
Task :app:kaptDebugKotlin UP-TO-DATE

就是 apt 的位置, apt 后才会生成 class 文件, 进一步dex , 最后 package

具体 apt 的代码都写在 AbstractProcessor 的实现类中

该类中主要常用的几个元素
    override fun init(processingEnvironment: ProcessingEnvironment?) {
        super.init(processingEnvironment)
        mTypeUtil = processingEnvironment?.getTypeUtils()
        mElementUtil = processingEnvironment?.getElementUtils()
        mFiler = processingEnvironment?.getFiler()
        mMessager = processingEnvironment?.getMessager()
    }
    override fun process(
        set: MutableSet<out TypeElement>,
        processingEnvironment: RoundEnvironment
    ): Boolean {
        // 具体 apt 代码
}

process 中, 可以根据 RoundEnvironment 可以取到所有带有某个注释的 类、接口、方法

TypeElement 是 类/接口
Elements 是一个 工具类, 常用来获取所有带某个注解的元素如: 所有方法

一般流程:


整个过程来说:

    1. 解析注解
    1. 构造一个数据结构保存注解中的有效信息

javapoet

习惯语法即可

首先写一个 javaFile涉及到的核心步骤:

  • 类相关 TypeSpec
  • 构造函数 MethodSpec
  • 成员变量 FieldSpec
  • 方法 MethodSpec
  • 注解 AnnotationSpec

具体如何使用可以直接参考:
https://blog.csdn.net/qq_17766199/article/details/112429217

不再赘述

         val genClass =
                TypeSpec.classBuilder(element.simpleName.toString() + "$\$Impl")
                    .addSuperinterface(ClassName.get(element))
                    .addModifiers(Modifier.PUBLIC)

            for (field in fields) {
                genClass.addField(field)
            }
            for (method in methods) {
                genClass.addMethod(method)
            }

            JavaFile.builder(
                mElementUtil!!.getPackageOf(element).qualifiedName.toString(),
                genClass.build()
            )
                .addFileComment("Generated code")
                .build()
                .writeTo(mFiler)

实践

  • app 模块
  • annotation模块

具体build.gradle 可以参考 github:

注解代码:

package com.example.perla

import com.example.annotation.*

@Man(name = "jackie", age = 1, coutry = JackCountry::class)
interface Jackie : IFigher {

    @Body(weight = 200, height = 200)
    fun body()

    @GetCE(algorithm = Algorithm::class)
    fun ce(): Int

    @GetInstance
    fun instance(): IFigher
}

class Algorithm : IAlgorithm {
    override fun ce(figher: IFigher): Int {
        return -1
    }
}

class JackCountry : ICountry {
    override fun name(): String {
        return "China"
    }

}

注解生成代码:

// Generated code
package com.example.perla;

import com.example.annotation.IAlgorithm;
import com.example.annotation.IFigher;
import java.lang.Override;
import java.lang.String;
import java.lang.System;

public class Jackie$$Impl implements Jackie {
  private String mKey;

  private String name;

  private int age;

  private String country;

  private int weight;

  private int height;

  private IAlgorithm algorithm;

  public Jackie$$Impl(String key) {
    mKey = key;
    name = "jackie";
    age = 1;
    country = new JackCountry().name();
    algorithm = new Algorithm();
  }

  @Override
  public void body() {
    weight = 200;
    height = 200;
  }

  @Override
  public int ce() {
    if (algorithm != null) {
      return algorithm.ce(instance());
    }
    return weight  + height;
  }

  @Override
  public IFigher instance() {
    return new Jackie$$Impl(String.valueOf(System.currentTimeMillis()));
  }
}

核心代码:

PerlaProcessor.kt

package com.example.annotation


import com.google.auto.common.AnnotationMirrors
import com.google.auto.common.MoreElements
import com.google.auto.service.AutoService
import com.squareup.javapoet.*
import javax.annotation.processing.*
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.Modifier
import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements
import javax.lang.model.util.Types


@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_11)
@SupportedOptions()
@SupportedAnnotationTypes("*")
class PerlaProcessor : AbstractProcessor() {

    private var mTypeUtil: Types? = null
    private var mElementUtil: Elements? = null
    private var mFiler: Filer? = null
    private var mMessager: Messager? = null
    private val aptSourceBook = HashMap<TypeElement, AptManInfo>()


    override fun init(processingEnvironment: ProcessingEnvironment?) {
        super.init(processingEnvironment)
        mTypeUtil = processingEnvironment?.getTypeUtils()
        mElementUtil = processingEnvironment?.getElementUtils()
        mFiler = processingEnvironment?.getFiler()
        mMessager = processingEnvironment?.getMessager()

    }

    override fun process(
        set: MutableSet<out TypeElement>,
        processingEnvironment: RoundEnvironment
    ): Boolean {


        try {

            for (element in processingEnvironment.getElementsAnnotatedWith(Man::class.java)) {
                parseAnnotation(aptSourceBook, element as TypeElement)
            }


            write()


        } catch (ex: Exception) {

        }
        return true
    }

    private fun write() {


        for ((element, info) in aptSourceBook) {

            val fields = ArrayList<FieldSpec>()
            val methods = ArrayList<MethodSpec>()

            val keyField = FieldSpec.builder(ClassName.get(String::class.java), "mKey")
                .addModifiers(Modifier.PRIVATE).build()

            val nameField = FieldSpec.builder(String::class.java, "name")
                .addModifiers(Modifier.PRIVATE)
                .build()

            val ageField = FieldSpec.builder(Int::class.java, "age")
                .addModifiers(Modifier.PRIVATE)
                .build()

            val countryField = FieldSpec.builder(String::class.java, "country")
                .addModifiers(Modifier.PRIVATE)
                .build()

            val weightField = FieldSpec.builder(Int::class.java, "weight")
                .addModifiers(Modifier.PRIVATE)
                .build()

            val heightField = FieldSpec.builder(Int::class.java, "height")
                .addModifiers(Modifier.PRIVATE)
                .build()


            val algorithmField = FieldSpec.builder(IAlgorithm::class.java, "algorithm")
                .addModifiers(Modifier.PRIVATE)
                .build()


            fields.add(keyField)
            fields.add(nameField)
            fields.add(ageField)
            fields.add(countryField)
            fields.add(weightField)
            fields.add(heightField)
            fields.add(algorithmField)

            val constructor =
                MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(String::class.java), "key")
                    .addStatement("mKey = key")
                    .addStatement("name = \$S", info.name)
                    .addStatement("age = \$L", info.age)
                    .addStatement("country = new \$T().name()", info.country)
                    .addStatement("algorithm = new \$T()", info.algorithm)


            val body =
                MethodSpec.methodBuilder("body")
                    .addAnnotation(Override::class.java)
                    .addModifiers(Modifier.PUBLIC)

            info.bodyInfo?.let {
                body.addStatement("weight = \$L", it.weight)
                body.addStatement("height = \$L", it.height)
            }

            val ce =
                MethodSpec.methodBuilder("ce")
                    .addAnnotation(Override::class.java)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(TypeName.INT)
                    .beginControlFlow("if (algorithm != null)")
                    .addStatement("return algorithm.ce(instance())")
                    .endControlFlow()
                    .addStatement("return weight  + height")

            val getInstance =
                MethodSpec.methodBuilder("instance")
                    .addAnnotation(Override::class.java)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(ClassName.get(IFigher::class.java))
                    .addStatement(
                        "return new \$T(String.valueOf(\$T.currentTimeMillis()))",
                        ClassName.bestGuess(element.simpleName.toString() + "$\$Impl"),
                        System::class.java
                    )




            methods.add(constructor.build())
            methods.add(body.build())
            methods.add(ce.build())
            methods.add(getInstance.build())


            val genClass =
                TypeSpec.classBuilder(element.simpleName.toString() + "$\$Impl")
                    .addSuperinterface(ClassName.get(element))
                    .addModifiers(Modifier.PUBLIC)

            for (field in fields) {
                genClass.addField(field)
            }
            for (method in methods) {
                genClass.addMethod(method)
            }

            JavaFile.builder(
                mElementUtil!!.getPackageOf(element).qualifiedName.toString(),
                genClass.build()
            )
                .addFileComment("Generated code")
                .build()
                .writeTo(mFiler)
        }
    }

    private fun parseAnnotation(
        aptSourceBook: java.util.HashMap<TypeElement, AptManInfo>,
        element: TypeElement
    ) {

        val aptManInfo = AptManInfo()
        val annotationInfo = element.getAnnotation(Man::class.java)
        aptManInfo.apply {
            name = annotationInfo.name
            age = annotationInfo.age
            country = getAnnotationClassName(element, Man::class.java, "coutry")?.toString()
                ?.let { ClassName.bestGuess(it) }
        }
        aptSourceBook[element] = aptManInfo

        val methods = mElementUtil!!.getAllMembers(element)
            .filter {
                it.kind == ElementKind.METHOD &&
                        MoreElements.isAnnotationPresent(it, GetInstance::class.java) ||
                        MoreElements.isAnnotationPresent(it, GetCE::class.java) ||
                        MoreElements.isAnnotationPresent(
                            it,
                            Body::class.java
                        )

            }.map { MoreElements.asExecutable(it) }.groupBy {
                when {
                    MoreElements.isAnnotationPresent(it, Body::class.java) -> Body::class.java
                    MoreElements.isAnnotationPresent(
                        it,
                        GetInstance::class.java
                    ) -> GetInstance::class.java
                    MoreElements.isAnnotationPresent(it, GetCE::class.java) -> GetCE::class.java
                    else -> Any::class.java
                }
            }

        methods[Body::class.java]?.forEach {
            val body = it.getAnnotation(Body::class.java)
            aptManInfo.bodyInfo = BodyInfo().apply {
                weight = body.weight
                height = body.height
            }
        }

        methods[GetInstance::class.java]?.forEach {
            val instance = it.getAnnotation(GetInstance::class.java)
            aptManInfo.getInstance = instance
        }


        methods[GetCE::class.java]?.forEach {
            aptManInfo.algorithm =
                getAnnotationClassName(it, GetCE::class.java, "algorithm").toString()
                    .let { ClassName.bestGuess(it) }
        }


    }

    private fun getAnnotationClassName(
        element: Element,
        key1: Class<out Annotation>,
        key: String
    ): Any? {
        return MoreElements.getAnnotationMirror(element, key1)
            .orNull()?.let {
                AnnotationMirrors.getAnnotationValue(it, key)?.value
            }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容