• AOP三剑客之Javassist


    Javassist

    Java字节码以二进制的形式存储在.class文件中,每一个class文件包含一个Java类或接口。Javassist框架就是一个用来处理Java字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者修改已有的方法,并且不需要对字节码方面有深入的了解。
    Javassist可以绕过编译,直接操作字节码,从而实现代码的注入。所以,使用Javassist框架的最佳时机就是在构建工具gradle将源文件编译成class文件之后,在将class打包成dex文件之前。

    Javassist基础

    • 读写字节码
      在Javassist框架中,class文件是用类Javassist.CtClass表示的。一个CtClass对像可以处理一个class文件。
      下面举一个简单的例子。
    ClassPool pool=ClassPool.getDefault();
    CtClass aClass = pool.get("com.test.A");
    aClass.setSuperclass("java.lang.Object");
    aClass.writeFile();
    
    • 1
    • 2
    • 3
    • 4

    在上面这个例子中,我们首先获取一个ClassPool对像。ClassPool是CtClass对像的容器,可以按需读取类文件用来创建并保存CtClass对像,以便之后可能会被使用到。
    为了修改类的定义,首先需要使用ClassPool.get()方法从ClassPool中获取一个CtClass对像。使用getDefault方法获取的ClassPool对像使用的是默认系统的类搜索路径。
    ClassPool是一个存储CtClass的Hash表,类的名称作为Hash表的key。ClassPool的get方法会从Hash表查找key对应的CtClass对像。如果根据对应的key没有找到CtClass对像,get方法就会创建并返回一个新的CtClass对像,这个对像同时也会保存在Hash表中。
    从ClassPool中获取的CtClass对像是可以被修改的。在上面的例子中,将A类的父类改为Object。调用writeFile方法后,这项修改会被写入原始类文件中。writeFile方法会将CtClass对象转换成类文件并写到本地磁盘。同时,也可以使用toBytecode方法来获取修改过的字节码。比如:

    byte[] b = aClass.toBytecode();
    
    • 1

    也可以使用toClass方法直接将CtClass对象转换成Class对象,比如:

    Class clazz= aClass.toClass();
    
    • 1

    toClass方法请求当前线程的ClassLoader加载CtClass对象所代表的类文件,它返回的是该类文件的Class对象。

    • 冻结类
      如果一个CtClass对象通过writeFile、toBytecode、toClass等方法被转换成一个类文件,此CtClass对象就会被冻结起来,不再允许修改,这是因为一个类只能被jvm加载一次。
      其实,一个冻结的CtClass对象也可以被解冻,比如:
    aClass.writeFile();
    aClass.defrost();//解冻
    aClass.setSuperClass();//因为这个类已经解冻了,所以可以更改该类
    
    • 1
    • 2
    • 3
    • 类搜索路径
      通过ClassPool.getDefault()获取的ClassPool是使用JVM的类搜索路径。如果程序运行在Tomcat等服务器上,ClassPool可能无法找到用户的类,因为Web服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool必须添加额外的类搜索路径,比如:
    ClassPool pool = ClassPool.getDefault();
    pool.insertClassPath(new ClassPath(this.getClass()));
    
    • 1
    • 2

    在上面的代码示例中,将this指向的类添加到ClassPool的类加载路径中。你可以使用任意Class对象来代替this.getClass(),从而将Class对象添加到类加载路径中,也可以注册一个目录作为搜索路径。比如:

    ClassPool pool = ClassPool.getDefault();
    pool.insertClassPath("/usr/local/Library");
    
    • 1
    • 2
    • ClassPool
      ClassPool是CtClass对象的容器。因为编译器在编译引用CtClass代表的Java类的源代码时,可能会引用CtClass对象,所以一旦一个CtClass被创建,它就会被保存在ClassPool中。

    • 避免内存溢出
      如果CtClass对象的数量变得非常多,ClassPool有可能会导致巨大的内存消耗。为了避免这个问题,我们可以从ClassPool中显式删除不必要的CtClass对象。如果对CtClass对象调用detach方法,那么该CtClass对象将会从ClassPool中删除。比如:

    CtClass aClass = ...;
    aClass.writeFile();
    aClass.detach();
    
    • 1
    • 2
    • 3

    在调用detach方法之后,就不能再调用这个CtClass对象的任何有关方法了。如果调用了ClassPool的get方法,ClassPool会再次读取这个类文件,并创建一个新的CtClass对象。

    • 注解(Annotations)
      CtClass、CtMethod、CtField和CtConstructor均提供了getAnnotations方法,用于读取对应类型上添加的注解。

    • 在方法体中插入代码
      CtMethod和CtConstructor均提供了insertBefore、insertAfter、addCatch等方法。它们可以把用Java编写的代码片段插入到现有的方法体中。Javassist包括一个用于处理源代码的小型编译器,它接收用Java编写的源代码,然后将其编译成Java字节码,并内联到方法体中。
      也可以按行号来插入代码段(如果行号表包含在类文件中)。向CtMethod和CtConstructor中的insertAt方法提供源代码和原始类定义中的源文件的行号,就可以将编译后的代码插入到指定行号位置。
      insertBefore、insertAfter、insertAt、addCatch等方法都能接收一个表示语句或语句块的String对象。一个语句是一个单一的控制结构,比如if和while,或者以分号结尾的表达式。语句块是一组用{}包围的语句。
      语句和语句块可以引用字段和方法,但不允许访问在方法中声明的局部变量,尽管在块中声明一个新的局部变量是允许的。
      传递给方法insertBefore、insertAfter、addCatch、insertAt的String对象是由Javassist的编译器编译的。

    由于编译器支持语言扩展,所以以$开头的几个标识符都有特殊的含义:

    • $0、$1、$2,…
      传递给目标方法的参数使用$1、$2,…来访问,而不是原始的参数名称。$1表示第1个参数,$2表示第2个参数,以此类推。这些变量的类型与参数类型相同。$0等价于this指针。如果方法是静态的,则$0不可用。
    • $args
      变量$args表示所有参数的数组。该变量的类型是Object类型的数组。如果参数类型是原始类型(如int、boolean等),则该参数值将被转换为包装器对象(如java.lang.Integer)以存储在$args中。因此,如果第一个参数的类型不是原始类型,那么$args[0]等于$1。
    • " role="presentation">
      是所有参数列表的缩写,用逗号分隔。
    • $_
      CtMethod中的insertAfter方法是在方法的末尾插入编译的代码。在传递给 insertAfter的语句中,不但可以使用特殊符号,如$0、$1,也可以使用$_来表示方法的结果值。
      该变量的类型是方法的返回类型。如果返回结果类型是void,那么$_的类型为Object,$_的值为null。虽然由insertAfter插入的编译代码通常在方法返回之前执行,但是当方法抛出异常时,也可以执行。要在抛出异常时执行它,insertAfter的第二个参数asFinally必须为true。如果抛出异常,$_的值为0或null。在编译代码的执行终止后,最初抛出的异常被重新抛出给调用者。注意,$_的值不会被抛给调用者,而是被抛弃。
    • addCatch
      addCatch插入方法体抛出异常时执行的代码,控制权会返回给调用者。在插入的源代码中,异常用$e表示。
    CtMethod m = ...;
    CtClass etype = ClassPool.getDefault().get("java.io.IOException");
    m.addCatch("{System.out.println($e);throw $e}",etype);
    
    • 1
    • 2
    • 3

    转换成对应的java代码如下:

    try{
      //the original method body
    }catch(java.io.IOException e){
       System.out.println(e);
       throw e;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    注意,插入的代码片段必须以throw或return语句结束。

    案例

    首先,创建一个buildSrc文件夹,引入依赖

    apply plugin: 'groovy'//gradle会根据插件的名称,找到这个插件并且调用插件里面的apply方法
    
    repositories {
        maven { url 'https://maven.aliyun.com/repository/google' }
        maven {
            url 'https://maven.aliyun.com/nexus/content/groups/public/'
        }
    //    jcenter()
    }
    
    sourceSets {
        main {
            groovy {
                srcDir 'src/main/groovy'
            }
        }
    }
    
    dependencies {
    //    implementation gradleApi() //buildSrc目录下可以不引用gradleApi
        implementation 'com.android.tools.build:gradle:3.4.3'
    }
    
    group 'com.brett.gradle'
    //version '1.0.2'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    然后,在resources文件夹下面创建一个properties

    implementation-class=com.brett.gradle.ReleaseHelperPlugin
    
    • 1

    创建一个插件:

    package com.brett.gradle;
    
    import com.android.build.gradle.AppExtension;
    import com.android.build.gradle.api.ApplicationVariant
    import com.brett.gradle.tasks.GenerateApkTask
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    import org.gradle.api.invocation.Gradle
    
    import java.util.regex.Matcher
    import java.util.regex.Pattern;
    
    
    class ReleaseHelperPlugin implements Plugin<Project> {
    
        private static final String sPluginExtensionName = "releaseHelper";
        private static final String ANDROID_EXTENSION_NAME = "android";
        private Project project;
    
    
        @Override
        public void apply(Project project) {
            this.project = project;
           // project.getExtensions().create(sPluginExtensionName, CustomExtension.class,project);
            project.getExtensions().findByType(AppExtension).registerTransform(new ReleaseHelperTransform(project))
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    class ReleaseHelperTransform extends Transform {
        private static Project project
    
        ReleaseHelperTransform(Project project) {
            this.project = project
        }
    
        @Override
        String getName() {
            return "ReleaseHelperTransform"
        }
    
        /**
         * 需要处理的数据类型,有两种枚举类型
         * CLASSES 代表处理的 java 的 class 文件,RESOURCES 代表要处理 java 的资源
         * @return
         */
        @Override
        Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS
        }
    
        /**
         * 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
         * 1. EXTERNAL_LIBRARIES        只有外部库
         * 2. PROJECT                   只有项目内容
         * 3. PROJECT_LOCAL_DEPS        只有项目的本地依赖(本地jar)
         * 4. PROVIDED_ONLY             只提供本地或远程依赖项
         * 5. SUB_PROJECTS              只有子项目。
         * 6. SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)。
         * 7. TESTED_CODE               由当前变量(包括依赖项)测试的代码
         * @return
         */
        @Override
        Set<QualifiedContent.Scope> getScopes() {
            return TransformManager.SCOPE_FULL_PROJECT
        }
    
        @Override
        boolean isIncremental() {
            return false
        }
    
        @Override
        void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
            if (!incremental) {
                outputProvider.deleteAll()
            }
    
            /**Transform 的 inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历 */
            inputs.each { TransformInput input ->
                /**遍历 jar*/
                input.jarInputs.each { JarInput jarInput ->
                    /**重命名输出文件(同目录copyFile会冲突)*/
                    String destName = jarInput.file.name
    
                    /**截取文件路径的 md5 值重命名输出文件,因为可能同名,会覆盖*/
                    def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath).substring(0, 8)
                    /** 获取 jar 名字*/
                    if (destName.endsWith(".jar")) {
                        destName = destName.substring(0, destName.length() - 4)
                    }
                    def dest = outputProvider.getContentLocation(destName + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                    FileUtils.copyFile(jarInput.file, dest)
    
                    context.getTemporaryDir().deleteDir()
                }
    
                /**遍历目录*/
                input.directoryInputs.each { DirectoryInput directoryInput ->
                    CodeInjects.inject(directoryInput.file.absolutePath, project)
    
                    def dest = outputProvider.getContentLocation(directoryInput.name,
                            directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                    /**将input的目录复制到output指定目录*/
                    FileUtils.copyDirectory(directoryInput.file, dest)
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    
    import javassist.ClassPool
    import javassist.CtClass
    import javassist.CtMethod
    import org.gradle.api.Project
    
    public class CodeInjects {
        private final static ClassPool pool =  ClassPool.getDefault();
    
        public static void inject(String path, Project project){
    
            //当前路径加入类池,不然找不到这个类
            pool.appendClassPath(path)
    
            //project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
            pool.appendClassPath(project.android.bootClasspath[0].toString())
    
            File dir = new File(path)
            if(dir.isDirectory()){
                //遍历目录
                dir.eachFileRecurse {File file->
                    String filePath = file.absolutePath
                    println("CodeInjects filePath:"+filePath)
                    if(file.getName().equals("MainActivity.class")){
    
                        //获取MainActivity.class
                        CtClass ctClass = pool.getCtClass("com.sogou.teemo.test_use_gradle_plugin.MainActivity");
                        println("CodeInjects ctClass = "+ctClass)
    
                        if(ctClass.isFrozen()){
                            ctClass.defrost()
                        }
    
                        //获取到onCreate方法
                        CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate");
                        println("CodeInjects 方法名 = " + ctMethod)
    
                        String insetBeforeStr = """ android.widget.Toast.makeText(this,"插件中自动生成的代码",android.widget.Toast.LENGTH_SHORT).show();
                                                """
    
                        ctMethod.insertAfter(insetBeforeStr)
    
                        ctClass.writeFile(path)
    
                        ctClass.detach()//释放
    
                    }
                }
            }
    
    
        }
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    参考资料

    https://github.com/jboss-javassist/javassist/wiki/Tutorial-1

  • 相关阅读:
    edge 浏览器插件 demo --chatGpt
    GRE隧道技术
    C# 异步编程中的任务取消机制
    Oracle(54)什么是本地索引(Local Index)?
    绘制函数堆栈
    2021年12月电子学会图形化一级编程题解析含答案:下雨
    029-第三代软件开发-加载本地字体库
    flutter run可以运行,但是Android sync同步一直报错
    拓展了个新业务枚举类型,资损了
    【无标题】
  • 原文地址:https://blog.csdn.net/qq_36828822/article/details/127757949