AOP : APT 和 ASM 纺织代码

2022-03-23  本文已影响0人  壹零二肆

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

apt 处理的是 java 源代码文件,项目中若有很多类具有相似的样板代码, 可以考虑将这些样板代码在编译期间进行处理。 常常会搭配 javapoet 来编译期间生成一些样板类, 解放手工

asm 处理的是 class 文件, 比如做一些代码插桩, 映射采集 等字节码增强和生成

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 是一个 工具类, 常用来获取所有带某个注解的元素如: 所有方法

一般流程:


整个过程来说:

javapoet

习惯语法即可

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

具体如何使用可以直接参考:
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)

实践

具体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
            }
    }
}

asm

apt 主要处理 java 文件, asm 处理 class 文件。
asm 也会搭配 gradle plugin 来进行一些代码增强,代码生成。

asm 主要是解决如何拿到 class 文件然后进行代码增强
参考:
https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html

举两个实例来看 asm 的使用:

插桩

假设我们需要写一个 trace 插桩

在函数出入口调用 Trace.beginSection 和 end 就可采集 Trace 数据事后使用 perfetto进行分析

下面是 Recyclerview 中的一个 trace 方法:

   TraceCompat.beginSection(TRACE_SCROLL_TAG);
        fillRemainingScrollValues(mState);

        int consumedX = 0;
        int consumedY = 0;
        if (dx != 0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        if (dy != 0) {
            consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
        }

        TraceCompat.endSection();

插桩后结合 systrace 统计的图,perfetto工具查看到的效果

如果不会使用 systrace 可以查看文章https://mp.weixin.qq.com/s/9dexhnWuWIopdhdU_aKkZw

这里避免 代码中手动每个函数调用 Trace.beginSection 采用字节码插桩来在 gradle plugin 中批处理添加插桩代码

下面是method-trace 插件的具体开发过程:

目录架构:

MethodTracePlugin

package com.ss.android.ugc.bytex.method_trace

import com.android.build.gradle.AppExtension
import com.ss.android.ugc.bytex.common.CommonPlugin
import com.ss.android.ugc.bytex.common.flow.main.Process
import com.ss.android.ugc.bytex.common.visitor.ClassVisitorChain
import com.ss.android.ugc.bytex.pluginconfig.anno.PluginConfig
import org.gradle.api.Project
import org.objectweb.asm.ClassReader


@PluginConfig("bytex.method-trace")
class MethodTracePlugin : CommonPlugin<MethodTraceExtension, MethodTraceContext>() {
    override fun getContext(
            project: Project,
            android: AppExtension,
            extension: MethodTraceExtension
    ): MethodTraceContext {
        return MethodTraceContext(project, android, extension)
    }

    override fun transform(relativePath: String, chain: ClassVisitorChain): Boolean {
        chain.connect(MethodTraceClassVisitor(context, extension))
        return super.transform(relativePath, chain)
    }

    override fun flagForClassReader(process: Process?): Int {
        return ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES or ClassReader.EXPAND_FRAMES
    }
}

MethodTraceExtension可读取如下build.gradle中配置

// apply ByteX宿主
apply plugin: 'bytex'
ByteX {
    enable pluginEnable
    enableInDebug pluginEnableInDebug
    logLevel pluginLogLevel
}

apply plugin: 'bytex.method-trace'

MethodTracePlugin {
    enable pluginEnable
    enableInDebug pluginEnableInDebug
    whiteList = ['com/gongshijie']
}
package com.ss.android.ugc.bytex.method_trace;

import com.ss.android.ugc.bytex.common.BaseExtension;

import java.util.ArrayList;
import java.util.List;

public class MethodTraceExtension extends BaseExtension {

    private List<String> whiteList = new ArrayList<>();


    @Override
    public String getName() {
        return "MethodTracePlugin";
    }

    public List<String> getWhiteList() {
        return whiteList;
    }

    public void setWhiteList(List<String> whiteList) {
        this.whiteList = whiteList;
    }
}

TraceMethodVisitor

package com.ss.android.ugc.bytex.method_trace

import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.commons.AdviceAdapter

class TraceMethodVisitor(private var context: MethodTraceContext,
                         private var className: String, api: Int, mv: MethodVisitor?,
                         access: Int, var methodName: String?, desc: String?
) : AdviceAdapter(api, mv, access, methodName, desc) {


    override fun onMethodEnter() {
        super.onMethodEnter()

        context.logger.i("TraceMethodVisitor", "----插桩----className: $className  methodName: ${methodName}------")

        if (methodName != null) {
            mv.visitLdcInsn("$className#$methodName");
            mv.visitMethodInsn(INVOKESTATIC, "com/ss/android/ugc/bytex/method_trace_lib/MyTrace", "beginSection", "(Ljava/lang/String;)V", false);
        }
    }

    override fun onMethodExit(opcode: Int) {
        super.onMethodExit(opcode)
        mv.visitMethodInsn(INVOKESTATIC, "com/ss/android/ugc/bytex/method_trace_lib/MyTrace", "endSection", "()V", false);

    }
}

映射采集

apt 和 asm 往往会搭配起来使用

比如我们各个模块内部,可以根据注解生成一些映射关系(apt), 后面再通过 asm 来跨模块收集这些映射关系

目录架构

熟悉的环境工作不再赘述, 核心部分就是 采集各模块 apt 生成的文件映射关系, 然后 asm 增强到 一个 class 文件内。

这样的处理在很多框架中都可以找到。

class ManCollectTransform(val project: Project, val appPlugin: AppPlugin?) : Transform() {
    override fun getName(): String {
        return "ManCollectTransform"
    }

    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    override fun isIncremental(): Boolean {
        return false
    }


    override fun transform(transformInvocation: TransformInvocation?) {
        val map = HashMap<String, String>()
        transformInvocation?.inputs?.forEach { it ->

            it.jarInputs.forEach { jarInput ->
                val jarFile = JarFile(jarInput.file)
                val entries = jarFile.entries()
                for (entry in entries) {
                    if (entry.name.endsWith("$\$Impl.class")) {
                        val inputStream = jarFile.getInputStream(entry)
                        val reader = ClassReader(inputStream)
                        val classNode = ClassNode(ASM5)
                        reader.accept(classNode, ClassReader.SKIP_DEBUG)
                        map.put(classNode.interfaces.first(), classNode.name)
                        inputStream.close()
                    }
                }
            }

            it.directoryInputs.forEach { dirInput ->
                project.fileTree(dirInput.file).forEach {
                    if (it.absolutePath.endsWith("$\$Impl.class")) {
                        val inputStream = FileInputStream(it)
                        val reader = ClassReader(inputStream)
                        val classNode = ClassNode(Opcodes.ASM5)
                        reader.accept(classNode, ClassReader.SKIP_DEBUG)
                        if (classNode.interfaces.isNotEmpty()) {
                            map.put(classNode.interfaces.first(), classNode.name)
                        }
                        inputStream.close()
                    }
                }
            }
        }

        println("输出映射关系")
        for((k, v) in map) {
            println("""映射关系采集: $k : $v""")
        }

        transformInvocation?.inputs?.forEach { it ->
            it.jarInputs.forEach { jarInput ->
                val jarFile = JarFile(jarInput.file)
                val manFinderEntry = jarFile.getJarEntry("com/example/mancollect_api/ManFinder.class")
                val dest = transformInvocation.outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                if (manFinderEntry != null) {
                    val inputStream = jarFile.getInputStream(manFinderEntry)
                    val reader = ClassReader(inputStream)
                    val writer = ClassWriter(ClassWriter.COMPUTE_FRAMES)
                    val vis = ManFinderClassAdapter(writer, map)
                    reader.accept(vis, ClassReader.SKIP_DEBUG)
                    inputStream.close()
                }
            }
        }
    }
}

总之 , apt 和 asm 可以帮助我们处理大量的样板代码, 可以帮助我们自动化一些配置化的代码。

上一篇 下一篇

猜你喜欢

热点阅读