美文网首页
javassist 动态修改字节码

javassist 动态修改字节码

作者: feifei_fly | 来源:发表于2019-07-24 11:33 被阅读0次

    一、gradle Transform 接收一个输入input,同时需要有一组输出,作为下一个Transform的输入。

    (1)最简单的一个Transform实现,需要实现
    将输入数据input,原样不动输出到output
    (2)Transform处理的结果,会位于工程目录⁩/build⁩/⁨intermediates⁩/transform文件夹下。
    如下图XXX目录即为自定义的一个Transfrom。
    由图可知除XXX外,还经过了dexBuilder、dexMerger、mergeJavaRes、mergeJniLibs、StripDebugSymbol等多个Transform处理

    image.png

    二、自定义gradle插件实例

    1、自定义gradle插件的build.gradle

    apply plugin: 'groovy'
    apply plugin: 'maven'
    apply plugin: 'java'
    apply plugin: 'maven-publish'
    
    dependencies {
    //    implementation fileTree(dir: 'libs', include: ['*.jar'])
    
        compile gradleApi()//gradle sdk
        compile localGroovy()//groovy sdk
    
    
        //build tools
        compile 'com.android.tools.build:gradle:3.1.2'
        //transform
        compile 'com.android.tools.build:transform-api:1.5.0'
        //javassit
        compile 'javassist:javassist:3.12.1.GA'
        //commons-io
        compile 'commons-io:commons-io:2.5'
    }
    repositories {
        jcenter()
        google()//加在这里
    }
    
    
    

    SecondPlugin.groovy 自定义插件,内部为android注册了一个ReClassTransform 接口。

    package com.feifei.second
    
    import com.android.build.gradle.AppExtension
    import com.android.build.gradle.AppPlugin
    import com.feifei.second.transform.ReClassTransform
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    
    public class SecondPlugin implements Plugin<Project>{
    
        void apply(Project project){
            System.out.println("==========")
            System.out.println("feifei  第二个内部用插件")
            System.out.println("==========")
    
            project.extensions.create("pluginExt",PluginExtension)
            project.pluginExt.extensions.create("nestExt", PluginNestExtension)
            project.task('customTask',type:CustomTask)
    
            def isApp = project.plugins.getPlugin(AppPlugin)
    
            if(isApp){
                def android =  project.extensions.getByType(AppExtension)
                android.registerTransform(new ReClassTransform(project))
            }
        }
    }
    

    最原始的Transform实现。
    ReClassTransfrom.groovy

    package com.feifei.second.transform
    import com.android.build.api.transform.*
    import com.android.build.api.transform.Context
    import com.android.build.api.transform.DirectoryInput
    import com.android.build.api.transform.JarInput
    import com.android.build.api.transform.QualifiedContent
    import com.android.build.api.transform.Transform
    import com.android.build.api.transform.TransformInput
    import com.android.build.api.transform.TransformOutputProvider
    import com.android.utils.FileUtils
    import org.apache.commons.codec.digest.DigestUtils
    import org.gradle.api.Project
    import org.gradle.internal.impldep.org.apache.ivy.util.FileUtil
    import org.gradle.jvm.tasks.Jar
    import com.android.build.gradle.internal.pipeline.TransformManager
    
    import javax.xml.crypto.dsig.TransformException
    
    public class ReClassTransform extends Transform{
    
        private Project mProject;
    
        public ReClassTransform(Project p){
            this.mProject = p;
        }
    
        //transform的名称
        /**
         * 最终运行的名字为 transformClassWith+getName()+For+{BuildType}+{ProductFlavor}
         * 如 transformClassWithXXXForDebug
         * @return
         */
        @Override
        String getName() {
            return "XXX"
        }
    
        /**
         * 需要处理的数据类型,有两种枚举类型
         * CLASSES和RESOURCES,CLASSES代表处理的java的class文件;RESOURCES代表要处理java的资源.
         * @return
         */
        @Override
        Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS;
        }
    
        /**
         * 指Transform要操作内容的范围,官方文档Scope有7种类型:
         * EXTERNAL_LIBRARIES   只有外部库
         * PROJECT              只有项目内容
         * PROJECT_LOCAL_DEPS   只有项目的本地依赖(本地jar)
         * PROVIDED_ONLY        只提供本地或远程依赖项
         * SUB_PROJECTS         只有子项目
         * SUB_PROJECTS_LOCAL_DEPS 只有子项目的本地依赖项(本地jar)。
         * TESTED_CODE          由当前变量(包括依赖项)测试的代码
         * @return
         */
        @Override
        Set<? super QualifiedContent.Scope> getScopes() {
            return TransformManager.SCOPE_FULL_PROJECT;
        }
    //指明当前Transform是否支持增量编译
        @Override
        boolean isIncremental() {
            return false
        }
    
        /**
         * Transform中的核心方法,
         *
         * @param context 。
         * @param inputs  传过来的输入流, 其中有两种格式,一种是jar包格式一种是目录格式
         * @param referencedInputs
         * @param outputProvider  获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
         * @param isInCremental
         * @throws IOException
         * @throws TransformException
         */
        @Override
        public void transform(Context context,
                              Collection<TransformInput> inputs,
                Collection<TransformInput> referencedInputs,
                TransformOutputProvider outputProvider,
                boolean isInCremental
        ) throws IOException, TransformException{
    
            welecome()
    
            inputs.each { TransformInput input->
    
                //遍历目录
                input.directoryInputs.each { DirectoryInput directoryInput ->
    
                    println "direction = "+directoryInput.file.getAbsolutePath()
                    //获取输出目录
                    def dest = outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes,Format.DIRECTORY)
    
                    //对于目录中的class文件原样输出
                    FileUtils.copyDirectory(directoryInput.file,dest)
                }
    
                //遍历jar文件,对jar不操作,但是要输出到out目录
                input.jarInputs.each { JarInput jarInput->
    
                    // 将jar文件 重命名输出文件(同目录copyFile会冲突)
                    def jarName = jarInput.name
                    println "jar = "+jarInput.file.getAbsolutePath()
    
                    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)
                }
    
            }
    
            end()
    
        }
    
    
        def welecome(){
            println "----welcome to ReClassTransform"
        }
    
        def end(){
            println "----ReClassTransform end"
        }
    }
    

    执行./gradlew :test_gradle_use_plugin:assembleDebug
    时的输出内容。

    > Task :test_gradle_use_plugin:transformClassesWithXXXForDebug 
    ----welcome to ReClassTransform
    jar = /Users/feifei/.gradle/caches/transforms-1/files-1.1/constraint-layout-1.1.0.aar/ad39ea76672d18218cf29f42ea94a4d7/jars/classes.jar
    jar = /Users/feifei/.gradle/caches/modules-2/files-2.1/com.android.support.constraint/constraint-layout-solver/1.1.0/931532e953a477f876f2de18c2e7f16eee01078f/constraint-layout-solver-1.1.0.jar
    direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/intermediates/classes/debug
    direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/tmp/kotlin-classes/debug
    ----ReClassTransform end
    
    

    2、利用向文件中写入字符串的形式直接生成类文件

    Hostconfig.groovy
    增加HostConfig的调用

    package com.feifei.second.hostconfig
    
    
    public class HostConfig {
        static def void createHostConfig(variant,config){
    
    def content = """
    package com.sogou.teemo.test_use_gradle_plugin;
    public class TheHostConfig{
        public static final String ip = "${config.param1}";
        public static final String port = "5050"; 
    
    }
    
    """
    
            File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()
            println "feifei createHostConfig outputDir:"+outputDir.getAbsolutePath()
            def javaFile = new File(outputDir, "TheHostConfig.java")
            javaFile.write(content,'UTF-8')
    
        }
    }
    

    SecondPlugin.groovy

    package com.feifei.second
    
    import com.android.build.gradle.AppExtension
    import com.android.build.gradle.AppPlugin
    import com.android.build.gradle.api.ApplicationVariant
    import com.android.repository.impl.meta.Archive
    import com.feifei.second.hostconfig.HostConfig
    import com.feifei.second.transform.ReClassTransform
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    
    public class SecondPlugin implements Plugin<Project>{
    
        void apply(Project project){
            System.out.println("==========")
            System.out.println("feifei  第二个内部用插件")
            System.out.println("==========")
    
            project.extensions.create("pluginExt",PluginExtension)
            project.pluginExt.extensions.create("nestExt", PluginNestExtension)
            project.task('customTask',type:CustomTask)
    
            def isApp = project.plugins.getPlugin(AppPlugin)
    
            if(isApp){
                def android =  project.extensions.getByType(AppExtension)
                android.registerTransform(new ReClassTransform(project))
    
    
    
                android.applicationVariants.all { variants->
    
                    def variantData =  variants.variantData
                    def scope = variantData.scope
    
                    println "feifei current scope:"+scope
    
                    //scope.getTaskName 的作用 就是结合当前scope 拼接人物名
                    def taskName = scope.getTaskName("CreateHostConfig")
                    def createTask = project.task(taskName)
    
                    println "feifei CreateHostConfigTaskName:"+taskName
    
                    //自定义task 增加action
                    createTask.doLast {
                        HostConfig.createHostConfig(variants,project.pluginExt)
                    }
    
                    String generateBuildConfigTaskName = scope.getGenerateBuildConfigTask().name
                    def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
                    println "feifei  generateBuildConfigTaskName:"+generateBuildConfigTaskName
    
                    if(generateBuildConfigTask){
                        createTask.dependsOn generateBuildConfigTask
                        generateBuildConfigTask.finalizedBy(createTask)//执行完generateBuildConfigTask之后,执行createTask任务
                    }
                }
            }
        }
    }
    

    执行 ./gradlew clean :test_gradle_use_plugin:assembleDebug
    输出如下:

     Configure project :test_gradle_use_plugin 
    ==========
    feifei  第二个内部用插件
    ==========
    feifei current scope:VariantScopeImpl{debug}
    feifei CreateHostConfigTaskName:CreateHostConfigDebug
    feifei  generateBuildConfigTaskName:generateDebugBuildConfig
    feifei current scope:VariantScopeImpl{release}
    feifei CreateHostConfigTaskName:CreateHostConfigRelease
    feifei  generateBuildConfigTaskName:generateReleaseBuildConfig
    
    > Task :test_gradle_use_plugin:transformClassesWithXXXForDebug 
    ----welcome to ReClassTransform
    jar = /Users/feifei/.gradle/caches/transforms-1/files-1.1/constraint-layout-1.1.0.aar/5ae74cdeff58ee396218df991052866b/jars/classes.jar
    jar = /Users/feifei/.gradle/caches/modules-2/files-2.1/com.android.support.constraint/constraint-layout-solver/1.1.0/931532e953a477f876f2de18c2e7f16eee01078f/constraint-layout-solver-1.1.0.jar
    direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/intermediates/classes/debug
    direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/tmp/kotlin-classes/debug
    ----ReClassTransform end
    
    
    
    
    

    生成类文件的位置:

    image.png

    3、利用javassist 向现有类中动态插入代码

    Javassist是一个动态类库,可以用来检查、”动态”修改以及创建 Java类。其功能与jdk自带的反射功能类似,但比反射功能更强大
    ClassPool:javassist的类池,使用ClassPool 类可以跟踪和控制所操作的类,它的工作方式与 JVM 类装载器非常相似,
    CtClass: CtClass提供了检查类数据(如字段和方法)以及在类中添加新字段、方法和构造函数、以及改变类、父类和接口的方法。不过,Javassist 并未提供删除类中字段、方法或者构造函数的任何方法。
    CtField:用来访问域
    CtMethod :用来访问方法
    CtConstructor:用来访问构造器
    
    

    新建
    CodeInjects.groovy 用于想MainActivity中动态插入代码

    package com.feifei.second.codeinject
    
    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())
    
            pool.importPackage("android.os.Bundle");
            pool.importPackage(" android.app.Activity")
    
            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()//释放
    
                    }
                }
            }
    
    
        }
    
    
    }
    

    ReClassTransform.groovy中,遍历class文件时,调用CodeInjects.inject(directoryInput.file.absolutePath,mProject)。过滤出MainActivity.class并动态修改onCreate()方法

      /**
         * Transform中的核心方法,
         *
         * @param context 。
         * @param inputs  传过来的输入流, 其中有两种格式,一种是jar包格式一种是目录格式
         * @param referencedInputs
         * @param outputProvider  获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
         * @param isInCremental
         * @throws IOException
         * @throws TransformException
         */
        @Override
        public void transform(Context context,
                              Collection<TransformInput> inputs,
                Collection<TransformInput> referencedInputs,
                TransformOutputProvider outputProvider,
                boolean isInCremental
        ) throws IOException, TransformException{
    
            welecome()
    
            inputs.each { TransformInput input->
    
                //遍历目录
                input.directoryInputs.each { DirectoryInput directoryInput ->
    
                    println "direction = "+directoryInput.file.getAbsolutePath()
    
                    CodeInjects.inject(directoryInput.file.absolutePath,mProject)
                    //获取输出目录
                    def dest = outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes,Format.DIRECTORY)
    
                    //对于目录中的class文件原样输出
                    FileUtils.copyDirectory(directoryInput.file,dest)
                }
    
                //遍历jar文件,对jar不操作,但是要输出到out目录
                input.jarInputs.each { JarInput jarInput->
    
                    // 将jar文件 重命名输出文件(同目录copyFile会冲突)
                    def jarName = jarInput.name
                    println "jar = "+jarInput.file.getAbsolutePath()
    
                    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)
                }
    
            }
    
            end()
    
        }
    
    

    将test_gradle_use_plugin-debug.apk 反编译后,如下图所示:


    image.png

    Github: 查看buildSrc 和test_gradle_use_plugin 两个module

    四、相关知识背景

    1、Transfrom API

    基于Gradle的Transform API,在编译期的构建任务流中,class转为dex之前,插入一个Transform,并在此Transform流中,基于Javassist实现对字节码文件的注入。
    [图片上传失败...(image-317838-1563938953106)]
    http://google.github.io/android-gradle-dsl/javadoc/current/

    2、javassist

    Javassist是一个动态类库,可以用来检查、”动态”修改以及创建 Java类.其功能与jdk自带的反射功能类似,但比反射功能更强大.

    • ClassPool:javassist的类池,使用ClassPool 类可以跟踪和控制所操作的类,它的工作方式与 JVM 类装载器非常相似。
    • CtClass: CtClass提供了检查类数据(如字段和方法)以及在类中添加新字段、方法和构造函数、以及改变类、父类和接口的方法。不过,Javassist 并未提供删除类中字段、方法或者构造函数的任何方法。
    • CtField:用来访问域
    • CtMethod :用来访问方法
    • CtConstructor:用来访问构造器
    • insertClassPath:为ClassPool添加搜索路径,否则ClassPool 无法找打对应的类
         classPool.insertClassPath(new ClassClassPath(String.class));
                classPool.insertClassPath(new ClassClassPath(Person.class));
                classPool.insertClassPath("/Users/feifei/Desktop/1");
    
    • classPool.get(className);加载一个类

    • classPool.makeClass(className);//创建一个类

    • CtClass.addField();CtClass.addMethod(); 添加方法和属性

      CtField ageField = new CtField(CtClass.intType,"age",stuClass);
                stuClass.addField(ageField);
        CtMethod setMethod = CtMethod.make("public void setAge(int age) { this.age = age;}",stuClass);
    
                stuClass.addMethod(getMethod);
    
    • Class<?>clazz = stuClass.toClass();将CtCLass对象转化为JVM对象

    创建一个类,并写入到本地文件

     public static void testCreateClass(){
    
            System.out.println("testCreateClass");
            //创建ClassPool
            ClassPool classPool = ClassPool.getDefault();
    
            //添加类路径
    //        classPool.insertClassPath(new ClassClassPath(this.getClass()));
            classPool.insertClassPath(new ClassClassPath(String.class));
            //创建类
            CtClass stuClass = classPool.makeClass("com.feifei.Student");
    
            //加载类
            //classPool.get(className)
            try {
                //添加属性
                CtField idField = new CtField(CtClass.longType,"id",stuClass);
                stuClass.addField(idField);
    
                CtField nameField = new CtField(classPool.get("java.lang.String"),"name",stuClass);
                stuClass.addField(nameField);
    
                CtField ageField = new CtField(CtClass.intType,"age",stuClass);
                stuClass.addField(ageField);
    
    
                //添加方法
                CtMethod getMethod = CtMethod.make("public int getAge(){return this.age;}",stuClass);
                CtMethod setMethod = CtMethod.make("public void setAge(int age) { this.age = age;}",stuClass);
    
                stuClass.addMethod(getMethod);
                stuClass.addMethod(setMethod);
    
                //toClass 将CtClass 转换为java.lang.class
                Class<?>clazz = stuClass.toClass();
                System.out.println("testCreateClass clazz:"+clazz);
    
                System.out.println("testCreateClas ------ 属性列表 -----");
                Field[] fields = clazz.getDeclaredFields();
                for(Field field:fields){
                    System.out.println("testCreateClass"+field.getType()+"\t"+field.getName());
                }
    
                System.out.println("testCreateClass ------ 方法列表 -----");
    
                Method[] methods = clazz.getDeclaredMethods();
                for(Method method:methods){
                    System.out.println("feifei  "+method.getReturnType()+"\t"+method.getName()+"\t"+ Arrays.toString(method.getParameterTypes()));
                }
    
                stuClass.writeFile("/Users/feifei/Desktop/1");
            } catch (CannotCompileException e) {
                e.printStackTrace();
            } catch (NotFoundException e) {
                e.printStackTrace();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
    
                //将stuClass 从ClassPool 移除
                if(stuClass != null){
                    stuClass.detach();
                }
            }
    
        }
    
    

    修改一个类的父类

    
    package com.example.myjavassist;
    
    public class Person {
    }
    
    public static void testSetSuperClass(){
    
            System.out.println("testSetSuperClass");
            //创建ClassPool
            ClassPool classPool = ClassPool.getDefault();
    
    
            try {
                //添加类路径
                classPool.insertClassPath(new ClassClassPath(String.class));
                classPool.insertClassPath(new ClassClassPath(Person.class));
                classPool.insertClassPath("/Users/feifei/Desktop/1");
    
                // 加载类
                //创建类
                CtClass stuClass = classPool.get("com.feifei.Student");
                CtClass personClass = classPool.get("com.example.myjavassist.Person");
    
                if(stuClass.isFrozen()){
                    stuClass.freeze();
                }
                stuClass.setSuperclass(personClass);
    
                //toClass 将CtClass 转换为java.lang.class
                Class<?>clazz = stuClass.toClass();
                System.out.println("testSetSuperClass ------ 属性列表 -----");
                Field[] fields = clazz.getDeclaredFields();
                for(Field field:fields){
                    System.out.println("testCreateClass"+field.getType()+"\t"+field.getName());
                }
    
                System.out.println("testSetSuperClass ------ 方法列表 -----");
    
                Method[] methods = clazz.getDeclaredMethods();
                for(Method method:methods){
                    System.out.println("testSetSuperClass  "+method.getReturnType()+"\t"+method.getName()+"\t"+ Arrays.toString(method.getParameterTypes()));
                }
    
                stuClass.writeFile("/Users/feifei/Desktop/1");
                personClass.writeFile("/Users/feifei/Desktop/1");
    
            } catch (NotFoundException | CannotCompileException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
    
            }
        }
    
    image.png

    方法重命名、复制方法、新建方法,添加方法体。

    
    package com.example.myjavassist;
    
    public class Calculator {
    
        public void getSum(long n) {
            long sum = 0;
            for (int i = 0; i < n; i++) {
                sum += i;
            }
            System.out.println("n="+n+",sum="+sum);
        }
    
    }
    
    
     public static void testInsertMethod(){
    
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = null;
            try {
                ctClass = pool.get("com.example.myjavassist.Calculator");
    
                //获取类中现有的方法
                String getSumName = "getSum";
                CtMethod methodOld = ctClass.getDeclaredMethod(getSumName);
    
    
                String methodNewName = getSumName+"$impl";
                //修改原有方法的方法名
                methodOld.setName(methodNewName);
    
    
                //创建一个新的方法getSumName,并将旧方法 复制成新方法中.
                CtMethod newMethod = CtNewMethod.copy(methodOld,getSumName,ctClass,null);
    
                //设置新newMethod的方法体
                StringBuffer body = new StringBuffer();
                body.append("{\nlong start = System.currentTimeMillis();\n");
                // 调用原有代码,类似于method();($$)表示所有的参数
                body.append(methodNewName + "($$);\n");
                body.append("System.out.println(\"Call to method " + methodNewName
                        + " took \" +\n (System.currentTimeMillis()-start) + " + "\" ms.\");\n");
                body.append("}");
    
                newMethod.setBody(body.toString());
    
                //为类新添加方法
                ctClass.addMethod(newMethod);
    
                Calculator calculator =(Calculator)ctClass.toClass().newInstance();
                calculator.getSum(10000);
    
                //将类输出到文件
                ctClass.writeFile("/Users/feifei/Desktop/1");
    
            } catch (NotFoundException e) {
                e.printStackTrace();
            } catch (CannotCompileException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }catch (Exception e){
                e.printStackTrace();
            }
            finally {
                if(ctClass!=null){
                    ctClass.detach();
                }
            }
        }
    
    image.png

    Github: 选择 myjavaassit module

    五、参考文章

    https://www.jianshu.com/p/a6be7cdcfc65

    https://www.jianshu.com/p/a9b3aaba8e45

    https://blog.csdn.net/top_code/article/details/51708043

    http://www.javassist.org/tutorial/tutorial2.html

    javassit github:
    https://github.com/jboss-javassist/javassist

    相关文章

      网友评论

          本文标题:javassist 动态修改字节码

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