前语

ASM 是一款读写Java字节码的工具,能够到达越过源码编写,编译,直接以字节码的形式创立类,修正现已存在类(或许jar中的class)的特点,办法等。 一般用来开发一些Java开发的辅助结构,其做法是在你编写的Java代码中注入一些特定代码(俗称字节码插装)到达特定意图,以Android开发为例最常用的办法经过字节码插装完成热修复,事件监听,埋点,开源结构等非常规操作,当然在Android开发中一般辅以Gradle插件一同运用,这个改天在写。

背景

早就听说过ASM和字节码插桩技术,可是工作中很少直接运用,因为近期有这个学习需求,特做此笔记,供有需求的同学依葫芦画瓢,也作为自己的参阅笔记以备后用。

据我实操进程中发现至少有以下三个当地能够获得ASM API的当地

1.ASM官网:asm.ow2.io/ 这里有从4.0到最新的9.3 所有版本,你能够下载到响应的jar包,还有运用手册asm.ow2.io/asm4-guide.…, 然后依赖到Java工程中即可,其API没有太大的差异。

2.jdk 自带的asm api(我的是JDK11)

3.gradle 自带的api(因为我是Android开发,我用了这种办法:运用的时候只需求增加依赖就行


dependencies {
    implementation gradleApi()
//    testImplementation 'org.ow2.asm:asm:7.1'
//    testImplementation 'org.ow2.asm:asm-commons:7.1'
}

为了更好的参阅字节码主张在Android Studio装置 ASM 相关插件,如图所示,装置一下3种中的一种即可,主张装置第三种(最多只能装置1种,不然你的Android studio 下次就无法重启了

Java ASM 框架:字节码操作的常见用法(生成类,修改类,方法插桩,方法注入)

四种ASM的常用运用场景,供有需求的同学做参阅

1.生成一个完好的类(包含几种根本特点和办法)

假如咱们想生成如下类


package com.study;
import java.util.ArrayList;
public class Human {
    private String name;
    private long age;
    protected int no;
    public static long score;
    public static final String real_name = "Sand哥";
    public Human() {
    }
    public int greet(String var1) {
        System.out.println(var1);
        ArrayList var2 = new ArrayList();
        StringBuilder var3 = new StringBuilder();
        var3.append("Hello java asm StringBuilder");
        long var4 = System.nanoTime();
        return 10 + 11;
    }
    public static void staticMethod(String var0) {
        System.out.println("Hello Java Asm!");
    }
}

用Java ASM 该如何生成(必要的当地有详细注释)


public static void testCreateAClass()throws Exception{
        //新建一个类生成器,COMPUTE_FRAMES,COMPUTE_MAXS这2个参数能够让asm主动更新操作数栈
        ClassWriter cw=new ClassWriter(COMPUTE_FRAMES|COMPUTE_MAXS);
        //生成一个public的类,类途径是com.study.Human
        cw.visit(V1_8,ACC_PUBLIC,"com/study/Human",null,"java/lang/Object",null);
        //生成默认的结构办法: public Human()
        MethodVisitor mv=cw.visitMethod(ACC_PUBLIC,"<init>","()V",null,null);
        mv.visitVarInsn(ALOAD,0);
        mv.visitMethodInsn(INVOKESPECIAL,"java/lang/Object","<init>","()V",false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(0,0);//更新操作数栈
        mv.visitEnd();//一定要有visitEnd
        //生成成员变量
        //1.生成String类型的成员变量:private String name;
        FieldVisitor fv= cw.visitField(ACC_PRIVATE,"name","Ljava/lang/String;",null,null);
        fv.visitEnd();//不要忘掉end
        //2.生成Long类型成员:private long age
        fv=cw.visitField(ACC_PRIVATE,"age","J",null,null);
        fv.visitEnd();
        //3.生成Int类型成员:protected int no
        fv=cw.visitField(ACC_PROTECTED,"no","I",null,null);
        fv.visitEnd();
        //4.生成静态成员变量:public static long score
        fv=cw.visitField(ACC_PUBLIC+ACC_STATIC,"score","J",null,null);
        //5.生成常量:public static final String real_name = "Sand哥"
        fv=cw.visitField(ACC_PUBLIC+ACC_STATIC+ACC_FINAL,"real_name","Ljava/lang/String;",null,"Sand哥");
        fv.visitEnd();
        //6.生成成员办法greet
        mv=cw.visitMethod(ACC_PUBLIC,"greet","(Ljava/lang/String;)I",null,null);
        mv.visitCode();
        mv.visitIntInsn(ALOAD,0);
        mv.visitIntInsn(ALOAD,1);
        //6.1 调用静态办法 System.out.println("Hello");
        mv.visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
//        mv.visitLdcInsn("Hello");//加载字符常量
        mv.visitIntInsn(ALOAD,1);//加载形参
        mv.visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);//打印形参
        //6.2 创立局部变量
        LocalVariablesSorter lvs=new LocalVariablesSorter(ACC_PUBLIC,"(Ljava/lang/String;)I",mv);
        //创立ArrayList 目标
        //new ArrayList ,分配内存不初始化
        mv.visitTypeInsn(NEW,"java/util/ArrayList");
        mv.visitInsn(DUP);//压入栈
        //弹出一个目标所在的地址,进行初始化操作,结构函数默以为空,此刻栈巨细为1(到现在只有一个局部变量)
        mv.visitMethodInsn(INVOKESPECIAL,"java/util/ArrayList","<init>","()V",false);
        int time=lvs.newLocal(Type.getType(List.class));
        mv.visitVarInsn(ASTORE,time);
        mv.visitVarInsn(ALOAD,time);
        //创立StringBuilder目标
        mv.visitTypeInsn(NEW,"java/lang/StringBuilder");
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKESPECIAL,"java/lang/StringBuilder","<init>","()V",false);
        //这里需求留意在lvs.newLocal的时候运用Type.geteType("类途径") 会报错,需求改成Type.geteType("XXX.class“)的办法
        time=lvs.newLocal(Type.getType(StringBuilder.class));
        mv.visitVarInsn(ASTORE,time);
        mv.visitVarInsn(ALOAD,time);
        mv.visitLdcInsn("Hello java asm StringBuilder");
        mv.visitMethodInsn(INVOKEVIRTUAL,"java/lang/StringBuilder","append","(Ljava/lang/String;)Ljava/lang/StringBuilder;",false);
        mv.visitMethodInsn(INVOKESTATIC,"java/lang/System","nanoTime","()J",false);
        time=lvs.newLocal(Type.LONG_TYPE);
        mv.visitVarInsn(LSTORE,time);
        mv.visitLdcInsn(10);
        mv.visitLdcInsn(11);
        mv.visitInsn(IADD);
        mv.visitInsn(IRETURN);
        mv.visitMaxs(0,0);
        mv.visitEnd();
        //生成静态办法
        mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "staticMethod", "(Ljava/lang/String;)V", null, null);
        //生成静态办法中的字节码指令
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("Hello Java Asm!");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
        //设置必要的类途径
        String path= PathUtils.getCurrentClassPath(DemoMain.class)+ File.separator+"Human.class";
        //获取类的byte数组
        byte[] classByteData=cw.toByteArray();
        //把类数据写入到class文件,这样你就能够把这个类文件打包供其他的人运用
        IOUtils.write(classByteData,new FileOutputStream(path));
        System.err.println("类生成的方位:"+path);
    }
留意点
1.ClassWriter(COMPUTE_FRAMES|COMPUTE_MAXS),这个ClassWriter的flag参数,主张设置这两种之和,按照官网所说,性能尽管差点,可是能够主动更新操作数栈和办法调用帧核算。
2.任何时候千万别忘掉visitEnd。
3.lvc 创立暂时变量的时候lvs.newLocal(Type.getType(“com/xx/YYY”));办法调用或许出错,改成如下办法 int time=lvs.newLocal(Type.getType(YYY.class));能正常运转,具体原因还没有来的研究。
4.生成类小结

经过这样的办法生成的class文件能够打包后供别人运用了,面向Java目标编程变成面向字节码编程,当然这种用法还有可读性更好的Javapoet 办法,这里不做评论

2.修正现已存在的类(增加特点,增加办法,修正办法等)

假设这个类代码如下(留意咱们修正的事class文件),asm代码将做3个当地修正

1.增加了一个phone字段 2.删去testA办法 3.将testC办法改成protected 4.新增一个getPhone办法

package com.test.javase_module;
public class TestFunction {
    private int a;
    public void testA(){
        System.out.println("I am A");
    }
    public void testB(){
        System.err.println("===>I am B");
    }
    public int testC(){
        return a;
    }
}

修正后的如下(修正后的class文件能够替换本来的文件从头打入jar包)

package com.test.javase_module;
public class TestFunction {
    private int a;
    //1.增加phone字段
    public String phone;
    public TestFunction() {
    }
    //2.现已删去了办法testA
    public void testB() {
        System.err.println("===>I am B");
    }
    //3.testC办法现已变成了protected
    protected int testC() {
        return this.a;
    }
    //4.增加了getPhone办法
    public String getPhone() {
        return this.phone;
    }
}

ASM代码如下

 private static void testModifyCalss()throws Exception{
        ClassReader cr = new ClassReader("com.test.javase_module.TestFunction");
        final ClassWriter cw=new ClassWriter(cr,0);
//        cr.accept(cw, 0);//能够直接承受一个writer,完成仿制
        cr.accept(new ClassVisitor(ASM4,cw) {//承受一个带classWriter的visitor,完成定制化办法复制或许特点删去字段
            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                System.out.println("visit method:"+name+"====> "+descriptor);
                if("testA".equals(name)){//复制的进程中删去一个办法
                    return null;
                }
                if("testC".equals(name)){//将testC public办法变成protect
                    access=ACC_PROTECTED;
                }
                return super.visitMethod(access, name, descriptor, signature, exceptions);
            }
            @Override
            public void visitEnd() {
                //特别留意的是:要为类增加特点和办法,放到visitEnd中,避免破坏之前现已摆放好的类结构,在结尾增加新结构
                //增加一个字段(留意不能重复),留意最终都要visitEnd
                FieldVisitor fv = cv.visitField(ACC_PUBLIC, "phone", "Ljava/lang/String;", null, null);
                fv.visitEnd();//不能缺少visitEnd
                //增加一个办法
                MethodVisitor mv=cv.visitMethod(ACC_PUBLIC,"getPhone","()Ljava/lang/String;",null,null);
                mv.visitCode();
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETFIELD,"com/test/javase_module/TestFunction","phone","Ljava/lang/String;");
                mv.visitInsn(IRETURN);
                mv.visitMaxs(1, 1);
                mv.visitEnd();//不能缺少visitEnd
                super.visitEnd();//留意原本的visiEnd不能少
            }
        },0);
        //指定新生成的class途径的生成方位,这个途径你能够随意指定
        String path=PathUtils.getCurrentClassPath(TestASM.class)+ File.separator+"TestFunction3.class";
        System.err.println("类生成的方位:"+path);
        IOUtils.write(cw.toByteArray(),new FileOutputStream(path));
    }

3.完成办法注入(用处广泛)

* 在一个办法的开端处和办法完毕处增加自己的代码
* 当然还能够用ASM自带的AdviceAdapter来完成更简略
* 本来的办法如下
     public void testB() {
         System.err.println("===>I am B");
     }
*插装后反编译如下
     public void testB() {
         long var1 = System.currentTimeMillis();
         System.err.println("===>I am B");
         long var3 = System.currentTimeMillis();
         System.out.println((new StringBuilder()).append("cost:").append(var3 - var1).toString());
     }

ASM 代码(留意避开结构办法)

public static void testInspectCode() throws IOException {
        ClassReader cr = new ClassReader("com.test.javase_module.TestFunction");
        //--------------------------------------------------------------------------------
        //1.在运用 new ClassWriter(0)时,不会主动核算任何东西。有必要自行核算帧、局部变 量与操作数栈的巨细。
        //--------------------------------------------------------------------------------
        //2.在运用 new ClassWriter(ClassWriter.COMPUTE_MAXS)时,将为你核算局部变量与操作数栈部分的巨细。
        // 还是有必要调用 visitMaxs,但能够运用任何参数:它们将被疏忽并从头核算。运用这一选项时,依然有必要自行核算这些帧。
        //--------------------------------------------------------------------------------
        //3.在 new ClassWriter(ClassWriter.COMPUTE_FRAMES)时,一切都是主动核算。不再需求调用 visitFrame,
        // 但依然有必要调用 visitMaxs(参数将被疏忽并从头核算)
        //--------------------------------------------------------------------------------
        ClassWriter cw=new ClassWriter(cr,ClassWriter.COMPUTE_FRAMES+ClassWriter.COMPUTE_MAXS);
        cr.accept(new ClassVisitor(ASM4, cw) {
            @Override
            public MethodVisitor visitMethod(int access, final String name, String descriptor, String signature, String[] exceptions) {
                MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
                return new MethodVisitor(ASM4,mv) {
                    @Override
                    public void visitLineNumber(int line, Label start) {
                        System.out.println("经过这个测验行数:"+line+"能够对应到源代码的行数");
                        super.visitLineNumber(line, start);
                    }
                    @Override
                    public void visitParameter(String name, int access) {
                        super.visitParameter(name, access);
                    }
                    public void visitInsn(int opcode) {
                        //这里是拜访句子完毕,在return完毕之前增加句子
                        //其中的 owner 有必要被设定为所转化类的名字。现在有必要在恣意 RETURN 之前增加其他四条
                        //指令,还要在任何 xRETURN 或 ATHROW 之前增加,它们都是停止该办法履行进程的指令。这些
                        //指令没有任何参数,因此在 visitInsn 办法中拜访。所以,能够重写这一办法,以增加指令:
                        if (!"<init>".equals(name) && (opcode >= Bytecodes.IRETURN && opcode <= Bytecodes.RETURN) || opcode == ATHROW) {
                            //在办法return之前增加代码
                            mv.visitMethodInsn(INVOKESTATIC,"java/lang/System",  "currentTimeMillis", "()J",false);
                            mv.visitIntInsn(Bytecodes.LSTORE,3);
                            mv.visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream");
                            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
                            mv.visitInsn(Bytecodes.DUP);
                            mv.visitMethodInsn(Bytecodes.INVOKESPECIAL,"java/lang/StringBuilder","<init>","()V",false);
                            mv.visitLdcInsn("cost:");//便是传入一个字符串常量
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/lang/StringBuilder","append","(Ljava/lang/String;)Ljava/lang/StringBuilder",false);
                            mv.visitVarInsn(LLOAD, 3);
                            mv.visitVarInsn(LLOAD,1);
                            mv.visitInsn(LSUB);
//
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/lang/StringBuilder","append","(J)Ljava/lang/StringBuilder",false);
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/lang/StringBuilder","toString","()Ljava/lang/String",false);
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);
                        }
                        mv.visitInsn(opcode);
                    }
                    @Override
                    public void visitCode() {
                        super.visitCode();
                        //办法开端(能够在此处增加代码,在本来的办法之前履行)
                        System.out.println("办法名字=========>"+name);
                        if(!"<init>".equals(name)){//不要在结构办法中增加代码
                            mv.visitMethodInsn(INVOKESTATIC,"java/lang/System","currentTimeMillis", "()J",false);
                            mv.visitIntInsn(Bytecodes.LSTORE,1);
                        }
                    }
                };
            }
        },0);
        String path=PathUtils.getCurrentClassPath(DemoMain.class)+ File.separator+"TestFunction4.class";
        System.err.println("类生成的方位:"+path);
        IOUtils.write(cw.toByteArray(),new FileOutputStream(path));
    }

4.注入办法调用

在办法的末尾插入了Tool.useTool()办法调用,这个办法具体内容能够自己随意写


* 在原有的办法中插入字节码指令中插入办法调用
* 在testInspectCode的办法基础上增加com.utils.Tool#useTool(long) 的办法调用
* 本来的办法
    public void testA(){
         System.out.println("I am A");
     }
* 插入字节码后反编译如下
   public void testA() {
         long var1 = System.currentTimeMillis();
         System.out.println("I am A");
         long var3 = System.currentTimeMillis();
         System.out.println((new StringBuilder()).append("cost:").append(var3 - var1).toString());
         Tool.useTool(var1);//在这里调用了第三方的办法,大批量的注入改用办法调用注入,能够节约注入的字节码量
     }

ASM代码

public static void testInspectCode2()throws Exception{
        ClassReader cr = new ClassReader("com.test.javase_module.TestFunction");
        ClassWriter cw=new ClassWriter(cr,0);
        cr.accept(new ClassVisitor(ASM4, cw) {
            @Override
            public MethodVisitor visitMethod(int access, final String name, String descriptor, String signature, String[] exceptions) {
                MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
                return new MethodVisitor(ASM4,mv) {
                    public void visitInsn(int opcode) {
               //这里是拜访句子完毕,在return完毕之前增加句子
               //其中的 owner 有必要被设定为所转化类的名字。现在有必要在恣意 RETURN 之前增加其他四条
               //指令,还要在任何 xRETURN 或 ATHROW 之前增加,它们都是停止该办法履行进程的指令。这些
                //指令没有任何参数,因此在 visitInsn 办法中拜访。所以,能够重写这一办法,以增加指令:
                        if("<init>".equals(name)){
                            return;
                        }
                        if ((opcode >= Bytecodes.IRETURN && opcode <= Bytecodes.RETURN) || opcode == ATHROW) {
                            //在办法return之前增加代码
                            mv.visitMethodInsn(INVOKESTATIC,"java/lang/System",  "currentTimeMillis", "()J",false);
                            mv.visitIntInsn(Bytecodes.LSTORE,3);
                            mv.visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream");
                            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
                            mv.visitInsn(Bytecodes.DUP);
                            mv.visitMethodInsn(Bytecodes.INVOKESPECIAL,"java/lang/StringBuilder","<init>","()V",false);
                            mv.visitLdcInsn("cost:");//便是传入一个字符串常量
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/lang/StringBuilder","append","(Ljava/lang/String;)Ljava/lang/StringBuilder",false);
                            mv.visitVarInsn(LLOAD, 3);
                            mv.visitVarInsn(LLOAD,1);
                            mv.visitInsn(LSUB);
//
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/lang/StringBuilder","append","(J)Ljava/lang/StringBuilder",false);
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/lang/StringBuilder","toString","()Ljava/lang/String",false);
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);
                            mv.visitVarInsn(LLOAD, 1);
                            mv.visitMethodInsn(INVOKESTATIC,"com/utils/Tool","useTool","(J)V",false);
                        }
                        mv.visitInsn(opcode);
                    }
                    @Override
                    public void visitCode() {
                        super.visitCode();
                        //办法开端(能够在此处增加代码,在本来的办法之前履行)
                        if(!"<init>".equals(name)){
                            mv.visitMethodInsn(INVOKESTATIC,"java/lang/System","currentTimeMillis", "()J",false);
                            mv.visitIntInsn(Bytecodes.LSTORE,1);
                        }
                    }
                };
            }
        },0);
        String path=PathUtils.getCurrentClassPath(DemoMain.class)+ File.separator+"TestFunction4.class";
        System.err.println("类生成的方位:"+path);
        IOUtils.write(cw.toByteArray(),new FileOutputStream(path));
    }

5.小结

ASM 功能其实很强壮,官方文档中有更为丰厚的应用场景介绍。

ps:辛苦找到了一份中文文档并和代码一同放在了github中,因为整个项目不方便一同共享,只放出了来用到java module代码(不会影响运转,并且是完好示例),把github上的源代码粘贴到一个Android studio中的Java module 中即可运转) 如下图

Java ASM 框架:字节码操作的常见用法(生成类,修改类,方法插桩,方法注入)

github:github.com/woshiwzy/as…