美文网首页gradle groovy脚本插件
Javassist实现无侵入埋点

Javassist实现无侵入埋点

作者: NIIIICO | 来源:发表于2021-12-02 16:35 被阅读0次

    Apk编译流程

    Apk编译流程主要经过以下几步:
    1、使用javac将java文件编译成class
    2、使用dex工具将class打包成dex
    3、使用apkbuilder工具将dex、资源文件打包成apk
    4、使用jarsigner工具对apk签名

    其实在编译过程中,google工程师留给了我们很多api用来添加自己的操作。如APT在编译时可以对代码进行处理,Transform在将class打包成dex中途,可以对class文件做自己的处理。

    Apk编译流程

    操作流程

    一、创建工程、基础配置

    1、新建Java Library工程


    新建工程

    2、将monitor中build.gradle的plugins改成groovy

    plugins {
        id 'java-library'
    }
    
    //------------------改成----------------------
    
    plugins {
        id 'groovy'
    }
    

    3、删除java目录,并在main中新建groovy目录。


    新建groovy目录

    4、在monitor的build.gradle中添加依赖

    plugins {
        id 'groovy'
        id 'maven-publish'
    }
    
    dependencies {
        implementation gradleApi()
        implementation localGroovy()
    
        implementation "com.android.tools.build:gradle:3.1.3"
        implementation "org.javassist:javassist:3.20.0-GA"
    }
    
    java {
        sourceCompatibility = JavaVersion.VERSION_1_7
        targetCompatibility = JavaVersion.VERSION_1_7
    }
    

    如果报错,Build was configured to prefer settings repositories over project repositories but repository 'Gradle Libs' was added by unknown code;可以进入settings.gradle,将repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)注释掉。

    二、插件开发

    1、新建groovy类

    新建MonitorPlugin,实现Plugin接口,泛型为Project。

    package com.niiiico.monitor
    
    import org.gradle.api.Plugin
    import org.gradle.api.Project;
    
    public class MonitorPlugin implements Plugin<Project> {
        @Override
        void apply(Project project) {
            println "hello plugin"
        }
    }
    

    2、设置properties

    在main目录下新建文件夹resources/META-INF/gradle-plugins,新建文件com.niiiico.monitor.properties(包名.properties)。

    com.niiiico.monitor.properties

    文件内容为:implementation-class=插件全路径,以此来表示插件的入口。

    implementation-class=com.niiiico.monitor.MonitorPlugin
    

    3、打包插件,并发布到本地仓库

    3.1、在monitor的build.gradle添加publishing代码:

    plugins {
        id 'groovy'
        id 'maven-publish'
    }
    
    dependencies {
        implementation gradleApi()
        implementation localGroovy()
    
        implementation "com.android.tools.build:gradle:3.1.3"
        implementation "org.javassist:javassist:3.20.0-GA"
    }
    
    java {
        sourceCompatibility = JavaVersion.VERSION_1_7
        targetCompatibility = JavaVersion.VERSION_1_7
    }
    
    // 将插件打包发布到本地
    publishing {
        publications {
            // Creates a Maven publication called "monitor".
            monitor(MavenPublication) {
                // 表示是一个java插件,最终会打包成jar包
                from components.java
    
                groupId = 'com.niiiico.monitor'
                artifactId = 'monitor'
                version = '1.0'
            }
        }
    
        repositories {
            maven {
                // 发布地址
                url('../monitor-jar')
            }
        }
    }
    

    3.2、点击右上角的Sync now,在右上角的gradle->Tasks便能找到publish任务。


    publish任务

    3.3、双击publish,可以在工程目录看到多了一个monitor-jar目录。打好的插件包便在这个目录下。


    monitor-jar目录

    4、依赖插件

    4.1、在项目的build.gradle中添加maven本地路径,并在dependencies添加插件依赖。classpath "groupId:artifactId:version"

    添加插件
    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    buildscript {
        repositories {
            google()
            mavenCentral()
            maven {
                url('monitor-jar')
            }
        }
        dependencies {
            classpath "com.android.tools.build:gradle:7.0.3"
            classpath "com.niiiico.monitor:monitor:1.0"
            // NOTE: Do not place your application dependencies here; they belong
            // in the individual module build.gradle files
        }
    }
    
    task clean(type: Delete) {
        delete rootProject.buildDir
    }
    

    4.2、在app的build.gradle中添加插件依赖:apply plugin: 'com.niiiico.monitor';内容即我们在resources/META-INF/gradle-plugins下创建的文件名称。

    插件依赖

    4.3、点击sync,即可在build中看到如下打印,表示插件引入成功。


    打印

    三、继承Transform

    1、新建MonitorTransform继承自Transform

    package com.niiiico.monitor
    
    import com.android.build.api.transform.QualifiedContent
    import com.android.build.api.transform.Transform
    import com.android.build.gradle.internal.pipeline.TransformManager;
    
    public class MonitorTransform extends Transform {
        def project
    
        MonitorTransform(Project project) {
            this.project = project
        }
    
        // 在app/build/intermediates/transforms/路径下生成新的文件夹
        // 用来存储本次transform操作的数据
        @Override
        String getName() {
            return "monitor"
        }
    
        // 接收什么类型的数据
        @Override
        Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS
        }
    
        // 接收数据的范围
        @Override
        Set<? super QualifiedContent.Scope> getScopes() {
            return TransformManager.SCOPE_FULL_PROJECT
        }
    
        // 一般不修改
        @Override
        boolean isIncremental() {
            return false
        }
    }
    

    2、重写输入输出

    2.1、在MonitorPlugin的apply方法中使用project.android.registerTransform(new MonitorTransform(project))注册自定义的Transform。

    package com.niiiico.monitor
    
    import org.gradle.api.Plugin
    import org.gradle.api.Project;
    
    public class MonitorPlugin implements Plugin<Project> {
        @Override
        void apply(Project project) {
            project.android.registerTransform(new MonitorTransform(project))
        }
    }
    

    2.2、重新使用publish发布插件,然后运行app,发现apk无法运行。因为注册Transform后,系统会把我们的Transform插入编译打包流程,上一个节点会将编译好的class和jar等信息告诉我们,如果我们不进行任何处理,下一个节点便无法拿到这些信息,因此需要重写输入输出,将从上一个节点拿到的数据告诉下一个节点。


    Transform

    2.3、要将数据告诉下个节点,需要以下几步:
    (1)遍历inputs目录,查询输入的文件
    (2)查询输出文件路径
    (3)将输入文件复制到下一个节点

    重写transform函数

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOExcepti
        super.transform(transformInvocation)
        println "--------------------transform-------------------"
        // 1、查询输入,遍历inputs目录
        transformInvocation.inputs.each {
            // 1.1 jar包目录
            it.jarInputs.each {
                // 2.查询输出
                def dest = transformInvocation.outputProvider.getContentLocation(
                        it.name,
                        it.contentTypes,
                        it.scopes,
                        Format.JAR)
                println "jar dest----->" + dest
                // 3.复制到下一环节
                FileUtils.copyFile(it.file, dest);
            }
            // 1.2 class目录
            it.directoryInputs.each {
                // 2.查询输出
                def dest = transformInvocation.outputProvider.getContentLocation(
                        it.name,
                        it.contentTypes,
                        it.scopes,
                        Format.DIRECTORY)
                println "class dest----->" + dest
                // 3.复制到下一环节
                FileUtils.copyDirectory(it.file, dest);
            }
        }
    }
    

    2.4、重新发布,点击安装,即可安装成功。此时,在build\intermediates\transforms\下可以发现,新增了monitor目录,这边是getName函数定义的名字。


    monitor目录

    四、Javassist修改class文件

    1、通过ClassPool加载class文件

    // 缓存class字节码对象的容器
    def pool = ClassPool.getDefault()
    
    def preFileName = it.file.absolutePath
    // 加载路径下的class文件
    pool.insertClassPath(preFileName)
    // project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
    pool.appendClassPath(project.android.bootClasspath[0].toString());
    // 引入android.os.Bundle包,因为onCreate方法参数有Bundle
    pool.importPackage("android.os.Bundle");
    

    2、找到class文件

    遍历系统传过来的class文件目录,找到class文件

    // 找到需要处理的文件并处理
    // fileName D:\workplace_github\JavassistDemo\app\build\intermediates\javac\debug\classes
    private void findTargetAndSettle(File dir, String fileName) {
        if (dir.isDirectory()) {
            // 如果是目录,继续遍历
            dir.listFiles().each {
                findTargetAndSettle(it, fileName)
            }
        } else {
            def filePath = dir.absolutePath
            // 只处理class文件
            if (filePath.endsWith(".class")) {
                println "find class----->" + filePath
                // 修改文件
                modify(filePath, fileName)
            }
        }
    }
    

    3、过滤class

    过滤系统生成的class,然后截取class全类名,通过ClassPool查找到CtClass 对象。

    // 过滤class
    private void filterClass(def filePath, String fileName) {
        // 过滤系统文件
        if (filePath.contains('R$')
                || filePath.contains('R.class')
                || filePath.contains("BuildConfig.class")) {
            return
        }
        // 获取className
        def className = filePath.replace(fileName, "")
                .replace("\\", ".")
                .replace("/", ".")
                .replace(".class", "")
                .substring(1)
        println "find className----->" + className
        // 获取CtClass对象,用来操作class
        CtClass ctClass = pool.get(className)
        addCode(ctClass, fileName)
    }
    

    4、修改代码并写入文件

    // 添加代码
    private void addCode(CtClass ctClass, String fileName) {
        // 解冻
        ctClass.defrost()
    
        CtMethod[] methods = ctClass.getDeclaredMethods()
        for (method in methods) {
            println "method " + method.getName() + "参数个数  " + method.getParameterTypes().length
            if ("onCreate".equals(method.getName())) {
                method.insertBefore("{ System.out.println(\"调用了" + method.getName() + "\");}")
            }
        }
    
        // 将修改的文件写出去
        ctClass.writeFile(fileName)
        ctClass.detach()
    }
    

    5、验证结果
    点击publish重新打包插件,重新打包并运行apk。

    控制台日志

    查看build\intermediates\transforms\monitor\目录下的MainActivity.class文件,发现代码已经被修改。


    MainActivity.class

    6、MonitorTransform全部代码

    package com.niiiico.monitor
    
    import com.android.build.api.transform.Format
    import com.android.build.api.transform.QualifiedContent
    import com.android.build.api.transform.Transform
    import com.android.build.api.transform.TransformException
    import com.android.build.api.transform.TransformInvocation
    import com.android.build.gradle.internal.pipeline.TransformManager
    import com.android.utils.FileUtils
    import javassist.ClassPool
    import javassist.CtClass
    import javassist.CtMethod
    import org.gradle.api.Project;
    
    public class MonitorTransform extends Transform {
        def project
        // 缓存class字节码对象的容器
        def pool = ClassPool.getDefault()
    
        MonitorTransform(Project project) {
            this.project = project
        }
    
        @Override
        void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
            super.transform(transformInvocation)
            println "--------------------transform-------------------"
    
    
            // 1、查询输入,遍历inputs目录
            transformInvocation.inputs.each {
                // 1.1 jar包目录
                it.jarInputs.each {
                    // 2.查询输出
                    def destDir = transformInvocation.outputProvider.getContentLocation(
                            it.name,
                            it.contentTypes,
                            it.scopes,
                            Format.JAR)
                    println "jar destDir----->" + destDir
    
                    // 3.复制到下一环节
                    FileUtils.copyFile(it.file, destDir);
                }
    
                // 1.2 class目录
                it.directoryInputs.each {
    
                    def preFileName = it.file.absolutePath
                    // 加载路径下的class文件
                    pool.insertClassPath(preFileName)
                    // project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
                    pool.appendClassPath(project.android.bootClasspath[0].toString());
                    // 引入android.os.Bundle包,因为onCreate方法参数有Bundle
                    pool.importPackage("android.os.Bundle");
    
                    println "========directoryInputs======== " + preFileName
                    findTargetAndSettle(it.file, preFileName)
    
                    // 2.查询输出
                    def destDir = transformInvocation.outputProvider.getContentLocation(
                            it.name,
                            it.contentTypes,
                            it.scopes,
                            Format.DIRECTORY)
                    println "class destDir----->" + destDir
    
                    // 3.复制到下一环节
                    FileUtils.copyDirectory(it.file, destDir);
                }
            }
        }
    
        // 找到需要处理的文件并处理
        // fileName D:\workplace_github\JavassistDemo\app\build\intermediates\javac\debug\classes
        private void findTargetAndSettle(File dir, String fileName) {
            if (dir.isDirectory()) {
                // 如果是目录,继续遍历
                dir.listFiles().each {
                    findTargetAndSettle(it, fileName)
                }
            } else {
                def filePath = dir.absolutePath
                // 只处理class文件
                if (filePath.endsWith(".class")) {
                    println "find class----->" + filePath
                    // 修改文件
                    filterClass(filePath, fileName)
                }
            }
        }
    
        // 过滤class
        private void filterClass(def filePath, String fileName) {
            // 过滤系统文件
            if (filePath.contains('R$')
                    || filePath.contains('R.class')
                    || filePath.contains("BuildConfig.class")) {
                return
            }
    
            // 获取className
            def className = filePath.replace(fileName, "")
                    .replace("\\", ".")
                    .replace("/", ".")
                    .replace(".class", "")
                    .substring(1)
    
            println "find className----->" + className
    
            // 获取CtClass对象,用来操作class
            CtClass ctClass = pool.get(className)
            addCode(ctClass, fileName)
        }
    
        // 添加代码
        private void addCode(CtClass ctClass, String fileName) {
            // 解冻
            ctClass.defrost()
            CtMethod[] methods = ctClass.getDeclaredMethods()
            for (method in methods) {
                println "method " + method.getName() + "参数个数  " + method.getParameterTypes().length
                if ("onCreate".equals(method.getName())) {
                    method.insertBefore("{ System.out.println(\"调用了" + method.getName() + "\");}")
                }
            }
    
            // 将修改的文件写出去
            ctClass.writeFile(fileName)
            ctClass.detach()
        }
    
        // 在app/build/intermediates/transforms/路径下生成新的文件夹
        // 用来存储本次transform操作的数据
        @Override
        String getName() {
            return "monitor"
        }
    
        // 接收什么类型的数据
        @Override
        Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS
        }
    
        // 接收数据的范围
        @Override
        Set<? super QualifiedContent.Scope> getScopes() {
            return TransformManager.SCOPE_FULL_PROJECT
        }
    
        // 一般不修改
        @Override
        boolean isIncremental() {
            return false
        }
    }
    

    五、git工程地址

    https://github.com/Timey729/JavassistDemo

    相关文章

      网友评论

        本文标题:Javassist实现无侵入埋点

        本文链接:https://www.haomeiwen.com/subject/qwdyxrtx.html