Gradle Plugin: Transform + Javassist 编译期操作Class文件

一、Gradle 自定义插件步骤

参考:https://www.jianshu.com/p/03eb55536298

在Gradle中自定义插件,有三种方式:

  • 在 build.gradle 脚本中直接创建使用
  • 在 buildSrc 模块中使用
  • 在独立 Module 中使用

对比这三种方式,各自优缺点如下:

  • 方式一比较快捷,但是可能不能为其它项目使用;
  • 方式二创建了 buildSrc 模块后,Android Studio 会直接识别其为插件模块,在主工程 .gradle 文件中可以直接 apply 插件,而不用引入 maven 或 jcenter 等仓库才能使用插件;
  • 方式三就需要使用引入 maven 或 jcenter 等仓库才能使用插件。

另外,在 IDEA 中也能开发 Gradle 插件,但是在 Android Studio 中更利于进行插件依赖和调试。所以建议直接在 Android Studio 中创建插件,若提供给其他项目使用,则创建 maven 、jcenter 仓库上传脚本上传到远程仓库后进行远程依赖就行。

以下所有的插件实现都是通过在 Android Studio 中创建 buildSrc 模块实现的。

通过 buildSrc 方式自定义插件过程中遇见的问题:
Q:定义了多个插件如何声明和使用?
A:gradle-plugins 为声明插件的目录,项目中创建了多个 Plugin.groovy 文件,可以在这里创建多个 youPluginName.properties 文件,内容为:

implementation-class=包名.插件类名

使用时,直接在 app.gradle 中进行依赖:

apply plugin: 'youPluginName'

如果是远程maven等仓库依赖,则需要添加仓库地址,并且需要在项目根目录添加插件版本 classPath 。

Q:buildSrc 插件模块,在定义插件时,如何使用第三方依赖?
A:同一般依赖引用,比如下面要使用 Transform + Javassist 进行操作字节码,则需要同时加入 gradle 和 javassist 远程依赖,在 buildSrc 模块下的 .gradle 文件配置如下:

apply plugin: 'groovy'

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    repositories {
        google()
        mavenLocal()
        jcenter()
    }

    implementation gradleApi()    // gradle sdk
    implementation localGroovy()  // groovy sdk
    
    // transform 时需要用到gradle tool的api,需要单独引入
    implementation 'com.android.tools.build:gradle:3.1.3'
    implementation 'com.android.tools.build:gradle-api:3.1.3'
    implementation 'org.javassist:javassist:3.20.0-GA'
}
二、Javassist + Task 自动生成 .java 文件

按照上面的配置,先来试一下如何使用 javassit 在编译期自动生成 java 代码。
具体场景:在系统自动生成 BuildConfig.java 文件后(也就是系统内置任务 generateDebugBuildConfig 之后新建任务执行),自动生成我们自定义的 java 代码文件。

具体 .groovy 代码如下:

package com.coral.plugin

import com.android.build.gradle.AppPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.gradle.AppExtension

/**
 * desc: 利用 Javassist,在系统自动生成BuildConfig.java文件后,自动生成我们的java文件
 */
public class CreateJavaPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        System.out.println("----------------Begin----------------")
        System.out.println("This is out custom plugin.")

        def android = project.extensions.getByType(AppExtension)

        // 注册一个Transform
        def classTransform = new MyTransform(project)
        android.registerTransform(classTransform)

        // 创建一个 Extension
        project.extensions.create("testCreateJavaConfig", CreateJavaExtension)

        // 生产一个类
        if (project.plugins.hasPlugin(AppPlugin)) {
            // 获取到 Extension,也即是 .gradle 文件中的闭包
            android.applicationVariants.all { variant ->
                // 获取到 scope 作用域
                def variantData = variant.variantData
                def scope = variantData.scope

                // 拿到 .gradle 中配置的 Extension 值
                def config = project.extensions.getByName("testCreateJavaConfig")

                // 创建一个 Task(名称为:coralDebugCreateJavaPlugin 或 coralReleaseCreateJavaPlugin)
                def createTaskName = scope.getTaskName("coral", "CreateJavaPlugin")
                def createTask = project.task(createTaskName)

                // 设置 task 要执行的任务
                createTask.doLast {
                    // 生成 java 类
                    createJavaTest(variant, config)
                }

                // 设置 task 依赖于生成 BuildConfig 的 task,然后在生成 BuildConfig 后生成我们的类
                String generateBuildConfigTaskName = variant.getVariantData()
                        .getScope().getGenerateBuildConfigTask().name
                // 任务名称:generateDebugBuildConfig
                println("generateBuildConfigTaskName = " + generateBuildConfigTaskName)

                def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
                if (generateBuildConfigTask) {
                    createTask.dependsOn generateBuildConfigTask
                    generateBuildConfigTask.finalizedBy createTask
                }
            }
        }

        System.out.println("----------------Has it finished?----------------")
    }

    static void createJavaTest(variant, config) {
        println("---begin create: " + variant + ", " + config.str)
        // 要生成的内容
        def content = """package com.coral.demo;
/**
* Created by xss on 2018/11/20.
*/
public class TestClass {
    public static final String str = "${config.str}";
}
                      """
        // 获取到 BuildConfig 类的路径
        File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()
        // app/build/generated/source/buildConfig/debug
        println("outputDir = " + outputDir.absolutePath)
        def javaFile = new File(outputDir, "TestClass.java")
        javaFile.write(content, 'UTF-8')
        println("---create finished---")
    }
}

public class CreateJavaExtension {
    def str = "动态生成Java类的字符串"
}

在 app.gradle 文件中配置如下:

// 自动生成 Java 类插件 
apply plugin: 'myPluginCreateJava'

testCreateJavaConfig {
    str = '动态生成Java类'
}

同步gradle 后,在Studio右侧 app -> Tasks -> other 可以看到自定义的任务:

《Gradle Plugin: Transform + Javassist 编译期操作Class文件》 自定义任务名称

双击执行任务编译成功后,在 app/build/generated/source/buildConfig/debug 目录下可以看到自动生成的 java 文件,在项目中可以进行直接引用该类。

参考:http://www.10tiao.com/html/227/201709/2650241354/1.html

三、Transform + Javassist 编译期注入代码到 .class文件

使用 Transform + Javassit 操作字节码,需要在 .gradle 中添加 Transform 和 Javassist 的 API ,配置按上面的 .gradle 配置就行。

具体场景:在项目的 MainActivity 的 onCreate() 方法内部插入一行代码。

MyTransform.groovy 文件代码如下:

package com.coral.plugin

import com.android.build.api.transform.Context
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.Transform
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import org.apache.commons.codec.digest.DigestUtils
import org.gradle.api.Project

public class MyTransform extends Transform {

    Project project

    /**
     * 构造方法,保留原project备用
     */
    MyTransform(Project project) {
        this.project = project
    }

    /**
     * 设置自定义 Transform 对应的 Task 名称
     * 类似:TransformClassesWithPreDexForXXX,对应的 task 名称为:transformClassesWithMyTransformForDebug
     * 会生成目录 build/intermediates/transforms/MyTransform/
     */
    @Override
    String getName() {
        return "MyTransform"
    }

    /**
     * 指定输入的类型,可指定我们要处理的文件类型(保证其他类型文件不会传入)
     * CLASSES - 表示处理java的class文件
     * RESOURCES - 表示处理java的资源
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 指定 Transform 的作用范围
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    /**
     * 是否支持增量编译
     */
    @Override
    boolean isIncremental() {
        return false
    }

    /**
     * 核心方法,具体如何处理输入和输出
     * @param inputs          为传过来的输入流,两种格式,一种jar包格式,一种目录格式
     * @param outputProvider  获取到输出目录,最后将修改的文件复制到输出目录,这一步必须执行,不让编译会报错
     */
    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs,
                   TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {

        System.out.println("----------开始Transform-----------")
        // Transform 的 inputs 分为两种类型,一直是目录,一种是 jar 包。需要分开遍历

        inputs.each { TransformInput input ->
            // 1) 对类型为"目录"的 input 进行遍历
            input.directoryInputs.each { DirectoryInput dirInput ->
                // demo1. 在MainActivity的onCreate()方法之前注入代码
                MyInject.injectOnCreate(dirInput.file.absolutePath, project)
                // 获取 output 目录
                def dest = outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes,
                    dirInput.scopes, Format.DIRECTORY)
                // 将 input 的目录复制到 output 指定目录
                FileUtils.copyDirectory(dirInput.file, dest)
            }

            // 2) 对类型为 jar 文件的 input 进行遍历
            input.jarInputs.each { JarInput jarInput ->
                // jar 文件一般是第三方依赖库jar包

                // 重命名输出文件(同目录 copyFile 会冲突)
                def jarName = jarInput.name
                def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())

                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                // 生成输出路径
                def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes,
                    jarInput.scopes, Format.JAR)
                // 将输入内容复制到输出
                FileUtils.copyFile(jarInput.file, dest)
            }
        }

        System.out.println("----------结束Transform-----------")
    }
}

MyInject.groovy 文件操作字节码代码如下:

package com.coral.plugin

import javassist.ClassPool
import javassist.CtClass
import javassist.CtConstructor
import javassist.CtMethod
import org.gradle.api.Project

public class MyInject {
    private static ClassPool classPool = ClassPool.getDefault()

    public static void injectOnCreate(String path, Project project) {
        classPool.appendClassPath(path)
        classPool.appendClassPath(project.android.bootClasspath[0].toString())
        classPool.importPackage("android.os.Bundle")

        File dir = new File(path)
        if (dir.isDirectory()) {
            dir.eachFileRecurse { File file ->
                String filePath = file.absolutePath
                if (file.getName().equals("MainActivity.class")) {
                    // 获取 MainActivity
                    CtClass ctClass = classPool.getCtClass("com.coral.demo.MainActivity")
                    println("ctClass = " + ctClass)

                    // 解冻
                    if (ctClass.isFrozen()) {
                        ctClass.defrost()
                    }

                    // 获取到 onCreate() 方法
                    CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate")
                    println("ctMethod = " + ctMethod)
                    // 插入日志打印代码
                    String insertBeforeStr = """android.util.Log.e("--->", "Hello");"""

                    ctMethod.insertBefore(insertBeforeStr)
                    ctClass.writeFile(path)
                    ctClass.detach()
                }
            }
        }
    }
}

如何使自定义的 Transform 有作用?需要定义插件进行注册,MyTransformPlugin.groovy 代码如下:

import com.android.build.gradle.AppPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.gradle.AppExtension

public class MyTransformPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        def android = project.extensions.getByType(AppExtension)
        // 注册Transform
        def classTransform = new MyTransform(project)
        android.registerTransform(classTransform)
    }
}

在 app.gradle 使用时也需要 apply plugin ,依赖脚本同上。

说明:

  • 自定义的 Transform 在编译的时候并不会被触发执行,在安装 apk 时会触发执行;

  • 自定义的 Transform 会自动生成几种不同 gradle task,任务名称规则为:transformClassWith$${getName}For${variant}

    《Gradle Plugin: Transform + Javassist 编译期操作Class文件》 自定义Transform任务名称

  • 双击上述自定义的 transform 任务会去执行 Transform 中的 transform() 方法,进行字节码操作代码。这一步可以看到我们再 groovy 中的打印日志,很方便调试。

  • 在自定义的 MyTransform 中,使用 transform() 方法处理字节码,除了调用 MyInject 类的方法处理不同,其他的处理步骤都是统一的。

  • transform() 处理步骤大致可以分为:1)对类型为目录的 input 遍历;2)调用 javassist api 处理字节码;3)生成输出路径,将操作后的 input 目录复制到 output 指定目录;4)对类型为 jar 的 input 遍历;5)重命名输出文件(防止复制文件冲突);5)生成输出路径 & 将输入内容复制到输出。

    原文作者:Coralline_xss
    原文地址: https://www.jianshu.com/p/882325acd200
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞