ASM字节码处理工具原理及实践(二)

news2024/12/23 13:49:15

0. 相关分享

ASM字节码处理工具原理及实践(一)

上一篇讲了ASM的简介、导入,以及字节码文件结构,并给出了ASM通过ClassVisitor对class进行访问的基础实战。本篇将进入MethodVisitor,尝试对方法进行访问、生成、转换。方法的代码存储为字节码指令序列。在此之前,我们需要先复习JVM栈结构,才能更好地理解方法中字节码指令的逻辑。

1. JVM栈结构

一个JVM栈中包含了若干个栈帧,表征着一个个方法的调用栈。一个栈帧中存储着:

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack)
  • 动态链接(Dynamic Linking)指向运行时常量池的方法引用(MethodRef)
  • 方法返回地址(Return Address)方法正常退出或异常退出的定义
  • 附加信息

每个线程都有自己各自的栈,栈是线程私有的。栈帧的大小主要由局部变量表和操作数栈决定。操作数栈的深度、局部变量表的长度在编译器就已经确定,并写入到字节码中。如果栈帧只展示局部变量表和操作数栈,一个执行堆栈可能会为如下形式:

请添加图片描述

1.1 局部变量表

局部变量表也称为 局部变量数组 或 本地变量表。最基本的存储单元是 Slot (变量槽),参数值的存放总是在局部变量数组的 index = 0 开始,直到 数组长度-1 的索引结束。

局部变量表中,32位以内的类型占用一个 slot, 64位的类型(long、double)占用两个 slot。上图中的 L1、L2 等都忽略了slot的个数,可能L2是double类型,那么它应当占用 2 个 slot。局部变量表的长度按 slot 的个数计算。

1.2 操作数栈

独立的栈帧除了包含有局部变量表之外,还包含一个后进先出的操作数栈(Operand Stack)。操作数栈,在方法执行的执行过程中,根据字节码指令,往栈中写入数据或者提取数据,即入栈(push)和出栈(pop)

某些字节码指令将值压入操作数栈,其余的字节码指令将操作数栈取出栈,使用它们之后再把执行结果压入栈。

与局部变量表类似的,栈中可以是任意类型的Java数据类型

  • 32位的类型占用一个栈单位深度
  • 64位的类型占用两个栈单位深度

2. 字节码指令

字节码指令由操作码 opcode 和操作数 arguments 表征:

  • 操作码 opcode 是一个无符号的字节值,是代号,由助记符标识。
  • 操作数 arguments 是定义精确指令行为的静态值。

根据字节码指令和操作数栈的关系,字节码指令可以分为两类:一类字节码指令设计用于将值从局部变量表转移到操作数栈中,反之亦然;另一类则只操作操作数栈,它们从操作数栈弹出一些值,根据这些值进行计算,将结果重新压入操作数栈栈顶。

例如 ILOAD、LLOAD、FLOAD、DLOAD和ALOAD指令读取一个局部变量的值,并将这个值压入到操作数栈中。由于这些指令操作的是局部变量表,需要提供表/数组的索引 i 作为参数,来表示读取哪一个局部变量。我们之前提到32位以内的类型都存入到一个 slot 中,反过来,提取的时候这里 ILOAD 可以用于加载 boolean、byte、char、short和int类型的局部变量。不仅如此,我们也说了 64位 的局部变量将会占用两个插槽 slot ,故LLOAD、DLOAD加载数据时候,实际上加载了 i 和 i+1 两个插槽的内容。ALOAD 用于加载其他类型,比引用类型、数组引用类型等。

我们可以观察到 xLOAD和 xSTORE指令都是由x表征类型的,者用来确保不进行非法的转换。

除了上述 xLOAD 和 xSTORE 的指令之外,其他字节码指令只在操作数栈上工作。这里给出字节码指令的汇总:

注意:

  • a 和 b 表示 int、float、long、double类型
  • o 和 p 表示 对象引用类型
  • v 表示单位为1的类型
  • w 表示long、double这样单位为2的类型
  • i 和 j 和 n 表示 int 类型

2.1 Local variables 局部变量

指令栈(原先)栈(指令执行后)
ILOAD,LLOAD,FLOAD,DLOAD var…, a
ALOAD var…, o
ISTORE,LSTORE,FSTORE,DSTORE var…, a
ASTORE var…, o
IINC var incr

示例:

【局部变量压栈指令】将一个局部变量加载到操作数栈:xload 、xload_<n>(其中x为i、l、f、d、a, n从0到3

aload_0 // 将局部变量表中0号局部变量的值压入到操作数栈
aload 5 // 将局部变量表中5号局部变量的值压入操作数栈

【出栈装入局部变量表指令】将一个数值从操作数栈存储到局部变量表:xstore、xstore_<n>(其中x为i、l、f、d、a,n为0到3);xastore(其中x为i、l、f、d、a、b、c、s)

2.2 Stack 操作数栈

指令栈(原先)栈(指令执行后)
POP…, v
POP2…, v1, v2
…, w
DUP…, v…, v , v
DUP2…, v1, v2…, v1, v2, v1, v2
…, w…, w, w
SWAP…, v1, v2…, v2, v1
DUP_X1…, v1, v2…, v2, v1, v2
DUP_X2…, v1, v2, v3…, v3, v1, v2, v3
…, w, v…, v, w, v
DUP2_X1…, v1, v2, v3…, v2, v3, v1, v2, v3
…, v, w…, w, v, w
DUP2_X2…, v1, v2, v3, v4…, v3, v4, v1, v2, v3, v4
…, w, v1, v2…, v1, v2, w, v1, v2
…, v1, v2, w…, w, v1, v2, w
…, w1, w2…, w2, w1, w2

2.3 Constants 常量操作

指令栈(原先)栈(指令执行后)
ICONST_n (1 n 5)… , n
LCONST_n (0 n 1)… , nL
FCONST_n (0 n 2)… , nF
DCONST_n (0 n 1)… , nD
BIPUSH b, 128 b < 127… , b
SIPUSH s, 32768 s < 32767… , s
LDC cst (int, float, long, double, String or Type)… , cst
ACONST_NULL… , null

【常量入栈指令】将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>)、fconst_<f>、dconst_<d>

示例:

ldc指令可以接受一个8位的参数,指向常量池中intfloat或者String的索引,并将指定的内容压入操作数栈
例如:
    ldc #9 , 常量池表#9为“hello”,将这个字符串索引压入栈中
ldc_w 把常量池中的项压入栈,w表示使用宽索引,款缩影支持索引范围大于ldc,接收两个8位的参数
ldc2_w 把常量池中 long 或者 double 类型的项压入栈
aconst_null 把null压入操作数栈
fconst_0 把浮点数0压入栈

2.4 Arithmetic and logic 计算和逻辑运算

指令栈(原先)栈(指令执行后)
IADD,LADD,FADD,DADD…, a, b…, a+b
ISUB,LSUB,FSUB,DSUB…, a, b…, a- b
IMUL, LMUL, FMUL, DMUL… , a , b… , a * b
IDIV, LDIV, FDIV, DDIV… , a , b… , a / b
IREM, LREM, FREM, DREM… , a , b… , a % b
INEG, LNEG, FNEG, DNEG (negtive)… , a… , -a
ISHL, LSHL (left)… , a , n… , a << n
ISHR, LSHR (right)… , a , n… , a >> n
IUSHR, LUSHR… , a , n… , a >>> n
IAND, LAND… , a , b… , a & b
IOR, LOR… , a , b… , a | b
IXOR, LXOR… , a , b… , a ^ b
LCMP… , a , b… , a == b ? 0 : (a < b ? -1 : 1)
FCMPL, FCMPG… , a , b… , a == b ? 0 : (a < b ? -1 : 1)
DCMPL, DCMPG… , a , b… , a == b ? 0 : (a < b ? -1 : 1)

2.4 Cast类型转换

指令栈(原先)栈(指令执行后)
I2B… , i… , (byte) i
I2C… , i… , (char) i
I2S… , i… , (short) i
L2I, F2I, D2I… , a… , (int) a
I2L, F2L, D2L… , a… , (long) a
I2F, L2F, D2F… , a… , (float) a
I2D, L2D, F2D… , a… , (double) a
CHECKCAST class… , o… , (class) o

从byte、char、short类型到int类型的宽化类型转换实际上是不存在的,虚拟机对这种类型转换并没有做实质性的转化处理,只是通过操作数栈交换了两个数据。

窄化数据转换可能发生精度丢失,可能丢失掉几个最低有效位上的值,转换后的浮点数值根据IEEE754最接近含入模式所得到的正确整数值。窄化类型转换可能会发生上限溢出、下限溢出和精度丢失等情况。

2.5 Objects对象、Field字段、Method方法

c: class类, f: field字段名, m:method方法名, t: description描述符

指令栈(原先)栈(执行后)
NEW class…, new class
GETFIELD c f t…, o… , o.f
PUTFIELD c f t… , o , v
GETSTATIC c f t… , c.f
PUTSTATIC c f t… , v
INVOKEVIRTUAL c m t… , o , v1 , … , vn… , o.m(v1, … vn)
INVOKESPECIAL c m t… , o , v1 , … , vn… , o.m(v1, … vn)
INVOKESTATIC c m t… , v1 , … , vn… , c.m(v1, … vn)
INVOKEINTERFACE c m t… , o , v1 , … , vn… , o.m(v1, … vn)
INVOKEDYNAMIC m t bsm… , o , v1 , … , vn… , o.m(v1, … vn)
INSTANCEOF class… , o… , o instanceof class
MONITORENTER… , o
MONITOREXIT… , o

2.6 Arrays集合

指令栈(原先)栈(指令执行后)
NEWARRAY type (for any primitive type)… , n… , new type[n]
ANEWARRAY class… , n… , new class[n]
MULTIANEWARRAY […[t n… , i1 , … , in… , new t[i1]…[in]…
BALOAD, CALOAD, SALOAD… , o , i… , o[i]
IALOAD, LALOAD, FALOAD, DALOAD… , o , i… , o[i]
AALOAD… , o , i… , o[i]
BASTORE, CASTORE, SASTORE… , o , i , j
IASTORE, LASTORE, FASTORE, DASTORE… , o , i , a
AASTORE… , o , i , p
ARRAYLENGTH… , o… , o.length

示例:

public class Student {
    public String[] infos;

    public String getInfo(int index){
        return infos[index];
    }
}

其中 getInfo(int index) 的字节码指令为:

0 aload_0 // 先将 this 压入栈
1 getfield #2 <asmcore/base/Student.infos> //拿到class下的infos变量,这是个 [Ljava/lang/String 类型的
4 iload_1 //将局部变量表 1号 变量 index 压入栈
5 aaload //..., o, i -> ..., o[i]
6 areturn //将 o[i] 返回出去

由于 getInfo 是非静态方法,所以局部变量表的0号局部变量为this引用

2.7 Jumps 跳转指令

指令栈(原先)说明
IFEQ… , ijump if i == 0
IFNE… , ijump if i != 0
IFLT… , ijump if i < 0
IFGE… , ijump if i >= 0
IFGT… , ijump if i > 0
IFLE… , ijump if i <= 0
IF_ICMPEQ… , i , jjump if i == j
IF_ICMPNE… , i , jjump if i != j
IF_ICMPLT… , i , jjump if i < j
IF_ICMPGE… , i , jjump if i >= j
IF_ICMPGT… , i , jjump if i > j
IF_ICMPLE… , i , jjump if i <= j
IF_ACMPEQ… , o , pjump if o == p
IF_ACMPNE… , o , pjump if o != p
IFNULL… , ojump if o == null
IFNONNULL… , ojump if o != null
GOTOjump always
TABLESWITCH… , ijump always
LOOKUPSWITCH… , ijump always

示例:

public class Student {
    public String[] infos;

    public String getInfo(int index){
        if (index < 0 ){
            return "nothing";
        }else{
            return infos[index];
        }
    }
}

编译后,代码优化为:

public class Student {
    public String[] infos;

    public Student() {
    }

    public String getInfo(int index) {
        return index < 0 ? "nothing" : this.infos[index];
    }
}

其中 getInfo(int index) 字节码指令为:

 0 iload_1 //将1号局部变量index压入栈
 1 ifge 7 (+6) //如果栈顶元素大于0,跳转到7行
 4 ldc #2 <nothing> //从常量池中将 "nothing" 压入栈
 6 areturn //将栈顶 “nothing” 返回出去
 7 aload_0	//如果栈顶元素大于0,来到这里,将0号局部变量this压入栈
 8 getfield #3 <asmcore/base/Student.infos> //拿到其field
11 iload_1 //将1号局部变量index压入栈
12 aaload //...,o,i -> ..., o[i]
13 areturn //将栈顶 o[i] 返回出去

在 ASM 中字节码可以表示为:

public getInfo(I)Ljava/lang/String; //方法
   L0
    LINENUMBER 7 L0 //记录行号
    ILOAD 1	//获取1号int类型局部变量 index 压入栈
    IFGE L1 //如果 index >=0 跳转到 L1
   L2
    LINENUMBER 8 L2 //记录行号
    LDC "nothing" //将常量 "nothing" 压入栈
    ARETURN	//栈顶返回引用类型对象
   L1
    LINENUMBER 10 L1 //记录行号
   FRAME SAME	//帧
    ALOAD 0	//获取0号引用类型局部变量 this 压入栈
    GETFIELD asmcore/base/Student.infos : [Ljava/lang/String; //拿到 this 的String[]类型的 infos 字段
    ILOAD 1 //获取1号int类型局部变量 index 压入栈
    AALOAD //...,o,i -> ..., o[i]
    ARETURN //栈顶返回引用类型对象
   L3
    LOCALVARIABLE this Lasmcore/base/Student; L0 L3 0 //局部变量表中变量this索引为0
    LOCALVARIABLE index I L0 L3 1 //局部变量表中index变量索引为1
    MAXSTACK = 2 //操作数栈深度
    MAXLOCALS = 2 //局部变量表长度

2.8 Return 返回指令

指令说明
IRETURN, LRETURN, FRETURN, DRETURN… , a返回数据类型
ARETURN… , o返回引用类型
RETURN返回类型为void
ATHROW…, o抛出异常结束执行

3. MethodVisitor

与 ClassVisitor类似,MethodVisitor也有访问回调顺序:

  1. visitAnnotationDefalt 最多一次

  2. ( visitAnnotation | visitParameterAnnotation | visitAttribute ) 零或多次

  3. ( visitCode

    (visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn | visitLocalVariable | visitLineNumber) 零或多次

    visitMaxs ) 最多一次

  4. visitEnd 固定一次

MethodVisitor 的相关方法为:

abstract class MethodVisitor { // public accessors ommited
    MethodVisitor(int api);
    MethodVisitor(int api, MethodVisitor mv);
    AnnotationVisitor visitAnnotationDefault();
    AnnotationVisitor visitAnnotation(String desc, boolean visible);
    AnnotationVisitor visitParameterAnnotation(int parameter,
                                               String desc, boolean visible);
    void visitAttribute(Attribute attr);
    void visitCode();
    void visitFrame(int type, int nLocal, Object[] local, int nStack,
                    Object[] stack);
    void visitInsn(int opcode);
    void visitIntInsn(int opcode, int operand);
    void visitVarInsn(int opcode, int var);
    void visitTypeInsn(int opcode, String desc);
    void visitFieldInsn(int opc, String owner, String name, String desc);
    void visitMethodInsn(int opc, String owner, String name, String desc);
    void visitInvokeDynamicInsn(String name, String desc, Handle bsm,
                                Object... bsmArgs);
    void visitJumpInsn(int opcode, Label label);
    void visitLabel(Label label);
    void visitLdcInsn(Object cst);
    void visitIincInsn(int var, int increment);
    void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels);
    void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels);
    void visitMultiANewArrayInsn(String desc, int dims);
    void visitTryCatchBlock(Label start, Label end, Label handler,
                            String type);
    void visitLocalVariable(String name, String desc, String signature,
                            Label start, Label end, int index);
    void visitLineNumber(int line, Label start);
    void visitMaxs(int maxStack, int maxLocals);
    void visitEnd();
}

我们发现,annotations 和 attributes 必须要首先访问,然后再访问方法的方法体。按照上述的访问顺序进行访问。visitCode 和 visitMaxs 表征着当前来到方法体的开始、结束位置。和 ClassVisitor 类似, visitEnd 在最后被调用,表征着访问事件的结束。

我们并不能直接访问到方法,而是需要通过一些 MethodVisitor 相关的API 来访问:

  • 首先通过 ClassReader 的 accept 方法来开启访问事件,将事件传递给 ClassVisitor
  • 当访问到方法时,ClassVisitor会被调用 visitMethod 方法,这个方法返回了一个 MethodVisitor实例,接下去进入这个 MethodVisitor进行方法的访问事件的回调
  • MethodVisitor 也把它接收到的所有方法转发给另一个 MethodVisitor 实例,所以它也可以被看做是一个事件过滤器。

最后通常还是会来到事件消费者 ClassWriter,当它接收到 MethodVisitor 传来的方法访问细节时,可能会面临计算帧、计算局部变量表、操作数栈大小的问题。 ASM 为我们提供了几种选择:

  • new Classwriter(0):不做任何自动化计算,程序员必须自己计算帧、局部变量表和操作数栈的大小
  • new ClassWriter(ClassWriter.COMPUTE_MAXS):自动计算局部变量表和操作数栈的大小,你仍然必须调用 visitMaxs,但它的参数将不被使用。
  • new ClassWriter(ClassWriter.COMPUTE_FRAMES):所有东西都被自动计算,你无需调用 visitFrame,但你仍然必须调用 visitMaxs(参数将忽略,不被使用)

自动计算有好处,就是方便了程序员的开发,但是它会带来性能损耗,官方给出:使用 COMPUTE_MAXS 会让性能降低 10% , 使用 COMPUTE_FRAMES 会让性能降低 20% 。

如果我们要自行计算帧,我们要使用 visitFrame(F_NEW, nLocals, locals, nStack, stack),其中 nLocals 和 nStack 是局部变量表和操作数栈的大小, locals 和 stack 是相应的集合。自动计算帧的时候,可能会加载父类到JVM,并通过反射的手段做一些事情,如果你正在生成的几个类相互之间有关联,可能关联的类此时还不存在,会出现自动计算错误。官方提示可以通过重写 getCommonSuperClass方法来解决这个问题。

3.1 生成一个 Method 方法 - ClassWriter+MethodVisitor

假设我们当前有一个类:

public class Bean{
    public int f;
}

我要加一个给f设置值的方法:

public class Bean{
    public int f;
    //添加一个方法:
    public void checkAndSet(int f){
        if(f >= 0){
            this.f = f;
        }else{
            throw new IllegalArgumentException();
        }
    }
}

我们可以在把访问事件分发给 ClassWriter 的时候,模拟分发一个原本不存在的MethodVisitor的访问事件,从而实现让 ClassWriter 添加一个方法的效果,首先根据 ASMPlugin 查看字节码形式,或者 jclasslib查看也行:

使用ASMPlugin查看

public checkAndSet(I)V
   L0
    LINENUMBER 8 L0
    ILOAD 1
    IFLT L1
   L2
    LINENUMBER 9 L2
    ALOAD 0
    ILOAD 1
    PUTFIELD asmcore/base/Bean.f : I
    GOTO L3
   L1
    LINENUMBER 11 L1
   FRAME SAME
    NEW java/lang/IllegalArgumentException
    DUP
    INVOKESPECIAL java/lang/IllegalArgumentException.<init> ()V
    ATHROW
   L3
    LINENUMBER 13 L3
   FRAME SAME
    RETURN
   L4
    LOCALVARIABLE this Lasmcore/base/Bean; L0 L4 0
    LOCALVARIABLE f I L0 L4 1
    MAXSTACK = 2
    MAXLOCALS = 2

使用 jclasslib 查看:

 0 iload_1
 1 iflt 12 (+11)
 4 aload_0
 5 iload_1
 6 putfield #2 <asmcore/base/Bean.f>
 9 goto 20 (+11)
12 new #3 <java/lang/IllegalArgumentException>
15 dup
16 invokespecial #4 <java/lang/IllegalArgumentException.<init>>
19 athrow
20 return

根据 ASMPlugin 的结果,我们来尝试构建MethodVisitor大概的代码结构,分析字节码,其逻辑大概是判断传入参数 f 是否非负,如果是,继续执行字节码,如果不是,跳转到另一个代码块(跳转到另一个label/跳转到另一个代码段起始位置)。需要注意 visitCode为方法代码的开始标志,visitEnd为方法代码的结束标志。

mv.visitCode();//模拟访问代码开始
mv.visitVarInsn(ILOAD, 1);//变量操作,将传入参数f压入操作数栈
Label label = new Label();
mv.visitJumpInsn(IFLT,label);//跳转标志,跳转判断的数字为操作数栈顶元素,如果符合条件,跳入else{}代码块的label标识
//如果上述判断成功,继续执行下面字节码指令
mv.visitVarInsn(ALOAD, 0);//把this压入栈
mv.visitVarInsn(ILOAD, 1);//把传入参数f压入操作数栈(局部变量表中并没有把f剔除,所以可以多次使用,例如压入操作数栈)
mv.visitFieldInsn(PUTFIELD,"asmcore/base/Bean","f","I");//访问field字段,Bean类的f字段,描述符为I,意为int类型数据
Label end = new Label();
mv.visitJumpInsn(GOTO, end);//if(){}的代码块执行完成,进入return,return这块代码由end标识
//来到else{}代码块的label部分
mv.visitLabel(label);//打上label标签
mv.visitFrame(F_SAME, 0, null, 0, null);//自行计算帧
mv.visitTypeInsn(NEW,"java/lang/IllegalArgumentException");//类相关,new一个对象
mv.visitInsn(DUP);//在操作数栈中复制一份栈顶元素
mv.visitMethodInsn(INVOKESPECIAL,"java/lang/IllegalArgumentException","<init>","()V");//取出栈顶元素,调用这个类的构造函数,得到的对象放入操作数栈顶
mv.visitInsn(ATHROW);//无参指令,ATHROW将栈顶对象作为异常对象抛出
//来到return代码块的label部分
mv.visitLabel(end);
mv.visitFrame(F_SAME, 0, null, 0, null);//自行计算帧
mv.visitInsn(RETURN);//直接返回
mv.visitMaxs(2,2);//局部变量表、操作数栈的深度计算
mv.visitEnd();//方法的代码段结束

我们来实践一下:

  1. ClassReader读取 Bean 类
  2. 将事件传递给 ClassVisitor,其中这个ClassVisitor的visitEnd()中,模拟转发这个方法访问事件的转发,交给ClassWriter去消费/记录

首先,我们写一个 AddMethodAdapter 用来添加模拟转发方法事件,且将模拟转发方法访问事件设计在ClassVisitor的 visitEnd 调用返回之前完成。

public class AddMethodAdapter extends ClassVisitor {
    public AddMethodAdapter(ClassVisitor downstream) {
        super(ASM4,downstream);
    }

    @Override
    public void visitEnd() {
        //模拟添加一个方法
        MethodVisitor mv = super.visitMethod(0x1,"checkAndSet","(I)V",null,new String[]{});
        mv.visitCode();//模拟访问代码开始
        mv.visitVarInsn(ILOAD, 1);//变量操作,将传入参数f压入操作数栈
        Label label = new Label();
        mv.visitJumpInsn(IFLT,label);//跳转标志,跳转判断的数字为操作数栈顶元素,如果符合条件,跳入else{}代码块的label标识
//如果上述判断成功,继续执行下面字节码指令
        mv.visitVarInsn(ALOAD, 0);//把this压入栈
        mv.visitVarInsn(ILOAD, 1);//把传入参数f压入操作数栈(局部变量表中并没有把f剔除,所以可以多次使用,例如压入操作数栈)
        mv.visitFieldInsn(PUTFIELD,"asmcore/base/Bean","f","I");//访问field字段,Bean类的f字段,描述符为I,意为int类型数据
        Label end = new Label();
        mv.visitJumpInsn(GOTO, end);//if(){}的代码块执行完成,进入return,return这块代码由end标识
//来到else{}代码块的label部分
        mv.visitLabel(label);//打上label标签
        mv.visitFrame(F_SAME, 0, null, 0, null);//自行计算帧
        mv.visitTypeInsn(NEW,"java/lang/IllegalArgumentException");//类相关,new一个对象
        mv.visitInsn(DUP);//在操作数栈中复制一份栈顶元素
        mv.visitMethodInsn(INVOKESPECIAL,"java/lang/IllegalArgumentException","<init>","()V",false);//取出栈顶元素,调用这个类的构造函数,得到的对象放入操作数栈顶
        mv.visitInsn(ATHROW);//无参指令,ATHROW将栈顶对象作为异常对象抛出
//来到return代码块的label部分
        mv.visitLabel(end);
        mv.visitFrame(F_SAME, 0, null, 0, null);//自行计算帧
        mv.visitInsn(RETURN);//直接返回
        mv.visitMaxs(2,2);//局部变量表、操作数栈的深度计算
        mv.visitEnd();//方法的代码段结束
        super.visitEnd();
    }
}

接下去就可以按我们的老套路,开始利用这个代码增强的性质,进行链式转发:

ClassWriter cw=  new ClassWriter(0);
AddMethodAdapter addMethodAdapter = new AddMethodAdapter(cw);
try {
    ClassReader cr = new ClassReader("asmcore.base.Bean");
    cr.accept(addMethodAdapter,0);
    byte[] b = cw.toByteArray();
    //将byte[]写入文件
    save(b,"Bean");
} catch (IOException e) {
    e.printStackTrace();
}

最后我们查看生成的字节码文件的反编译结果:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package asmcore.base;

public class Bean {
    public int f;

    public Bean() {
    }

    public void checkAndSet(int var1) {
        if (var1 >= 0) {
            this.f = var1;
        } else {
            throw new IllegalArgumentException();
        }
    }
}

符合我们的预期。

3.2 转变方法 Transforming Methods

除了增加、删除一个方法,我们可能还要对原有方法进行修改,例如在原有代码的基础上进行代码增强。我们尝试在方法的开头和结束位置加上一个执行时间的记录。假设我们有一个工具类,其中代码增强部分为该方法的耗时计算:

public class Util {
    //耗时操作时间记录
    public void doSomething() throws Exception {
        //----增加的代码begin----
        long begin = System.currentTimeMillis();
        //----增加的代码end----
        Thread.sleep(100);
        //----增加的代码begin----
        System.out.println(System.currentTimeMillis() - begin);
        //----增加的代码end----
    }
}

我们来看一下这个字节码是什么样的,先来看一下没有增加代码的时候的情况:

public doSomething()V throws java/lang/Exception 
   L0
    LINENUMBER 8 L0
    LDC 100
    INVOKESTATIC java/lang/Thread.sleep (J)V
   L1
    LINENUMBER 10 L1
    RETURN
   L2
    LOCALVARIABLE this Lasmcore/base/Util; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

然后是增加代码之后的情况,在其中标注出了新增加的内容:

public doSomething()V throws java/lang/Exception 
   L0
    //新代码
    LINENUMBER 7 L0
    //新增加了方法调用
    INVOKESTATIC java/lang/System.currentTimeMillis ()J
    //将返回值存入var1(也就是 begin 这个本地变量)
    LSTORE 1
   L1
    //原有代码
    LINENUMBER 8 L1
    LDC 100
    INVOKESTATIC java/lang/Thread.sleep (J)V
   L2
    //新代码
    LINENUMBER 9 L2
    //获取PrintStream对象
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
	//调用System.currentMillis
    INVOKESTATIC java/lang/System.currentTimeMillis ()J
    //将begin的值存入操作数栈
    LLOAD 1
    //操作数栈减法
    LSUB
    //调用打印(传入参数就一个,就是操作数栈栈顶元素)
    INVOKEVIRTUAL java/io/PrintStream.println (J)V
   L3
    //原有代码
    LINENUMBER 10 L3
    RETURN
   L4
    //局部变量表信息
    LOCALVARIABLE this Lasmcore/base/Util; L0 L4 0
    LOCALVARIABLE begin J L1 L4 1
    MAXSTACK = 5
    MAXLOCALS = 3

由于这里引入了新的局部变量,所以我们需要处理局部变量表,这部分我们直接使用 LocalVariablesSorter 这个封装好的 MethodVisitor 的实现类,帮我们处理局部变量。ClassWriter 使用 ClassWriter.COMPUTE_MAXS 来自动计算局部变量表。其他部分尽量使用原生API,保持与上文描述一致:

public class AddTimerAdapter extends ClassVisitor {
    public AddTimerAdapter(ClassVisitor downstream) {
        super(ASM4,downstream);
    }

    //如果是 doSomething 方法,就对这个方法进行代码增强
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        if (name.equals("doSomething")){
            return new TimerMethodVisitor(ASM4,access,descriptor,cv.visitMethod(access, name, descriptor, signature, exceptions));
        }
        return cv.visitMethod(access, name, descriptor, signature, exceptions);
    }

    public static class TimerMethodVisitor extends LocalVariablesSorter {

        int beginIndex;

        protected TimerMethodVisitor(int api, int access, String descriptor, MethodVisitor methodVisitor) {
            super(api, access, descriptor, methodVisitor);
        }


        @Override
        public void visitCode() {
            //在原来MethodVisitor的基础上,在代码开始的地方插入内容
            mv.visitCode();
            //调用System.currentMillis方法
            mv.visitMethodInsn(INVOKESTATIC,"java/lang/System","currentMillis","()J",false);
            //由于这里无法处理局部变量表、操作数栈的下标,所以需要让Writer自己去做
            //借助 AdviceAdapter 帮我们封装好新增局部变量的方法进行新增局部变量
            beginIndex = newLocal(Type.LONG_TYPE);//index为本地变量下标
            //存入操作数栈
            mv.visitVarInsn(LSTORE,beginIndex);
        }

        //在方法退出之前计算时间
        @Override
        public void visitInsn(int opcode) {
            //需要注意的是owner给的是类,descriptor是描述符
            //如果是return,或者是throw exception,就提前打印时间
            if ((opcode >= IRETURN && opcode <= RETURN ) || opcode == ATHROW){
                //获取printStream对象,存入操作数栈
                mv.visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
                //调用System.currentMillis方法
                mv.visitMethodInsn(INVOKESTATIC,"java/lang/System","currentMillis","()J",false);
                //将begin的值存入操作数栈
                mv.visitVarInsn(LLOAD,beginIndex);
                //操作数栈减法
                mv.visitInsn(LSUB);
                //调用打印
                mv.visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println","(J)V",false);
            }
            super.visitInsn(opcode);
        }

        @Override
        public void visitMaxs(int maxStack, int maxLocals) {
            //其实这里没有用了,因为ClassWriter设置了 COMPUTE_MAXS
            super.visitMaxs(maxStack+2, maxLocals);
        }
    }
}

我们来调用一下,查看结果:

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
AddTimerAdapter addTimerAdapter = new AddTimerAdapter(cw);
try {
    ClassReader cr = new ClassReader("asmcore.base.Util");
    cr.accept(addTimerAdapter,0);
    byte[] b = cw.toByteArray();
    save(b,"Util");
} catch (IOException e) {
    e.printStackTrace();
}

运行后得到的字节码为:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package asmcore.base;

public class Util {
    public Util() {
    }

    public void doSomething() throws Exception {
        long var1 = System.currentMillis();
        Thread.sleep(100L);
        System.out.println(System.currentMillis() - var1);
    }
}

符合预期。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/442481.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

29.Linux网络编程

把昨天的 第二天的内容说一下&#xff0c;复习一下&#xff0c;第二天 讲的东西不算多&#xff0c;但是有两个作业题来写一写&#xff0c; 大致浏览一下&#xff0c;三次握手 四次挥手的过程&#xff0c;大家有没有画一下&#xff1f; 能画出来吗&#xff1f;同学们&#xff0…

前后端分离开发、Yapi、Swagger、项目部署

一、前后端分离开发 1.1、介绍 前后端分离开发&#xff0c;就是在项目开发过程中&#xff0c;对于前端代码的开发由专门的前端开发人员负责&#xff0c;后端代码则由后端开发人员负责&#xff0c;这样可以做到分工明确、各司其职&#xff0c;提高开发效率&#xff0c;前后端代…

RocketMQ单机环境搭建测试+springboot整合

1.资源下载 官网&#xff1a;下载 | RocketMQ 这里选择使用编译后可以直接用的 下载后解压&#xff1a;略 2.更改配置 主要是更改 conf/broker.conf 的配置&#xff0c;记得添加上下面这几行&#xff0c;否则消息发送失败 autoCreateTopicEnabletrue # 支持自动创建topic…

浅谈日出日落的计算方法以及替代工具 - 日出日落 API

引言 如果你想知道精确的日落日出时间&#xff0c;又或者你想设计一个日出日落时间查询的应用&#xff0c;又或者你只是好奇点进来了&#xff0c;还是可以过来围观一下涨涨知识&#xff0c;今天想跟大家聊一聊的是日出日落的计算方法以及替代工具 - 日出日落 API 。 日出日落…

大数据=SQL Boy,SQL Debug打破SQL Boy 的僵局

网上经常盛传 大数据sql boy&#xff0c;后端开发crud boy&#xff0c;算法工程师调参boy 在大数据领域也工作了好几年了&#xff0c;确实大数据开发&#xff0c;很多工作就是写sql&#xff0c;hive sql、spark sql、flink sql等等sql 一、背景&#xff1a; 但是经常有这样一…

NODEJS安装和vue安装及运行方法以及出现Cannot find module ‘node-sass‘ Require stack问题解决方法

安装nodejs 官网下载&#xff1a; https://registry.npmmirror.com/binary.html?pathnode/选择要下载的版本 一般建议下载msi 选择自己的安装位置一直下一步即可完成 检查一下是否安装成功 打开cmd&#xff0c;输入如下指令 node -vnpm -v输出了版本号就说明安装成功了 …

GCM与CCM的动作过程

CCM CCM&#xff08;Counter with CBC-MAC&#xff09;是一种基于对称加密算法的认证加密&#xff08;Authenticated Encryption&#xff09;模式&#xff0c;结合了CBC-MAC&#xff08;Cipher Block Chaining Message Authentication Code&#xff09;用于消息认证和CTR&…

[java聊天室]服务器发送消息给客户端守护线程同步锁(三)

守护线程 守护线程也称为:后台线程 守护线程是通过普通线程调用setDaemon(boolean on)方法设置而来的,因此创建上与普通线程无异.守护线程的结束时机上有一点与普通线程不同,即:进程的结束.进程结束:当一个进程中的所有普通线程都结束时,进程就会结束,此时会杀掉所有正在运行…

嵌入式Linux(2):将Helloworld驱动编译到内核

文章目录 分析一个例子仿写一个例子&#xff08;需要编译成.ko的&#xff09;写三个文件编辑上一级目录的Makefile文件编辑上一级目录的Kconfig文件make menuconfig进行配置 烧录到开发板上 分析一个例子 例子&#xff1a; source "drivers/redled/Kconfig" config…

简单分享微信怎么添加报名链接的步骤

最近看到很多小伙伴都在问有没有简单的报名链接制作办法&#xff0c;因为最近是暑期活动开展的前期&#xff0c;需要用到很多报名链接&#xff0c;希望可以直接通过微信小程序进行报名&#xff0c;扫一扫微信小程序的二维码就可以进入报名页面&#xff0c;然后制作步骤也是简单…

残差 Gabor 卷积网络和 FV-Mix 指数级数据增强策略用于手指静脉识别

论文背景 手指静脉识别系统的性能受到手指静脉训练样本不足的限制&#xff0c;导致特征学习不足和模型泛化能力弱&#xff1a;DCNN 需要大量的数据来学习更抽象的语义信息进行分类。对于指静脉识别&#xff0c;由于每个类别只包含少量样本&#xff0c;极易出现过拟合。原因之一…

MinIO快速入门

一、MinIO概述 官网地址&#xff1a;http://www.minio.org.cn/ 文档地址&#xff1a;http://docs.minio.org.cn/docs/ MinIO是一款基于Apache License v2.0开源协议的分布式文件系统&#xff08;或者叫对象存储服务&#xff09;&#xff0c;可以做为云存储的解决方案用来保存海…

如何借助测控终端实现设备远程运维?

随着物联网技术的发展&#xff0c;数字化越来越重要。数据是新的生产要素&#xff0c;是基础性资源和战略性资源&#xff0c;也是重要生产力。因此许多企业纷纷转型智慧工厂&#xff0c;但老旧的设备无法获取相应的数据&#xff0c;更换老旧设备的成本又太高&#xff0c;就无法…

【计算机架构】如何计算 CPU 时间

目录 0x00 响应时间和吞吐量&#xff08;Response Time and Throughput&#xff09; 0x01 相对性能&#xff08;Relative Performance&#xff09; 0x02 执行时间测量&#xff08;Measuring Execution Time&#xff09; 0x03 CPU 时钟&#xff08;Clocking&#xff09; 0x…

用docker承载mysql

这两天部署系统到生产服务器&#xff0c;前端后端部署docker是毫无疑义的&#xff0c;但mysql呢&#xff1f; 答案是mysql可以部署到docker。 1、数据文件挂载到宿主机 将mysql部署于docker&#xff0c;会有一个担心&#xff0c;就是docker容器的删除非常的容易&#xff0c;…

修改树莓派系统的更新源,软件安装源和pip安装源

本文目录 1、更换系统更新源2、更改软件源3、更换 pip 源4、更新系统与软件5、附加知识 Linux系统常用的安装源主要有系统更新源和软件安装源二大类&#xff0c;系统更新源是用于对Linux系统本身进行升级更新的&#xff0c;软件安装源是用于通过apt命令安装软件的。随着python的…

【Zigbee】解密Zigbee地址分配——你需要知道的一切

&#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是Zeeland&#xff0c;全栈领域优质创作者。&#x1f4dd; CSDN主页&#xff1a;Zeeland&#x1f525;&#x1f4e3; 我的博客&#xff1a;Zeeland&#x1f4da; Github主页: Undertone0809 (Zeeland) (github.com)&…

URL 转为QR code(二维码)

推荐一个良心的网站&#xff0c;能够免费地将url、text编码为二维码&#xff0c;而且还能设计logo、颜色等。 https://www.the-qrcode-generator.com/ 如下图&#xff1a; 可以自己定义logo、颜色&#xff1a; 还能查看扫描历史等统计信息&#xff1a; 上述所有功能都是免…

【人工智能概论】 RNN、LSTM、GRU简单入门与应用举例

【人工智能概论】 RNN、LSTM、GRU简单入门与应用举例 文章目录 【人工智能概论】 RNN、LSTM、GRU简单入门与应用举例一. RNN简介1.1 概念简介1.2 方法使用简介 二. 编码层embedding2.1 embedding的参数 一. RNN简介 1.1 概念简介 循环神经网络(Recurrent Neural Network)理念…

苹果电容笔值得买吗?ipad电容笔推荐平价

在当今时代&#xff0c;高科技已经成为推动数字产品发展的重要推动力。无论是在工作上&#xff0c;还是在学习上&#xff0c;大屏幕都能起到很好的作用。IPAD将会更好地融入我们的生活&#xff0c;不管是现在还是未来。而ipad配上一支简单的电容笔&#xff0c;不仅可以提高工作…